platformservice/ts/mta/mta.classes.emailsendjob.ts

174 lines
5.9 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';
import { EmailSignJob } from './mta.classes.emailsignjob.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 class EmailSendJob {
2024-02-16 19:42:26 +00:00
mtaRef: MtaService;
2024-02-16 12:28:40 +00:00
private email: Email;
private socket: plugins.net.Socket | plugins.tls.TLSSocket = null;
private mxRecord: string = null;
2024-02-16 19:42:26 +00:00
constructor(mtaRef: MtaService, emailArg: Email) {
2024-02-16 12:28:40 +00:00
this.email = emailArg;
this.mtaRef = mtaRef;
}
async send(): Promise<void> {
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<plugins.dns.MxRecord[]> {
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<void> {
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<void> {
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<void> {
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<void> {
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!');
}
}