Skip to content

Commit 25999b8

Browse files
committed
chore: wip
1 parent b5a4727 commit 25999b8

File tree

8 files changed

+4436
-45
lines changed

8 files changed

+4436
-45
lines changed

config/email.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,52 @@ export default {
2828
scan: true, // scans for spam and viruses
2929
subdomain: 'mail', // mail.stacksjs.com
3030

31+
/**
32+
* Server mode:
33+
* - 'serverless': Lightweight TypeScript/Bun server (default, ~$3/month)
34+
* - 'server': Full-featured Zig mail server with IMAP, POP3, CalDAV, etc.
35+
*/
36+
mode: (envVars.MAIL_SERVER_MODE || 'serverless') as 'serverless' | 'server',
37+
38+
/**
39+
* Path to the Zig mail server repository (only used when mode is 'server')
40+
* @default '/Users/chrisbreuer/Code/mail' or process.env.MAIL_SERVER_PATH
41+
*/
42+
serverPath: envVars.MAIL_SERVER_PATH || '/Users/chrisbreuer/Code/mail',
43+
3144
storage: {
3245
retentionDays: 90,
3346
archiveAfterDays: 30,
3447
},
3548

3649
// EC2 instance configuration for IMAP/SMTP server
3750
instance: {
38-
type: 't4g.nano', // ~$3/month - sufficient for light use
39-
spot: false, // set to true for ~$1.50/month (can be interrupted)
51+
// For 'serverless' mode: t4g.nano ARM64 (~$3/month) is sufficient
52+
// For 'server' mode: t3.small x86_64 required for Zig binary
53+
type: 't4g.nano',
54+
spot: false, // set to true for ~50% cost savings (can be interrupted)
4055
diskSize: 8, // GB
4156
// keyPair: 'my-key-pair', // optional SSH access
4257
},
4358

4459
ports: {
45-
imap: 993, // IMAP over TLS
46-
smtp: 465, // SMTP over TLS
47-
smtpStartTls: 587, // SMTP with STARTTLS
60+
smtp: 25, // Standard SMTP
61+
smtps: 465, // SMTP over TLS
62+
submission: 587, // SMTP with STARTTLS
63+
imap: 143, // IMAP
64+
imaps: 993, // IMAP over TLS
65+
pop3: 110, // POP3
66+
pop3s: 995, // POP3 over TLS
67+
},
68+
69+
// Features (only available in 'server' mode)
70+
features: {
71+
imap: true,
72+
pop3: true,
73+
webmail: false, // future
74+
calDAV: false, // calendar sync
75+
cardDAV: false, // contacts sync
76+
activeSync: false, // Exchange ActiveSync
4877
},
4978
},
5079

@@ -54,5 +83,5 @@ export default {
5483
complaints: true,
5584
},
5685

57-
default: envVars.MAIL_DRIVER || 'ses',
86+
default: (envVars.MAIL_DRIVER || 'ses') as 'ses' | 'sendgrid' | 'mailgun' | 'mailtrap' | 'smtp' | 'postmark',
5887
} satisfies EmailConfig

