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');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|