fix(core): update
This commit is contained in:
8
ts/mta/index.ts
Normal file
8
ts/mta/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export * from './mta.classes.dkimcreator.js';
|
||||
export * from './mta.classes.emailsignjob.js';
|
||||
export * from './mta.classes.dkimverifier.js';
|
||||
export * from './mta.classes.mta.js';
|
||||
export * from './mta.classes.smtpserver.js';
|
||||
export * from './mta.classes.emailsendjob.js';
|
||||
export * from './mta.classes.mta.js';
|
||||
export * from './mta.classes.email.js';
|
7
ts/mta/mta.classes.apimanager.ts
Normal file
7
ts/mta/mta.classes.apimanager.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class ApiManager {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
|
||||
}
|
119
ts/mta/mta.classes.dkimcreator.ts
Normal file
119
ts/mta/mta.classes.dkimcreator.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
|
||||
import { Email } from './mta.classes.email.js';
|
||||
import type { MTA } from './mta.classes.mta.js';
|
||||
|
||||
const readFile = plugins.util.promisify(plugins.fs.readFile);
|
||||
const writeFile = plugins.util.promisify(plugins.fs.writeFile);
|
||||
const generateKeyPair = plugins.util.promisify(plugins.crypto.generateKeyPair);
|
||||
|
||||
export interface IKeyPaths {
|
||||
privateKeyPath: string;
|
||||
publicKeyPath: string;
|
||||
}
|
||||
|
||||
export class DKIMCreator {
|
||||
private keysDir: string;
|
||||
|
||||
constructor(metaRef: MTA, keysDir = paths.keysDir) {
|
||||
this.keysDir = keysDir;
|
||||
}
|
||||
|
||||
public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
|
||||
return {
|
||||
privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
|
||||
publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if a DKIM key is present and creates one and stores it to disk otherwise
|
||||
public async handleDKIMKeysForDomain(domainArg: string): Promise<void> {
|
||||
try {
|
||||
await this.readDKIMKeys(domainArg);
|
||||
} catch (error) {
|
||||
console.log(`No DKIM keys found for ${domainArg}. Generating...`);
|
||||
await this.createAndStoreDKIMKeys(domainArg);
|
||||
const dnsValue = await this.getDNSRecordForDomain(domainArg);
|
||||
plugins.smartfile.fs.ensureDirSync(paths.dnsRecordsDir);
|
||||
plugins.smartfile.memory.toFsSync(JSON.stringify(dnsValue, null, 2), plugins.path.join(paths.dnsRecordsDir, `${domainArg}.dkimrecord.json`));
|
||||
}
|
||||
}
|
||||
|
||||
public async handleDKIMKeysForEmail(email: Email): Promise<void> {
|
||||
const domain = email.from.split('@')[1];
|
||||
await this.handleDKIMKeysForDomain(domain);
|
||||
}
|
||||
|
||||
// Read DKIM keys from disk
|
||||
public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
|
||||
const keyPaths = await this.getKeyPathsForDomain(domainArg);
|
||||
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
|
||||
readFile(keyPaths.privateKeyPath),
|
||||
readFile(keyPaths.publicKeyPath),
|
||||
]);
|
||||
|
||||
// Convert the buffers to strings
|
||||
const privateKey = privateKeyBuffer.toString();
|
||||
const publicKey = publicKeyBuffer.toString();
|
||||
|
||||
return { privateKey, publicKey };
|
||||
}
|
||||
|
||||
// Create a DKIM key pair
|
||||
private async createDKIMKeys(): Promise<{ privateKey: string; publicKey: string }> {
|
||||
const { privateKey, publicKey } = await generateKeyPair('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
});
|
||||
|
||||
return { privateKey, publicKey };
|
||||
}
|
||||
|
||||
// Store a DKIM key pair to disk
|
||||
private async storeDKIMKeys(
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
privateKeyPath: string,
|
||||
publicKeyPath: string
|
||||
): Promise<void> {
|
||||
await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
|
||||
}
|
||||
|
||||
// Create a DKIM key pair and store it to disk
|
||||
private async createAndStoreDKIMKeys(domain: string): Promise<void> {
|
||||
const { privateKey, publicKey } = await this.createDKIMKeys();
|
||||
const keyPaths = await this.getKeyPathsForDomain(domain);
|
||||
await this.storeDKIMKeys(
|
||||
privateKey,
|
||||
publicKey,
|
||||
keyPaths.privateKeyPath,
|
||||
keyPaths.publicKeyPath
|
||||
);
|
||||
console.log(`DKIM keys for ${domain} created and stored.`);
|
||||
}
|
||||
|
||||
private async getDNSRecordForDomain(domainArg: string): Promise<plugins.tsclass.network.IDnsRecord> {
|
||||
await this.handleDKIMKeysForDomain(domainArg);
|
||||
const keys = await this.readDKIMKeys(domainArg);
|
||||
|
||||
// Remove the PEM header and footer and newlines
|
||||
const pemHeader = '-----BEGIN PUBLIC KEY-----';
|
||||
const pemFooter = '-----END PUBLIC KEY-----';
|
||||
const keyContents = keys.publicKey
|
||||
.replace(pemHeader, '')
|
||||
.replace(pemFooter, '')
|
||||
.replace(/\n/g, '');
|
||||
|
||||
// Now generate the DKIM DNS TXT record
|
||||
const dnsRecordValue = `v=DKIM1; h=sha256; k=rsa; p=${keyContents}`;
|
||||
|
||||
return {
|
||||
name: `mta._domainkey.${domainArg}`,
|
||||
type: 'TXT',
|
||||
dnsSecEnabled: null,
|
||||
value: dnsRecordValue,
|
||||
};
|
||||
}
|
||||
}
|
35
ts/mta/mta.classes.dkimverifier.ts
Normal file
35
ts/mta/mta.classes.dkimverifier.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { MTA } from './mta.classes.mta.js';
|
||||
|
||||
class DKIMVerifier {
|
||||
public mtaRef: MTA;
|
||||
|
||||
constructor(mtaRefArg: MTA) {
|
||||
this.mtaRef = mtaRefArg;
|
||||
}
|
||||
|
||||
async verify(email: string): Promise<boolean> {
|
||||
console.log('Trying to verify DKIM now...');
|
||||
|
||||
try {
|
||||
const verification = await plugins.mailauth.authenticate(email, {
|
||||
/* resolver: (...args) => {
|
||||
console.log(args);
|
||||
} */
|
||||
});
|
||||
console.log(verification);
|
||||
if (verification && verification.dkim.results[0].status.result === 'pass') {
|
||||
console.log('DKIM Verification result: pass');
|
||||
return true;
|
||||
} else {
|
||||
console.error('DKIM Verification failed:', verification?.error || 'Unknown error');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DKIM Verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { DKIMVerifier };
|
10
ts/mta/mta.classes.dnsmanager.ts
Normal file
10
ts/mta/mta.classes.dnsmanager.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { MTA } from './mta.classes.mta.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class DNSManager {
|
||||
public mtaRef: MTA;
|
||||
|
||||
constructor(mtaRefArg: MTA) {
|
||||
this.mtaRef = mtaRefArg;
|
||||
}
|
||||
}
|
36
ts/mta/mta.classes.email.ts
Normal file
36
ts/mta/mta.classes.email.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export interface IAttachment {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface IEmailOptions {
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
attachments: IAttachment[];
|
||||
mightBeSpam?: boolean;
|
||||
}
|
||||
|
||||
export class Email {
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
attachments: IAttachment[];
|
||||
mightBeSpam: boolean;
|
||||
|
||||
constructor(options: IEmailOptions) {
|
||||
this.from = options.from;
|
||||
this.to = options.to;
|
||||
this.subject = options.subject;
|
||||
this.text = options.text;
|
||||
this.attachments = options.attachments;
|
||||
this.mightBeSpam = options.mightBeSpam || false;
|
||||
}
|
||||
|
||||
public getFromDomain() {
|
||||
return this.from.split('@')[1]
|
||||
}
|
||||
}
|
173
ts/mta/mta.classes.emailsendjob.ts
Normal file
173
ts/mta/mta.classes.emailsendjob.ts
Normal file
@ -0,0 +1,173 @@
|
||||
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!');
|
||||
}
|
||||
}
|
69
ts/mta/mta.classes.emailsignjob.ts
Normal file
69
ts/mta/mta.classes.emailsignjob.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { MTA } from './mta.classes.mta.js';
|
||||
|
||||
interface Headers {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface IEmailSignJobOptions {
|
||||
domain: string;
|
||||
selector: string;
|
||||
headers: Headers;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class EmailSignJob {
|
||||
mtaRef: MTA;
|
||||
jobOptions: IEmailSignJobOptions;
|
||||
|
||||
constructor(mtaRefArg: MTA, options: IEmailSignJobOptions) {
|
||||
this.mtaRef = mtaRefArg;
|
||||
this.jobOptions = options;
|
||||
}
|
||||
|
||||
async loadPrivateKey(): Promise<string> {
|
||||
return plugins.fs.promises.readFile(
|
||||
(await this.mtaRef.dkimCreator.getKeyPathsForDomain(this.jobOptions.domain)).privateKeyPath,
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
public async getSignatureHeader(emailMessage: string): Promise<string> {
|
||||
const signResult = await plugins.dkimSign(emailMessage, {
|
||||
// Optional, default canonicalization, default is "relaxed/relaxed"
|
||||
canonicalization: 'relaxed/relaxed', // c=
|
||||
|
||||
// Optional, default signing and hashing algorithm
|
||||
// Mostly useful when you want to use rsa-sha1, otherwise no need to set
|
||||
algorithm: 'rsa-sha256',
|
||||
|
||||
// Optional, default is current time
|
||||
signTime: new Date(), // t=
|
||||
|
||||
// Keys for one or more signatures
|
||||
// Different signatures can use different algorithms (mostly useful when
|
||||
// you want to sign a message both with RSA and Ed25519)
|
||||
signatureData: [
|
||||
{
|
||||
signingDomain: this.jobOptions.domain, // d=
|
||||
selector: this.jobOptions.selector, // s=
|
||||
// supported key types: RSA, Ed25519
|
||||
privateKey: await this.loadPrivateKey(), // k=
|
||||
|
||||
// Optional algorithm, default is derived from the key.
|
||||
// Overrides whatever was set in parent object
|
||||
algorithm: 'rsa-sha256',
|
||||
|
||||
// Optional signature specifc canonicalization, overrides whatever was set in parent object
|
||||
canonicalization: 'relaxed/relaxed', // c=
|
||||
|
||||
// Maximum number of canonicalized body bytes to sign (eg. the "l=" tag).
|
||||
// Do not use though. This is available only for compatibility testing.
|
||||
// maxBodyLength: 12345
|
||||
},
|
||||
],
|
||||
});
|
||||
const signature = signResult.signatures;
|
||||
return signature;
|
||||
}
|
||||
}
|
66
ts/mta/mta.classes.mta.ts
Normal file
66
ts/mta/mta.classes.mta.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import { Email } from './mta.classes.email.js';
|
||||
import { EmailSendJob } from './mta.classes.emailsendjob.js';
|
||||
import { DKIMCreator } from './mta.classes.dkimcreator.js';
|
||||
import { DKIMVerifier } from './mta.classes.dkimverifier.js';
|
||||
import { SMTPServer } from './mta.classes.smtpserver.js';
|
||||
import { DNSManager } from './mta.classes.dnsmanager.js';
|
||||
|
||||
export class MTA {
|
||||
public server: SMTPServer;
|
||||
public dkimCreator: DKIMCreator;
|
||||
public dkimVerifier: DKIMVerifier;
|
||||
public dnsManager: DNSManager;
|
||||
|
||||
constructor() {
|
||||
this.dkimCreator = new DKIMCreator(this);
|
||||
this.dkimVerifier = new DKIMVerifier(this);
|
||||
this.dnsManager = new DNSManager(this);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
// lets get the certificate
|
||||
/**
|
||||
* gets a certificate for a domain used by a service
|
||||
* @param serviceNameArg
|
||||
* @param domainNameArg
|
||||
*/
|
||||
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
const typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
||||
typedrouter,
|
||||
'https://cloudly.lossless.one:443'
|
||||
);
|
||||
const getCertificateForDomainOverHttps = async (domainNameArg: string) => {
|
||||
const typedCertificateRequest =
|
||||
typedsocketClient.createTypedRequest<any>('getSslCertificate');
|
||||
const typedResponse = await typedCertificateRequest.fire({
|
||||
authToken: '', // do proper auth here
|
||||
requiredCertName: domainNameArg,
|
||||
});
|
||||
return typedResponse.certificate;
|
||||
};
|
||||
const certificate = await getCertificateForDomainOverHttps('mta.lossless.one');
|
||||
await typedsocketClient.stop();
|
||||
this.server = new SMTPServer(this, {
|
||||
port: 25,
|
||||
key: certificate.privateKey,
|
||||
cert: certificate.publicKey,
|
||||
});
|
||||
await this.server.start();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (!this.server) {
|
||||
console.error('Server is not running');
|
||||
return;
|
||||
}
|
||||
await this.server.stop();
|
||||
}
|
||||
|
||||
public async send(email: Email): Promise<void> {
|
||||
await this.dkimCreator.handleDKIMKeysForEmail(email);
|
||||
const sendJob = new EmailSendJob(this, email);
|
||||
await sendJob.send();
|
||||
}
|
||||
}
|
191
ts/mta/mta.classes.smtpserver.ts
Normal file
191
ts/mta/mta.classes.smtpserver.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { Email } from './mta.classes.email.js';
|
||||
import type { MTA } from './mta.classes.mta.js';
|
||||
|
||||
export interface ISmtpServerOptions {
|
||||
port: number;
|
||||
key: string;
|
||||
cert: string;
|
||||
}
|
||||
|
||||
export class SMTPServer {
|
||||
public mtaRef: MTA;
|
||||
private smtpServerOptions: ISmtpServerOptions;
|
||||
private server: plugins.net.Server;
|
||||
private emailBufferStringMap: Map<plugins.net.Socket, string>;
|
||||
|
||||
constructor(mtaRefArg: MTA, optionsArg: ISmtpServerOptions) {
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
12
ts/mta/paths.ts
Normal file
12
ts/mta/paths.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const cwd = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
export const assetsDir = plugins.path.join(cwd, './assets');
|
||||
export const keysDir = plugins.path.join(assetsDir, './keys');
|
||||
export const dnsRecordsDir = plugins.path.join(assetsDir, './dns-records');
|
||||
export const sentEmailsDir = plugins.path.join(assetsDir, './sent-emails');
|
||||
export const receivedEmailsDir = plugins.path.join(assetsDir, './received-emails');
|
||||
plugins.smartfile.fs.ensureDirSync(keysDir);
|
67
ts/mta/plugins.ts
Normal file
67
ts/mta/plugins.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// node native
|
||||
import * as dns from 'dns';
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as tls from 'tls';
|
||||
import * as util from 'util';
|
||||
|
||||
export {
|
||||
dns,
|
||||
fs,
|
||||
crypto,
|
||||
net,
|
||||
path,
|
||||
tls,
|
||||
util,
|
||||
}
|
||||
|
||||
// @apiclient.xyz/cloudflare
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
|
||||
export {
|
||||
cloudflare,
|
||||
}
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@apiglobal/typedrequest';
|
||||
import * as typedsocket from '@apiglobal/typedsocket';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartfile from '@pushrocks/smartfile';
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smartrx from '@pushrocks/smartrx';
|
||||
|
||||
export {
|
||||
smartfile,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
smartrx,
|
||||
}
|
||||
|
||||
// tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export {
|
||||
tsclass,
|
||||
}
|
||||
|
||||
// third party
|
||||
import * as mailauth from 'mailauth';
|
||||
import { dkimSign } from 'mailauth/lib/dkim/sign.js';
|
||||
import mailparser from 'mailparser';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export {
|
||||
mailauth,
|
||||
dkimSign,
|
||||
mailparser,
|
||||
uuid,
|
||||
}
|
Reference in New Issue
Block a user