config/phone.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const envVars = typeof Bun !== 'undefined' ? Bun.env : process.env
1010
* Powered by Amazon Connect for enterprise-grade telephony.
1111
*/
1212
export default {
13-
enabled: false, // Set to true to enable phone features
13+
enabled: true, // Set to true to enable phone features
1414

1515
provider: 'connect', // Amazon Connect
1616

@@ -21,23 +21,22 @@ export default {
2121
},
2222

2323
phoneNumbers: [
24-
// Example phone number configuration
25-
// {
26-
// type: 'TOLL_FREE',
27-
// countryCode: 'US',
28-
// description: 'Main support line',
29-
// notifyOnCall: ['[email protected]'],
30-
// },
24+
{
25+
type: 'TOLL_FREE',
26+
countryCode: 'US',
27+
description: 'Main business line',
28+
notifyOnCall: ['[email protected]'],
29+
},
3130
],
3231

3332
notifications: {
3433
incomingCall: {
3534
enabled: true,
36-
channels: ['email'], // 'email', 'sms', 'slack', 'webhook'
35+
channels: ['email', 'sms'], // 'email', 'sms', 'slack', 'webhook'
3736
},
3837
missedCall: {
3938
enabled: true,
40-
channels: ['email'],
39+
channels: ['email', 'sms'],
4140
},
4241
voicemail: {
4342
enabled: true,
@@ -49,17 +48,40 @@ export default {
4948
enabled: true,
5049
transcription: true, // Use Amazon Transcribe
5150
maxDurationSeconds: 120,
52-
greeting: 'Please leave a message after the tone.',
51+
greeting: 'Thank you for calling Stacks. Please leave a message after the tone and we will get back to you as soon as possible.',
52+
},
53+
54+
// Call forwarding configuration
55+
forwarding: {
56+
enabled: true,
57+
rules: [
58+
{
59+
name: 'Forward to Chris (Business Hours)',
60+
condition: 'always', // Forward all calls during business hours
61+
forwardTo: '+18088218241',
62+
ringTimeout: 20, // seconds before going to voicemail
63+
priority: 1,
64+
},
65+
{
66+
name: 'Forward to Chris (After Hours)',
67+
condition: 'after_hours', // Forward outside business hours
68+
forwardTo: '+18088218241',
69+
ringTimeout: 15,
70+
priority: 2,
71+
},
72+
],
5373
},
5474

5575
businessHours: {
5676
timezone: 'America/Los_Angeles',
5777
schedule: [
58-
{ day: 'MONDAY', start: '09:00', end: '17:00' },
59-
{ day: 'TUESDAY', start: '09:00', end: '17:00' },
60-
{ day: 'WEDNESDAY', start: '09:00', end: '17:00' },
61-
{ day: 'THURSDAY', start: '09:00', end: '17:00' },
62-
{ day: 'FRIDAY', start: '09:00', end: '17:00' },
78+
{ day: 'MONDAY', start: '11:30', end: '20:00' },
79+
{ day: 'TUESDAY', start: '11:30', end: '20:00' },
80+
{ day: 'WEDNESDAY', start: '11:30', end: '20:00' },
81+
{ day: 'THURSDAY', start: '11:30', end: '20:00' },
82+
{ day: 'FRIDAY', start: '11:30', end: '20:00' },
83+
{ day: 'SATURDAY', start: '11:30', end: '20:00' },
84+
{ day: 'SUNDAY', start: '11:30', end: '20:00' },
6385
],
6486
},
6587
} satisfies PhoneConfig
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
CREATE TABLE "samplemodels" (
2-
2+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
3+
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
4+
"updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP
35
);

storage/framework/core/cloud/src/cloud/mail-server.ts

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,16 @@ export class MailServerStack {
111111
mailPorts.push({ port: ports.imaps || 993, name: 'IMAPS' })
112112
}
113113

114-
// SMTP ports for server mode
114+
// SMTP ports for serverless mode (relay to SES) and server mode
115+
// This allows mail.domain.com to be used as SMTP server with user-friendly credentials
116+
if (mode === 'serverless' || mode === 'server') {
117+
mailPorts.push({ port: ports.submission || 587, name: 'SMTP Submission' })
118+
mailPorts.push({ port: ports.smtps || 465, name: 'SMTPS' })
119+
}
120+
121+
// SMTP port 25 only for server mode (receiving mail directly)
115122
if (mode === 'server') {
116123
mailPorts.push({ port: ports.smtp || 25, name: 'SMTP' })
117-
mailPorts.push({ port: ports.smtps || 465, name: 'SMTPS' })
118-
mailPorts.push({ port: ports.submission || 587, name: 'Submission' })
119124
}
120125

121126
// POP3 if enabled in server mode
@@ -320,7 +325,7 @@ export class MailServerStack {
320325
}
321326

322327
/**
323-
* Build user data for serverless (TypeScript/Bun) IMAP server
328+
* Build user data for serverless (TypeScript/Bun) IMAP and SMTP server
324329
*/
325330
private buildServerlessUserData(
326331
userData: ec2.UserData,
@@ -349,13 +354,44 @@ export class MailServerStack {
349354
imapServerCode = '// IMAP server code will be deployed separately'
350355
}
351356

352-
// Create the startup script
357+
// Read the SMTP server code
358+
const smtpServerPath = join(__dirname, '../imap/smtp-server.ts')
359+
let smtpServerCode = ''
360+
if (existsSync(smtpServerPath)) {
361+
smtpServerCode = readFileSync(smtpServerPath, 'utf-8')
362+
}
363+
else {
364+
console.warn('SMTP server code not found, using placeholder')
365+
smtpServerCode = '// SMTP server code will be deployed separately'
366+
}
367+
368+
// Read the AWS client modules
369+
const clientPath = join(__dirname, '../imap/client.ts')
370+
let clientCode = ''
371+
if (existsSync(clientPath)) {
372+
clientCode = readFileSync(clientPath, 'utf-8')
373+
}
374+
375+
const s3Path = join(__dirname, '../imap/s3.ts')
376+
let s3Code = ''
377+
if (existsSync(s3Path)) {
378+
s3Code = readFileSync(s3Path, 'utf-8')
379+
}
380+
381+
const sesPath = join(__dirname, '../imap/ses.ts')
382+
let sesCode = ''
383+
if (existsSync(sesPath)) {
384+
sesCode = readFileSync(sesPath, 'utf-8')
385+
}
386+
387+
// Create the startup script that starts both IMAP and SMTP servers
353388
const startupScript = `#!/usr/bin/env bun
354389
import * as fs from 'node:fs'
355390
import { startImapServer } from './imap-server'
391+
import { startSmtpServer } from './smtp-server'
356392
357393
async function main() {
358-
console.log('Starting IMAP-to-S3 bridge server...')
394+
console.log('Starting mail server (IMAP + SMTP)...')
359395
360396
const hasTlsCerts = fs.existsSync('/etc/letsencrypt/live/mail.${domain}/privkey.pem')
361397
console.log('TLS certificates available:', hasTlsCerts)
@@ -365,7 +401,13 @@ async function main() {
365401
Object.entries(users).map(([k, v]) => [k, { password: process.env[\`IMAP_PASSWORD_\${k.toUpperCase()}\`] || 'changeme', email: (v as any).email }])
366402
), null, 2).replace(/process\.env\[`IMAP_PASSWORD_([A-Z]+)`\]/g, "process.env.IMAP_PASSWORD_$1 || 'changeme'")}
367403
368-
const server = await startImapServer({
404+
const tlsConfig = hasTlsCerts ? {
405+
key: '/etc/letsencrypt/live/mail.${domain}/privkey.pem',
406+
cert: '/etc/letsencrypt/live/mail.${domain}/fullchain.pem',
407+
} : undefined
408+
409+
// Start IMAP server
410+
const imapServer = await startImapServer({
369411
port: 143,
370412
sslPort: 993,
371413
host: '0.0.0.0',
@@ -374,25 +416,32 @@ async function main() {
374416
prefix: 'incoming/',
375417
domain: '${domain}',
376418
users,
377-
tls: hasTlsCerts ? {
378-
key: '/etc/letsencrypt/live/mail.${domain}/privkey.pem',
379-
cert: '/etc/letsencrypt/live/mail.${domain}/fullchain.pem',
380-
} : undefined,
419+
tls: tlsConfig,
381420
})
382-
383421
console.log('IMAP server running on port 143' + (hasTlsCerts ? ' and 993 (TLS)' : ''))
384422
385-
process.on('SIGINT', async () => {
386-
console.log('Shutting down...')
387-
await server.stop()
388-
process.exit(0)
423+
// Start SMTP server (relay to SES)
424+
const smtpServer = await startSmtpServer({
425+
port: 587,
426+
tlsPort: 465,
427+
host: '0.0.0.0',
428+
region: '${Stack.of({ node: { id: 'scope' } } as any).region || 'us-east-1'}',
429+
domain: '${domain}',
430+
users,
431+
tls: tlsConfig,
432+
sentBucket: '${this.mailBucket.bucketName}',
433+
sentPrefix: 'sent/',
389434
})
435+
console.log('SMTP server running on port 587' + (hasTlsCerts ? ' and 465 (TLS)' : ''))
390436
391-
process.on('SIGTERM', async () => {
437+
const shutdown = async () => {
392438
console.log('Shutting down...')
393-
await server.stop()
439+
await Promise.all([imapServer.stop(), smtpServer.stop()])
394440
process.exit(0)
395-
})
441+
}
442+
443+
process.on('SIGINT', shutdown)
444+
process.on('SIGTERM', shutdown)
396445
}
397446
398447
main().catch(console.error)
@@ -421,6 +470,26 @@ main().catch(console.error)
421470
imapServerCode,
422471
'EOFIMAPSERVER',
423472
'',
473+
'# Write SMTP server code',
474+
`cat > /opt/imap-server/smtp-server.ts << 'EOFSMTPSERVER'`,
475+
smtpServerCode,
476+
'EOFSMTPSERVER',
477+
'',
478+
'# Write AWS client code',
479+
`cat > /opt/imap-server/client.ts << 'EOFCLIENT'`,
480+
clientCode,
481+
'EOFCLIENT',
482+
'',
483+
'# Write S3 client code',
484+
`cat > /opt/imap-server/s3.ts << 'EOFS3'`,
485+
s3Code,
486+
'EOFS3',
487+
'',
488+
'# Write SES client code',
489+
`cat > /opt/imap-server/ses.ts << 'EOFSES'`,
490+
sesCode,
491+
'EOFSES',
492+
'',
424493
'# Write startup script',
425494
`cat > /opt/imap-server/server.ts << 'EOFSERVER'`,
426495
startupScript,
@@ -429,10 +498,10 @@ main().catch(console.error)
429498
'# Obtain TLS certificate with Let\'s Encrypt (standalone mode)',
430499
`certbot certonly --standalone --non-interactive --agree-tos --email admin@${domain} -d mail.${domain} || echo "Certificate already exists or failed to obtain"`,
431500
'',
432-
'# Create systemd service',
501+
'# Create systemd service for mail server (IMAP + SMTP)',
433502
'cat > /etc/systemd/system/imap-server.service << EOF',
434503
'[Unit]',
435-
'Description=IMAP-to-S3 Bridge Server',
504+
'Description=Mail Server (IMAP + SMTP)',
436505
'After=network.target',
437506
'',
438507
'[Service]',
@@ -458,7 +527,7 @@ main().catch(console.error)
458527
'echo "0 0,12 * * * root certbot renew --quiet && systemctl restart imap-server" > /etc/cron.d/certbot-renew',
459528
'',
460529
'# Log completion',
461-
'echo "IMAP server setup complete" >> /var/log/imap-server-setup.log',
530+
'echo "Mail server (IMAP + SMTP) setup complete" >> /var/log/imap-server-setup.log',
462531
)
463532
}
464533

0 commit comments

Comments
 (0)