@@ -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
354389import * as fs from 'node:fs'
355390import { startImapServer } from './imap-server'
391+ import { startSmtpServer } from './smtp-server'
356392
357393async 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 ( / p r o c e s s \. e n v \[ ` I M A P _ P A S S W O R D _ ( [ 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
398447main().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