import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { Email } from './mta.classes.email.js'; import { EmailSignJob } from './mta.classes.emailsignjob.js'; import type { MTA } from './mta.classes.mta.js'; export class EmailSendJob { mtaRef: MTA; private email: Email; private socket: plugins.net.Socket | plugins.tls.TLSSocket = null; private mxRecord: string = null; constructor(mtaRef: MTA, emailArg: Email) { this.email = emailArg; this.mtaRef = mtaRef; } async send(): Promise { const domain = this.email.to.split('@')[1]; const addresses = await this.resolveMx(domain); addresses.sort((a, b) => a.priority - b.priority); this.mxRecord = addresses[0].exchange; console.log(`Using ${this.mxRecord} as mail server for domain ${domain}`); this.socket = plugins.net.connect(25, this.mxRecord); await this.processInitialResponse(); await this.sendCommand(`EHLO ${this.email.from.split('@')[1]}\r\n`, '250'); try { await this.sendCommand('STARTTLS\r\n', '220'); this.socket = plugins.tls.connect({ socket: this.socket, rejectUnauthorized: false }); await this.processTLSUpgrade(this.email.from.split('@')[1]); } catch (error) { console.log('Error sending STARTTLS command:', error); console.log('Continuing with unencrypted connection...'); } await this.sendMessage(); } private resolveMx(domain: string): Promise { return new Promise((resolve, reject) => { plugins.dns.resolveMx(domain, (err, addresses) => { if (err) { console.error('Error resolving MX:', err); reject(err); } else { resolve(addresses); } }); }); } private processInitialResponse(): Promise { return new Promise((resolve, reject) => { this.socket.once('data', (data) => { const response = data.toString(); if (!response.startsWith('220')) { console.error('Unexpected initial server response:', response); reject(new Error(`Unexpected initial server response: ${response}`)); } else { console.log('Received initial server response:', response); console.log('Connected to server, sending EHLO...'); resolve(); } }); }); } private processTLSUpgrade(domain: string): Promise { return new Promise((resolve, reject) => { this.socket.once('secureConnect', async () => { console.log('TLS started successfully'); try { await this.sendCommand(`EHLO ${domain}\r\n`, '250'); resolve(); } catch (err) { console.error('Error sending EHLO after TLS upgrade:', err); reject(err); } }); }); } private sendCommand(command: string, expectedResponseCode?: string): Promise { return new Promise((resolve, reject) => { this.socket.write(command, (error) => { if (error) { reject(error); return; } if (!expectedResponseCode) { resolve(); return; } this.socket.once('data', (data) => { const response = data.toString(); if (response.startsWith('221')) { this.socket.destroy(); resolve(); } if (!response.startsWith(expectedResponseCode)) { reject(new Error(`Unexpected server response: ${response}`)); } else { resolve(); } }); }); }); } private async sendMessage(): Promise { console.log('Preparing email message...'); const messageId = `<${plugins.uuid.v4()}@${this.email.from.split('@')[1]}>`; // Create a boundary for the email parts const boundary = '----=_NextPart_' + plugins.uuid.v4(); const headers = { From: this.email.from, To: this.email.to, Subject: this.email.subject, 'Content-Type': `multipart/mixed; boundary="${boundary}"`, }; // Construct the body of the message let body = `--${boundary}\r\nContent-Type: text/html; charset=utf-8\r\n\r\n${this.email.text}\r\n`; // Then, the attachments for (let attachment of this.email.attachments) { body += `--${boundary}\r\nContent-Type: ${attachment.contentType}; name="${attachment.filename}"\r\n`; body += 'Content-Transfer-Encoding: base64\r\n'; body += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n\r\n`; body += attachment.content.toString('base64') + '\r\n'; } // End of email body += `--${boundary}--\r\n`; // Create an instance of DKIMSigner const dkimSigner = new EmailSignJob(this.mtaRef, { domain: this.email.getFromDomain(), // Replace with your domain selector: `mta`, // Replace with your DKIM selector headers: headers, body: body, }); // Construct the message with DKIM-Signature header let message = `Message-ID: ${messageId}\r\nFrom: ${this.email.from}\r\nTo: ${this.email.to}\r\nSubject: ${this.email.subject}\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`; message += body; let signatureHeader = await dkimSigner.getSignatureHeader(message); message = `${signatureHeader}${message}`; plugins.smartfile.fs.ensureDirSync(paths.sentEmailsDir); plugins.smartfile.memory.toFsSync(message, plugins.path.join(paths.sentEmailsDir, `${Date.now()}.eml`)); // Adding necessary commands before sending the actual email message await this.sendCommand(`MAIL FROM:<${this.email.from}>\r\n`, '250'); await this.sendCommand(`RCPT TO:<${this.email.to}>\r\n`, '250'); await this.sendCommand(`DATA\r\n`, '354'); // Now send the message content await this.sendCommand(message); await this.sendCommand('\r\n.\r\n', '250'); await this.sendCommand('QUIT\r\n', '221'); console.log('Email message sent successfully!'); } }