174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
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<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!');
|
|
}
|
|
}
|