platformservice/ts/mta/mta.classes.smtpserver.ts

192 lines
6.0 KiB
TypeScript
Raw Normal View History

2024-02-16 12:41:04 +00:00
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
2024-02-16 12:28:40 +00:00
import { Email } from './mta.classes.email.js';
2024-02-16 19:42:26 +00:00
import type { MtaService } from './mta.classes.mta.js';
2024-02-16 12:28:40 +00:00
export interface ISmtpServerOptions {
port: number;
key: string;
cert: string;
}
export class SMTPServer {
2024-02-16 19:42:26 +00:00
public mtaRef: MtaService;
2024-02-16 12:28:40 +00:00
private smtpServerOptions: ISmtpServerOptions;
private server: plugins.net.Server;
private emailBufferStringMap: Map<plugins.net.Socket, string>;
2024-02-16 19:42:26 +00:00
constructor(mtaRefArg: MtaService, optionsArg: ISmtpServerOptions) {
2024-02-16 12:28:40 +00:00
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 <CR><LF>.<CR><LF>\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');
});
}
}