import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { Email } from './mta.classes.email.js'; import type { MTA } from './mta.classes.mta.js'; export interface ISmtpServerOptions { port: number; key: string; cert: string; } export class SMTPServer { public mtaRef: MTA; private smtpServerOptions: ISmtpServerOptions; private server: plugins.net.Server; private emailBufferStringMap: Map; constructor(mtaRefArg: MTA, optionsArg: ISmtpServerOptions) { console.log('SMTPServer instance is being created...'); this.mtaRef = mtaRefArg; this.smtpServerOptions = optionsArg; this.emailBufferStringMap = new Map(); this.server = plugins.net.createServer((socket) => { console.log('New connection established...'); socket.write('220 mta.lossless.one ESMTP Postfix\r\n'); socket.on('data', (data) => { this.processData(socket, data); }); socket.on('end', () => { console.log('Socket closed. Deleting related emailBuffer...'); socket.destroy(); this.emailBufferStringMap.delete(socket); }); socket.on('error', () => { console.error('Socket error occurred. Deleting related emailBuffer...'); socket.destroy(); this.emailBufferStringMap.delete(socket); }); socket.on('close', () => { console.log('Connection was closed by the client'); socket.destroy(); this.emailBufferStringMap.delete(socket); }); }); } private startTLS(socket: plugins.net.Socket) { const secureContext = plugins.tls.createSecureContext({ key: this.smtpServerOptions.key, cert: this.smtpServerOptions.cert, }); const tlsSocket = new plugins.tls.TLSSocket(socket, { secureContext: secureContext, isServer: true, }); tlsSocket.on('secure', () => { console.log('Connection secured.'); this.emailBufferStringMap.set(tlsSocket, this.emailBufferStringMap.get(socket) || ''); this.emailBufferStringMap.delete(socket); }); // Use the same handler for the 'data' event as for the unsecured socket. tlsSocket.on('data', (data: Buffer) => { this.processData(tlsSocket, Buffer.from(data)); }); tlsSocket.on('end', () => { console.log('TLS socket closed. Deleting related emailBuffer...'); this.emailBufferStringMap.delete(tlsSocket); }); tlsSocket.on('error', (err) => { console.error('TLS socket error occurred. Deleting related emailBuffer...'); this.emailBufferStringMap.delete(tlsSocket); }); } private processData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: Buffer) { const dataString = data.toString(); console.log(`Received data:`); console.log(`${dataString}`) if (dataString.startsWith('EHLO')) { socket.write('250-mta.lossless.one Hello\r\n250 STARTTLS\r\n'); } else if (dataString.startsWith('MAIL FROM')) { socket.write('250 Ok\r\n'); } else if (dataString.startsWith('RCPT TO')) { socket.write('250 Ok\r\n'); } else if (dataString.startsWith('STARTTLS')) { socket.write('220 Ready to start TLS\r\n'); this.startTLS(socket); } else if (dataString.startsWith('DATA')) { socket.write('354 End data with .\r\n'); let emailBuffer = this.emailBufferStringMap.get(socket); if (!emailBuffer) { this.emailBufferStringMap.set(socket, ''); } } else if (dataString.startsWith('QUIT')) { socket.write('221 Bye\r\n'); console.log('Received QUIT command, closing the socket...'); socket.destroy(); this.parseEmail(socket); } else { let emailBuffer = this.emailBufferStringMap.get(socket); if (typeof emailBuffer === 'string') { emailBuffer += dataString; this.emailBufferStringMap.set(socket, emailBuffer); } socket.write('250 Ok\r\n'); } if (dataString.endsWith('\r\n.\r\n') ) { // End of data console.log('Received end of data.'); } } private async parseEmail(socket: plugins.net.Socket | plugins.tls.TLSSocket) { let emailData = this.emailBufferStringMap.get(socket); // lets strip the end sequence emailData = emailData?.replace(/\r\n\.\r\n$/, ''); plugins.smartfile.fs.ensureDirSync(paths.receivedEmailsDir); plugins.smartfile.memory.toFsSync(emailData, plugins.path.join(paths.receivedEmailsDir, `${Date.now()}.eml`)); if (!emailData) { console.error('No email data found for socket.'); return; } let mightBeSpam = false; // Verifying the email with DKIM try { const isVerified = await this.mtaRef.dkimVerifier.verify(emailData); mightBeSpam = !isVerified; } catch (error) { console.error('Failed to verify DKIM signature:', error); mightBeSpam = true; } const parsedEmail = await plugins.mailparser.simpleParser(emailData); console.log(parsedEmail) const email = new Email({ from: parsedEmail.from?.value[0].address || '', to: parsedEmail.to instanceof Array ? parsedEmail.to[0].value[0].address : parsedEmail.to?.value[0].address, subject: parsedEmail.subject || '', text: parsedEmail.html || parsedEmail.text, attachments: parsedEmail.attachments?.map((attachment) => ({ filename: attachment.filename || '', content: attachment.content, contentType: attachment.contentType, })) || [], mightBeSpam: mightBeSpam, }); console.log('mail received!'); console.log(email); this.emailBufferStringMap.delete(socket); } public start() { this.server.listen(this.smtpServerOptions.port, () => { console.log(`SMTP Server is now running on port ${this.smtpServerOptions.port}`); }); } public stop() { this.server.getConnections((err, count) => { if (err) throw err; console.log('Number of active connections: ', count); }); this.server.close(() => { console.log('SMTP Server is now stopped'); }); } }