How to set up SMTP server in native Node.js -- no dependencies whatsoever
Asked Answered
I

2

7

I've seen many blogs and Stack Overflow questions about setting up Node.js to use a pre-existing SMTP server, especially through modules like nodemailer etc. Some of what I've already seen:

/ Any suggestion for smtp mail server in nodejs? -- this one may be the only one that even attempts to answer it, although from the docs for the service mentioned there (smtp-server), I don't see where the actual makings of the SMTP server from scratch are, i.e. I don't see the part that shows how to make your own [email protected] using Node.js (assuming the server is configured on some kind of Linux VM like Google compute engine).

All of these answers and blogs only addressed sending emails via some other email client.

I am not interested in any other email servers.

I don't believe in Gmail -- or any other 3rd party email providers. I want to host my own from my own server.

How can I build an SMTP mail server entirely from scratch, utilizing only the "net" built-in library in Node.js, and not relying on any external dependencies? Assuming I have already registered my own domain and have it hosted on a virtual machine with HTTPS, I aim for this server to have the capability to both send and receive emails using the address [email protected], without involving any third-party servers.

What are the initial steps to embark on this project? Are there any references or tutorials available that specifically deal with the SMTP socket protocols? These resources would provide a valuable starting point for this endeavor.

I have already attempted to develop an SMTP client. While its current objective is merely to send a single email to any email provider, I have encountered an issue where, despite not receiving any error messages, the emails fail to appear, even in spam folders. Interestingly, the server file does successfully receive emails. The concern here primarily lies with the client file.

For my DKIM key I use this basic script to generate it

/**
 * B"H
 * Generate DKIM key pairs for email usage
 */

const { generateKeyPairSync } = require('crypto');

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
  modulusLength: 2048,
});

console.log('Private Key:', privateKey.export({
  type: 'pkcs1',
  format: 'pem',
}));
console.log('Public Key:', publicKey.export({
  type: 'pkcs1',
  format: 'pem',
}));

and add the correct record

v=DKIM1; k=rsa; p=PUBLIC_KEY_without_---begin rsa or --end--rsa liens or new lines

Server (working at least at a basic level):

/**
 * B"H
 * @module AwtsMail
 */

const AwtsmoosClient = require("./awtsmoosEmailClient.js");
const net = require('net');
const CRLF = '\r\n';

module.exports = class AwtsMail {
    constructor() {
        console.log("Starting instance of email");

        this.server = net.createServer(socket => {
            console.log("Some connection happened!", Date.now());
            socket.write('220 awtsmoos.one ESMTP Postfix' + CRLF);

            let sender = '';
            let recipients = [];
            let data = '';
            let receivingData = false;
            let buffer = '';

            socket.on('data', chunk => {
                buffer += chunk.toString();
                let index;
                while ((index = buffer.indexOf(CRLF)) !== -1) {
                    const command = buffer.substring(0, index);
                    buffer = buffer.substring(index + CRLF.length);

                    console.log("Received command:", command);
                    console.log("Command length:", command.length);

                    if (receivingData) {
                        if (command === '.') {
                            receivingData = false;
                            console.log("Received email data:", data);

                            socket.write(`250 2.0.0 Ok: queued as 12345${CRLF}`);

                            // Simulate sending a reply back.
                            if (sender) {
                              console.log("The email has ended!")
                              /*
                                console.log(`Sending a reply back to ${sender}`);
                                const replyData = `Subject: Reply from Awtsmoos ${
                                  Math.floor(Math.random() * 8)
                                }\r\n\r\nB"H\n\nHello from the Awtsmoos, the time is ${
                                  Date.now()
                                }.`;
                                this.smtpClient.sendMail('[email protected]', sender, replyData);
                            */
                            }
                        } else {
                            data += command + CRLF;
                        }
                        continue;
                    }

                    if (command.startsWith('EHLO') || command.startsWith('HELO')) {
                        socket.write(`250-Hello${CRLF}`);
                        socket.write(`250 SMTPUTF8${CRLF}`);
                    } else if (command.startsWith('MAIL FROM')) {
                        sender = command.slice(10);
                        socket.write(`250 2.1.0 Ok${CRLF}`);
                        console.log("The SENDER is:", sender);
                    } else if (command.startsWith('RCPT TO')) {
                        recipients.push(command.slice(8));
                        socket.write(`250 2.1.5 Ok${CRLF}`);
                    } else if (command.startsWith('DATA')) {
                        receivingData = true;
                        socket.write(`354 End data with <CR><LF>.<CR><LF>${CRLF}`);
                    } else if (command.startsWith('QUIT')) {
                        socket.write(`221 2.0.0 Bye${CRLF}`);
                        socket.end();
                    } else {
                        console.log("Unknown command:", command);
                        socket.write('500 5.5.1 Error: unknown command' + CRLF);
                    }
                }
            });

            socket.on("error", err => {
                console.log("Socket error:", err);
            });

            socket.on("close", () => {
                console.log("Connection closed");
            });
        });

        //this.smtpClient = new AwtsmoosClient("awtsmoos.one");

        this.server.on("error", err => {
            console.log("Server error: ", err);
        });
    }

    shoymayuh() {
        this.server.listen(25, () => {
            console.log("Awtsmoos mail listening to you, port 25");
        }).on("error", err => {
            console.log("Error starting server:", err);
        });
    }
}

I have a domain (awtsmoos.one) that has the correct A record for the IP address, MX records, SPF, DKIM and DMARC records configured.

This server code does successfully receive email data. The problem is with the client, no matter what it has not sent even one message to any email provider (even test providers/10 minute mails/etc.)

/**
 *B"H
 * @module AwtsmoosEmailClient
 * A client for sending emails.
 * @requires crypto
 * @requires net
 * @requires tls
 */

const crypto = require('crypto');
const net = require('net');

const CRLF = '\r\n';

class AwtsmoosEmailClient {
    constructor(smtpServer, port = 25, privateKey = null) {
        this.smtpServer = smtpServer;
        this.port = port;
        this.privateKey = privateKey ? privateKey.replace(/\\n/g, '\n') : null;
        this.multiLineResponse = '';
        this.previousCommand = '';
    }

    /**
     * Canonicalizes headers and body in relaxed mode.
     * @param {string} headers - The headers of the email.
     * @param {string} body - The body of the email.
     * @returns {Object} - The canonicalized headers and body.
     */
    canonicalizeRelaxed(headers, body) {
        const canonicalizedHeaders = headers.split(CRLF)
            .map(line => line.toLowerCase().split(/\s*:\s*/).join(':').trim())
            .join(CRLF);

        const canonicalizedBody = body.split(CRLF)
            .map(line => line.split(/\s+/).join(' ').trimEnd())
            .join(CRLF).trimEnd();

        return { canonicalizedHeaders, canonicalizedBody };
    }

    /**
     * Signs the email using DKIM.
     * @param {string} domain - The sender's domain.
     * @param {string} selector - The selector.
     * @param {string} privateKey - The private key.
     * @param {string} emailData - The email data.
     * @returns {string} - The DKIM signature.
     */
    signEmail(domain, selector, privateKey, emailData) {
        const [headers, ...bodyParts] = emailData.split(CRLF + CRLF);
        const body = bodyParts.join(CRLF + CRLF);

        const { canonicalizedHeaders, canonicalizedBody } = this.canonicalizeRelaxed(headers, body);
        const bodyHash = crypto.createHash('sha256').update(canonicalizedBody).digest('base64');

        const dkimHeader = `v=1;a=rsa-sha256;c=relaxed/relaxed;d=${domain};s=${selector};bh=${bodyHash};h=from:to:subject:date;`;
        const signature = crypto.createSign('SHA256').update(dkimHeader + canonicalizedHeaders).sign(privateKey, 'base64');

        return `${dkimHeader}b=${signature}`;
    }

    /**
     * Determines the next command to send to the server.
     * @returns {string} - The next command.
     */
    getNextCommand() {
        const commandOrder = ['EHLO', 'MAIL FROM', 'RCPT TO', 'DATA', 'END OF DATA'];
        const currentIndex = commandOrder.indexOf(this.previousCommand);

        if (currentIndex === -1) {
            throw new Error(`Unknown previous command: ${this.previousCommand}`);
        }

        if (currentIndex + 1 >= commandOrder.length) {
            throw new Error('No more commands to send.');
        }

        return commandOrder[currentIndex + 1];
    }

    /**
     * Handles the SMTP response from the server.
     * @param {string} line - The response line from the server.
     * @param {net.Socket} client - The socket connected to the server.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} emailData - The email data.
     */
    handleSMTPResponse(line, client, sender, recipient, emailData) {
        console.log('Server Response:', line);

        this.handleErrorCode(line);

        if (line.endsWith('-')) {
            console.log('Multi-line Response:', line);
            return;
        }

        this.previousCommand = this.currentCommand;
        const nextCommand = this.getNextCommand();
        
        const commandHandlers = {
            'EHLO': () => client.write(`MAIL FROM:<${sender}>${CRLF}`),
            'MAIL FROM': () => client.write(`RCPT TO:<${recipient}>${CRLF}`),
            'RCPT TO': () => client.write(`DATA${CRLF}`),
            'DATA': () => client.write(`${emailData}${CRLF}.${CRLF}`),
            'END OF DATA': () => client.end(),
        };

        const handler = commandHandlers[nextCommand];

        if (!handler) {
            throw new Error(`Unknown next command: ${nextCommand}`);
        }

        handler();
        this.currentCommand = nextCommand;
    }

    /**
     * Handles error codes in the server response.
     * @param {string} line - The response line from the server.
     */
    handleErrorCode(line) {
        if (line.startsWith('4') || line.startsWith('5')) {
            throw new Error(line);
        }
    }

    /**
     * Sends an email.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} subject - The subject of the email.
     * @param {string} body - The body of the email.
     * @returns {Promise} - A promise that resolves when the email is sent.
     */
    async sendMail(sender, recipient, subject, body) {
        return new Promise((resolve, reject) => {
            const client = net.createConnection(this.port, this.smtpServer);
            client.setEncoding('utf-8');
            let buffer = '';

            const emailData = `From: ${sender}${CRLF}To: ${recipient}${CRLF}Subject: ${subject}${CRLF}${CRLF}${body}`;
            const domain = 'awtsmoos.com';
            const selector = 'selector';
            const dkimSignature = this.signEmail(domain, selector, this.privateKey, emailData);
            const signedEmailData = `DKIM-Signature: ${dkimSignature}${CRLF}${emailData}`;

            client.on('connect', () => {
                this.currentCommand = 'EHLO';
                client.write(`EHLO ${this.smtpServer}${CRLF}`);
            });

            client.on('data', (data) => {
                buffer += data;
                let index;
                while ((index = buffer.indexOf(CRLF)) !== -1) {
                    const line = buffer.substring(0, index).trim();
                    buffer = buffer.substring(index + CRLF.length);

                    if (line.endsWith('-')) {
                        this.multiLineResponse += line + CRLF;
                        continue;
                    }

                    const fullLine = this.multiLineResponse + line;
                    this.multiLineResponse = '';

                    try {
                        this.handleSMTPResponse(fullLine, client, sender, recipient, signedEmailData);
                    } catch (err) {
                        client.end();
                        reject(err);
                        return;
                    }
                }
            });

            client.on('end', resolve);
            client.on('error', reject);
            client.on('close', () => {
                if (this.previousCommand !== 'END OF DATA') {
                    reject(new Error('Connection closed prematurely'));
                } else {
                    resolve();
                }
            });
        });
    }
}

const privateKey = process.env.BH_key;
const smtpClient = new AwtsmoosEmailClient('awtsmoos.one', 25, privateKey);

async function main() {
    try {
        await smtpClient.sendMail('[email protected]', '[email protected]', 'B"H', 'This is a test email.');
        console.log('Email sent successfully');
    } catch (err) {
        console.error('Failed to send email:', err);
    }
}

main();

module.exports = AwtsmoosEmailClient;
Inversely answered 30/4, 2020 at 22:38 Comment(0)
I
2

After much trial and error I was able to do it successfully, with DKIM signatures, reverse DNS lookup, and TLS encryption.

Client:

/**
 * B"H
 * @module AwtsmoosEmailClient
 * A client for sending emails.
 * @requires crypto
 * @requires net
 * @requires tls
 * @optional privateKey environment variable for your DKIM private key
 * matching your public key, can gnerate with generateKeyPairs.js script
 * @optional BH_email_cert and BH_email_key environemnt variables for certbot
 *  TLS cert and key
 * @overview:
 * 
 * 
 * @method handleSMTPResponse: This method handles the 
 * SMTP server responses for each command sent. It builds the multi-line response, checks
 *  for errors, and determines the next command to be sent based on the server’s response.

@method handleErrorCode: This helper method throws an
 error if the server responds with a 4xx or 5xx status code.

@property commandHandlers: An object map where keys are SMTP 
commands and values are functions that handle sending the next SMTP command.

@method sendMail: This asynchronous method initiates the process 
of sending an email. It establishes a connection to the SMTP server, sends the SMTP 
commands sequentially based on server responses, and handles the 
closure and errors of the connection.

@method emailData: The email content formatted with headers such as From, To, and Subject.

@method dkimSignature: If a private key is provided, it computes the
 DKIM signature and appends it to the email data.

@event client.on('connect'): Initiates the SMTP conversation by sending the EHLO command upon connection.

@event client.on('data'): Listens for data from the server,
 parses the responses, and calls handleSMTPResponse to handle them.

@event client.on('end'), client.on('error'), client.on('close'): These
 handlers resolve or reject the promise based on the connection status
  and the success of the email sending process.

Variables and Constants:

@const CRLF: Stands for Carriage Return Line Feed, which is not shown
 in the code but presumably represents the newline sequence "\r\n".
this.smtpServer, this.port, this.privateKey: Instance variables that
 store the SMTP server address, port, and private key for DKIM signing, respectively.
this.multiLineResponse, this.previousCommand, this.currentCommand: 
Instance variables used to store the state of the SMTP conversation.
 */

const crypto = require('crypto');
const tls = require("tls");
const fs = require("fs");
const net = require('net');
const dns = require('dns');
const CRLF = '\r\n';



class AwtsmoosEmailClient {
    socket = null;
    useTLS = false;
    cert = null;
    key = null;

    commandHandlers = {
        'START': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            this.currentCommand = 'EHLO';
            var command = `EHLO ${this.smtpServer}${CRLF}`;
            console.log("Sending to server: ", command)
            client.write(command);
        },
        'EHLO': ({
            sender,
            recipient,
            emailData,
            client,
            lineOrMultiline
        } = {}) => {
            
            console.log("Handling EHLO");
            if (lineOrMultiline.includes('STARTTLS')) {
                var cmd = `STARTTLS${CRLF}`;
                console.log("Sending command: ", cmd);
                client.write(cmd);
            } else {
                var cmd = `MAIL FROM:<${sender}>${CRLF}`;
                console.log("Sending command: ", cmd);
                client.write(cmd);
            }
        },
        'STARTTLS': ({
            sender,
            recipient,
            emailData,
            client,
            lineOrMultiline 
        } = {}) => {
            // Read the response from the server
            
            console.log("Trying to start TLS");
            
            const options = {
                socket: client,
                servername: 'gmail-smtp-in.l.google.com',
                minVersion: 'TLSv1.2',
                ciphers: 'HIGH:!aNULL:!MD5',
                maxVersion: 'TLSv1.3',
                key:this.key,
                cert:this.cert
            };
            
            const secureSocket = tls.connect(options, () => {
                console.log('TLS handshake completed.');
                console.log("Waiting for secure connect handler");
                
            });
    
            
    
            secureSocket.on('error', (err) => {
                console.error('TLS Error:', err);
                console.error('Stack Trace:', err.stack);
                this.previousCommand = '';
            });
    
            secureSocket.on("secureConnect", () => {
                console.log("Secure connect!");
                this.socket = secureSocket;
                client.removeAllListeners();
                
                
                
                try {
                    this.handleClientData({
                        client: secureSocket,
                        sender,
                        recipient,
                        dataToSend: emailData
                    });
                } catch(e) {
                    console.error(e)
                    console.error("Stack", e)
                    throw new Error(e)
                }

                console.log("Setting", this.previousCommand, "to: ")
                this.previousCommand = "STARTTLS";
                console.log(this.previousCommand, "<< set")
                // Once the secure connection is established, resend the EHLO command
                var command = `EHLO ${this.smtpServer}${CRLF}`;
                console.log("Resending EHLO command over secure connection:", command);
                secureSocket.write(command);


                
            });
    
            secureSocket.on("clientError", err => {
                console.error("A client error", err);
                console.log("Stack", err.stack);
            });
    
            secureSocket.on('close', () => {
                console.log('Connection closed');
                secureSocket.removeAllListeners();
                this.previousCommand = '';
            });
    
                
        
            // Send the STARTTLS command to the server
           // client.write('STARTTLS\r\n');
        },
        'MAIL FROM': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
    
            var rc = `RCPT TO:<${recipient}>${CRLF}`;
            console.log("Sending RCPT:", rc)
            client.write(rc)
        },
        'RCPT TO': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            var c = `DATA${CRLF}`;
            console.log("Sending data (RCPT TO) info: ", c)
            client.write(c)
        },
        'DATA': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            var data = `${emailData}${CRLF}.${CRLF}`;
            console.log("Sending data to the server: ", data)
            client.write(data);
            this.previousCommand = 'END OF DATA'; 
            // Set previousCommand to 'END OF DATA' 
            //after sending the email content
        },
    };
    constructor({
        port = 25
    } = {}) {
        
        const privateKey = process.env.BH_key;
        if(privateKey) {
            this.privateKey = 
            privateKey.replace(/\\n/g, '\n');
        }

        this.port = port || 25;
        this.multiLineResponse = '';
        this.previousCommand = '';


        const certPath = process.env.BH_email_cert;
        const keyPath = process.env.BH_email_key;

        console.log("certPath at",certPath,"keyPath at", keyPath)
        if (certPath && keyPath) {
            try {
                this.cert = fs.readFileSync(certPath, 'utf-8');
                this.key = fs.readFileSync(keyPath, 'utf-8');
                // if both are successfully loaded, set useTLS to true
                this.useTLS = true;
                console.log("Loaded cert and key")
            } catch (err) {
                console.error("Error reading cert or key files: ", err);
                // handle error, perhaps set useTLS to false or throw an error
            }
        }
    }

    /**
     * @method getDNSRecords
     * @param {String (Email format)} email 
     * @returns 
     */
    async getDNSRecords(email) {
        return new Promise((r,j) => {
            if(typeof(email) != "string") {
                j("Email paramter not a string");
                return;
            }
            const domain = email.split('@')[1];
            if(!domain) return j("Not an email");
            // Perform MX Record Lookup
            dns.resolveMx(domain, (err, addresses) => {
                if (err) {
                    console.error('Error resolving MX records:', err);
                    j(err);
                    return;
                }
                
                // Sort the MX records by priority
                addresses.sort((a, b) => a.priority - b.priority);
                r(addresses);
                return addresses
            });
        })
        
    }


    /**
     * Determines the next command to send to the server.
     * @returns {string} - The next command.
     */
    getNextCommand() {
        const commandOrder = [
            'START',
            'EHLO', 
            'STARTTLS', // Add STARTTLS to the command order
            'EHLO',
            'MAIL FROM', 
            'RCPT TO', 
            'DATA', 
            'END OF DATA'
        ];

        console.log("Current previousCommand:", this.previousCommand);


        const currentIndex = commandOrder.indexOf(this.previousCommand);
    
        if (currentIndex === -1) {
            return commandOrder[0]; 
        }
    
        if (currentIndex + 1 >= commandOrder.length) {
            throw new Error('No more commands to send.');
        }
    
        // If the previous command was STARTTLS, return EHLO to be resent over the secure connection
        if (this.previousCommand === 'STARTTLS') {
            return 'EHLO';
        }


        var nextCommand = commandOrder[currentIndex + 1]
        console.log("Next command: ",nextCommand)
        return  nextCommand ;
    }
    
    
    /**
     * Handles the SMTP response from the server.
     * @param {string} lineOrMultiline - The response line from the server.
     * @param {net.Socket} client - The socket connected to the server.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} emailData - The email data.
     */
    
    handleSMTPResponse({
        lineOrMultiline, 
        client, 
        sender, 
        recipient, 
        emailData
    } = {}) {
        console.log('Server Response:', lineOrMultiline);
    
        this.handleErrorCode(lineOrMultiline);
    
        var isMultiline = lineOrMultiline.charAt(3) === '-';
        var lastLine = lineOrMultiline;
        var lines;
        if(isMultiline) {
            lines =  lineOrMultiline.split(CRLF)
            lastLine = lines[lines.length - 1]
        }
    
        console.log("Got full response: ",  lines, lastLine.toString("utf-8"))
        this.multiLineResponse = ''; // Reset accumulated multiline response.
    
        try {
            let nextCommand = this.getNextCommand();
            
            if (lastLine.includes('250-STARTTLS')) {
                console.log('Ready to send STARTTLS...');
            } else if (lastLine.startsWith('220 ') && lastLine.includes('Ready to start TLS')) {
                console.log('Ready to initiate TLS...');
                // TLS handshake has been completed, send EHLO again.
                nextCommand = 'STARTTLS';
            } else if (this.previousCommand === 'STARTTLS' && lastLine.startsWith('250 ')) {
                console.log('Successfully received EHLO response after STARTTLS');
                // Proceed with the next command after validating EHLO response.
                // Additional checks here to validate the EHLO response if needed.
                this.previousCommand = 'EHLO'; // Update previousCommand here
            } else if (this.previousCommand === 'EHLO' && lastLine.startsWith('250 ')) {
                console.log('Successfully received EHLO response');
                nextCommand = 'MAIL FROM';
            }
    
    
            const handler = this.commandHandlers[nextCommand];
            if (!handler) {
                throw new Error(`Unknown next command: ${nextCommand}`);
            }
    
            handler({
                client,
                sender,
                recipient,
                emailData,
                lineOrMultiline
            });
            if (nextCommand !== 'DATA') this.previousCommand = nextCommand; // Update previousCommand here for commands other than 'DATA'
        } catch (e) {
            console.error(e.message);
            client.end();
        } 
    }
    
    

    

    /**
     * Handles error codes in the server response.
     * @param {string} line - The response line from the server.
     */
    handleErrorCode(line) {
        if (line.startsWith('4') || line.startsWith('5')) {
            throw new Error(line);
        }
    }

    /**
     * Sends an email.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} subject - The subject of the email.
     * @param {string} body - The body of the email.
     * @returns {Promise} - A promise that resolves when the email is sent.
     */
    async sendMail(sender, recipient, subject, body) {
        return new Promise(async (resolve, reject) => {
            console.log("Getting DNS records..");
            var addresses = await this.getDNSRecords(recipient);
            console.log("Got addresses", addresses);
            var primary = addresses[0].exchange;
            

            console.log("Primary DNS of recepient: ", primary)
            this.smtpServer = primary;
            
           
           
            this.socket = net.createConnection(
                this.port, this.smtpServer
            );
            
            
            this.socket.setEncoding('utf-8');
            

            const emailData = `From: ${sender}${CRLF}To: ${recipient}${CRLF}Subject: ${subject}${CRLF}${CRLF}${body}`;
            const domain = 'awtsmoos.one';
            const selector = 'selector';
            var dataToSend=emailData
            if(this. privateKey) {
                const dkimSignature = this.signEmail(
                    domain, selector, this.privateKey, emailData
                );
                const signedEmailData = `DKIM-Signature: ${dkimSignature}${CRLF}${emailData}`;
                dataToSend=signedEmailData;
                console.log("Just DKIM signed the email. Data: ", signedEmailData)
            }

            this.socket.on('connect', () => {
                console.log(
                    "Connected, waiting for first server response (220)"
                )
            });


            try {
                this.handleClientData({
                    client: this.socket,
                    sender,
                    recipient,
                    dataToSend
                });
            } catch(e) {
                reject(e);
            }
            


            this.socket.on('end', () => {
                this.socket.removeAllListeners();
                this.previousCommand = ''
                resolve()
            });

            this.socket.on('error', (e)=>{
                this.socket.removeAllListeners();
                console.error("Client error: ",e)
                this.previousCommand = ''
                reject("Error: " + e)
            });

            this.socket.on('close', () => {
                this.socket.removeAllListeners();
                if (this.previousCommand !== 'END OF DATA') {
                    reject(new Error('Connection closed prematurely'));
                } else {
                    this.previousCommand = ''
                    resolve();
                }
            });
        });
    }

    /**
     * 
     * @param {Object} 
     *  @method handleClientData
     * @description binds the data event
     * to the client socket, useful for switching
     * between net and tls sockets.
     * 
     * @param {NET or TLS socket} clientSocket 
     * @param {String <email>} sender 
     * @param {String <email>} recipient 
     * @param {String <email body>} dataToSend 
     * 
     *  
     */
    handleClientData({
        client,
        sender,
        recipient,
        dataToSend
    } = {}) {
        var firstData = false;

        let buffer = '';
        let multiLineBuffer = ''; // Buffer for accumulating multi-line response
        let isMultiLine = false; // Flag for tracking multi-line status
        let currentStatusCode = ''; // Store the current status code for multi-line responses

        client.on('data', (data) => {
            buffer += data;
            let index;

            while ((index = buffer.indexOf(CRLF)) !== -1) {
                const line = buffer.substring(0, index).trim();
                buffer = buffer.substring(index + CRLF.length);

                if (!firstData) {
                    firstData = true;
                    console.log("First time connected, should wait for 220");
                }

                const potentialStatusCode = line.substring(0, 3); // Extract the first three characters
                const fourthChar = line.charAt(3); // Get the 4th character

                // If the line's 4th character is a '-', it's a part of a multi-line response
                if (fourthChar === '-') {
                    isMultiLine = true;
                    currentStatusCode = potentialStatusCode;
                    multiLineBuffer += line + CRLF; // Remove the status code and '-' and add to buffer
                    
                    continue; // Continue to the next iteration to keep collecting multi-line response
                }

                // If this line has the same status code as a previous line but no '-', then it is the end of a multi-line response
                if (isMultiLine && currentStatusCode === potentialStatusCode && fourthChar === ' ') {
                    const fullLine = multiLineBuffer + line; // Remove the status code and space
                    multiLineBuffer = ''; // Reset the buffer
                    isMultiLine = false; // Reset the multi-line flag
                    currentStatusCode = ''; // Reset the status code

                    try {
                        console.log("Handling complete multi-line response:", fullLine);
                        this.handleSMTPResponse({
                            lineOrMultiline: fullLine, 
                            client, 
                            sender, 
                            recipient, 
                            emailData: dataToSend,
                            multiline:true
                        });
                    } catch (err) {
                        client.end();
                        
                        this.previousCommand = ''
                        throw new Error(err);
                    }
                } else if (!isMultiLine) {
                    // Single-line response
                    try {
                        console.log("Handling single-line response:", line);
                        this.handleSMTPResponse({
                            lineOrMultiline: line, 
                            client, 
                            sender, 
                            recipient, 
                            emailData: dataToSend
                        });
                    } catch (err) {
                        client.end();
                        this.previousCommand = ''
                        throw new Error(err);
                    }
                }
            }
        });
    }
    
    /**
     * Canonicalizes headers and body in relaxed mode.
     * @param {string} headers - The headers of the email.
     * @param {string} body - The body of the email.
     * @returns {Object} - The canonicalized headers and body.
     */
    canonicalizeRelaxed(headers, body) {
        const canonicalizedHeaders = headers.split(CRLF)
        .map(line => {
            const [key, ...value] = line.split(':');
            return key + ':' + value.join(':').trim();
        })
        .join(CRLF);


        const canonicalizedBody = body.split(CRLF)
            .map(line => line.split(/\s+/).join(' ').trimEnd())
            .join(CRLF).trimEnd();

        return { canonicalizedHeaders, canonicalizedBody };
    }

    /**
     * Signs the email using DKIM.
     * @param {string} domain - The sender's domain.
     * @param {string} selector - The selector.
     * @param {string} privateKey - The private key.
     * @param {string} emailData - The email data.
     * @returns {string} - The DKIM signature.
     */
    signEmail(domain, selector, privateKey, emailData) {
        try {
            const [headers, ...bodyParts] = emailData.split(CRLF + CRLF);
            const body = bodyParts.join(CRLF + CRLF);
        
            const { canonicalizedHeaders, canonicalizedBody } = 
            this.canonicalizeRelaxed(headers, body);
            const bodyHash = crypto.createHash('sha256')
            .update(canonicalizedBody).digest('base64');
        
            const headerFields = canonicalizedHeaders
            .split(CRLF).map(line => line.split(':')[0]).join(':');
            const dkimHeader = `v=1;a=rsa-sha256;c=relaxed/relaxed;d=${domain};s=${selector};bh=${bodyHash};h=${headerFields};`;
        
            const signature = crypto.createSign('SHA256').update(dkimHeader + CRLF + canonicalizedHeaders).sign(privateKey, 'base64');
        
            return `${dkimHeader}b=${signature}`;
        } catch(e) {
            console.error("There was an error", e);
            console.log("The private key is: ", this.privateKey, privateKey)
            return emailData;
        }
        
    }

}


/**
 * determine if we can use TLS by checking
 * if our cert and key exist.
 */





const smtpClient = new AwtsmoosEmailClient(
);

async function main() {
    try {
        await smtpClient.sendMail('[email protected]', 
        '[email protected]', 'B"H', 
        'This is a test email! The time is: ' + Date.now() 
        + " Which is " + 
        (new Date()));
        console.log('Email sent successfully');
    } catch (err) {
        console.error('Failed to send email:', err);
    }
}

main();

module.exports = AwtsmoosEmailClient;

Server pretty much the same.

DKIM record something like:

selector._domainkey TXT v=DKIM1; k=rsa; p=MIIBCg..(Your public key)

Inversely answered 27/9, 2023 at 5:56 Comment(0)
I
13

Some friendly advice -- you probably want to use an off-the-shelf MTA like postfix, exim4, or sendmail if you just want to receive mail on your local machine.

I say this because I have literally spent a good hunk of my career implementing MTAs and feel I should warn you that this is a solved problem that allows you to have complete control over your mail traffic, and there are some very tricky issues to solve to write an MTA that works at scale with large mail volumes.

That said, SMTP (note spelling) is a very simple protocol, and a great "first protocol" to implement if you're interested in that stuff. It would be very easy to write one in NodeJS.

The first edition you'd be interested in was released some time around 1982, as RFC-821, aka IETF STD-10. It was then updated over the years to RFC-2821 and a bunch of related specs, but basic RFC-821 support will get you what you need to talk to 99% of hosts on the Internet today. (That number will go down as you need ESMTP support for TLS - but this is not much harder nor much different).

Your daemon will need to listen on port 25, and need to process commands like this:

YOU:  220 my.computer.com SMTP Service Ready
THEM: EHLO blah blah
YOU:  500 Syntax Error. Try again using SMTP.
THEM: HELO blah blah
YOU:  250 G'day mate
THEM: MAIL FROM: <[email protected]>
YOU:  250 Sender Okay
THEM: RCPT TO: <[email protected]>
YOU:  250 OK
THEM: DATA
YOU:  354 Enter mail, end with "." on a line by itself...
THEM: <BUNCH OF STUFF>
      .
YOU:  250 Mail accepted
THEM: QUIT
YOU:  221 Goodbye

Obviously there is more here wrt error handling etc -- read the spec -- but this is the gist of it. The numbers are response codes and have specific meanings. The lines are separated by \r\n and are supposed to be less than 1024 bytes wide.

<BUNCH OF STUFF> is an email message, and will not have a line which is just a dot in it. If the email had a dot like that, the other end will send an extra dot. This is in the spec.

Finally, write <XXXX><BUNCH OF STUFF> into your $MAIL file (probably /var/mail/username or /var/spool/mail/username) and point your MUA at it. Pine, Alpine, Elm, or mutt would make a good MUA for sorting this stuff out.

<XXXX> needs to start with From (NO colon) and end with \n. This is the Berkeley mbox file format. It should reflect the MAIL FROM header in the SMTP transaction.

This file format is very common and supported by most POP3 and IMAP4 servers. You can probably also read it with Mozilla Thunderbird. I know Netscape Mail supported it back in the day.

Illhumored answered 30/4, 2020 at 23:13 Comment(6)
Thanks, its good to find actual answers to questions on here instead of just "don't do that.." this is really good, so basically just implement tools.ietf.org/html/rfc821 ? And also do you know how to handle multiple users, i.e. [email protected], [email protected] (or is it in the spec somewhere)?Disturbance
No problem -- and yes, implementing that spec will let you send and receive Internet e-mail. Note that the spec covers the transfer, not the format of the e-mail, which is in RFC-2822 / STD-11 and a LOT of other documents. At least 10 relevant ones. Multiple users is simply multiple RCPT-TO: lines in the envelope. Piece of cake! Also, re-read my message; I forgot to escape some &lt;entites&gt; when I wrote it. Good luck, and have fun!Illhumored
Thanks! Do u know how to properly implement the client to make the emails go through (see edit)?Disturbance
@B''HBi'ezras--BoruchHashem The client is a separate issue from the server. If you want to write a client, install a working SMTP server and use that to test so if there are any bugs you know which side they are on.Anesthesiology
@rand'Chris no. i dont beleive in installing. need to make everything from scratchDisturbance
@B''HBi'ezras--BoruchHashem You need to test your client against some SMTP server. It should be one you control, otherwise you will have a very hard time debugging. It should also be one you didn't write, else you can have bug-for-bug compatible code that doesn't talk to the rest of the world. You are still making everything from scratch after you're done testing. But feel free to do what you want with your time.Anesthesiology
I
2

After much trial and error I was able to do it successfully, with DKIM signatures, reverse DNS lookup, and TLS encryption.

Client:

/**
 * B"H
 * @module AwtsmoosEmailClient
 * A client for sending emails.
 * @requires crypto
 * @requires net
 * @requires tls
 * @optional privateKey environment variable for your DKIM private key
 * matching your public key, can gnerate with generateKeyPairs.js script
 * @optional BH_email_cert and BH_email_key environemnt variables for certbot
 *  TLS cert and key
 * @overview:
 * 
 * 
 * @method handleSMTPResponse: This method handles the 
 * SMTP server responses for each command sent. It builds the multi-line response, checks
 *  for errors, and determines the next command to be sent based on the server’s response.

@method handleErrorCode: This helper method throws an
 error if the server responds with a 4xx or 5xx status code.

@property commandHandlers: An object map where keys are SMTP 
commands and values are functions that handle sending the next SMTP command.

@method sendMail: This asynchronous method initiates the process 
of sending an email. It establishes a connection to the SMTP server, sends the SMTP 
commands sequentially based on server responses, and handles the 
closure and errors of the connection.

@method emailData: The email content formatted with headers such as From, To, and Subject.

@method dkimSignature: If a private key is provided, it computes the
 DKIM signature and appends it to the email data.

@event client.on('connect'): Initiates the SMTP conversation by sending the EHLO command upon connection.

@event client.on('data'): Listens for data from the server,
 parses the responses, and calls handleSMTPResponse to handle them.

@event client.on('end'), client.on('error'), client.on('close'): These
 handlers resolve or reject the promise based on the connection status
  and the success of the email sending process.

Variables and Constants:

@const CRLF: Stands for Carriage Return Line Feed, which is not shown
 in the code but presumably represents the newline sequence "\r\n".
this.smtpServer, this.port, this.privateKey: Instance variables that
 store the SMTP server address, port, and private key for DKIM signing, respectively.
this.multiLineResponse, this.previousCommand, this.currentCommand: 
Instance variables used to store the state of the SMTP conversation.
 */

const crypto = require('crypto');
const tls = require("tls");
const fs = require("fs");
const net = require('net');
const dns = require('dns');
const CRLF = '\r\n';



class AwtsmoosEmailClient {
    socket = null;
    useTLS = false;
    cert = null;
    key = null;

    commandHandlers = {
        'START': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            this.currentCommand = 'EHLO';
            var command = `EHLO ${this.smtpServer}${CRLF}`;
            console.log("Sending to server: ", command)
            client.write(command);
        },
        'EHLO': ({
            sender,
            recipient,
            emailData,
            client,
            lineOrMultiline
        } = {}) => {
            
            console.log("Handling EHLO");
            if (lineOrMultiline.includes('STARTTLS')) {
                var cmd = `STARTTLS${CRLF}`;
                console.log("Sending command: ", cmd);
                client.write(cmd);
            } else {
                var cmd = `MAIL FROM:<${sender}>${CRLF}`;
                console.log("Sending command: ", cmd);
                client.write(cmd);
            }
        },
        'STARTTLS': ({
            sender,
            recipient,
            emailData,
            client,
            lineOrMultiline 
        } = {}) => {
            // Read the response from the server
            
            console.log("Trying to start TLS");
            
            const options = {
                socket: client,
                servername: 'gmail-smtp-in.l.google.com',
                minVersion: 'TLSv1.2',
                ciphers: 'HIGH:!aNULL:!MD5',
                maxVersion: 'TLSv1.3',
                key:this.key,
                cert:this.cert
            };
            
            const secureSocket = tls.connect(options, () => {
                console.log('TLS handshake completed.');
                console.log("Waiting for secure connect handler");
                
            });
    
            
    
            secureSocket.on('error', (err) => {
                console.error('TLS Error:', err);
                console.error('Stack Trace:', err.stack);
                this.previousCommand = '';
            });
    
            secureSocket.on("secureConnect", () => {
                console.log("Secure connect!");
                this.socket = secureSocket;
                client.removeAllListeners();
                
                
                
                try {
                    this.handleClientData({
                        client: secureSocket,
                        sender,
                        recipient,
                        dataToSend: emailData
                    });
                } catch(e) {
                    console.error(e)
                    console.error("Stack", e)
                    throw new Error(e)
                }

                console.log("Setting", this.previousCommand, "to: ")
                this.previousCommand = "STARTTLS";
                console.log(this.previousCommand, "<< set")
                // Once the secure connection is established, resend the EHLO command
                var command = `EHLO ${this.smtpServer}${CRLF}`;
                console.log("Resending EHLO command over secure connection:", command);
                secureSocket.write(command);


                
            });
    
            secureSocket.on("clientError", err => {
                console.error("A client error", err);
                console.log("Stack", err.stack);
            });
    
            secureSocket.on('close', () => {
                console.log('Connection closed');
                secureSocket.removeAllListeners();
                this.previousCommand = '';
            });
    
                
        
            // Send the STARTTLS command to the server
           // client.write('STARTTLS\r\n');
        },
        'MAIL FROM': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
    
            var rc = `RCPT TO:<${recipient}>${CRLF}`;
            console.log("Sending RCPT:", rc)
            client.write(rc)
        },
        'RCPT TO': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            var c = `DATA${CRLF}`;
            console.log("Sending data (RCPT TO) info: ", c)
            client.write(c)
        },
        'DATA': ({
            sender,
            recipient,
            emailData,
            client
        } = {}) => {
            var data = `${emailData}${CRLF}.${CRLF}`;
            console.log("Sending data to the server: ", data)
            client.write(data);
            this.previousCommand = 'END OF DATA'; 
            // Set previousCommand to 'END OF DATA' 
            //after sending the email content
        },
    };
    constructor({
        port = 25
    } = {}) {
        
        const privateKey = process.env.BH_key;
        if(privateKey) {
            this.privateKey = 
            privateKey.replace(/\\n/g, '\n');
        }

        this.port = port || 25;
        this.multiLineResponse = '';
        this.previousCommand = '';


        const certPath = process.env.BH_email_cert;
        const keyPath = process.env.BH_email_key;

        console.log("certPath at",certPath,"keyPath at", keyPath)
        if (certPath && keyPath) {
            try {
                this.cert = fs.readFileSync(certPath, 'utf-8');
                this.key = fs.readFileSync(keyPath, 'utf-8');
                // if both are successfully loaded, set useTLS to true
                this.useTLS = true;
                console.log("Loaded cert and key")
            } catch (err) {
                console.error("Error reading cert or key files: ", err);
                // handle error, perhaps set useTLS to false or throw an error
            }
        }
    }

    /**
     * @method getDNSRecords
     * @param {String (Email format)} email 
     * @returns 
     */
    async getDNSRecords(email) {
        return new Promise((r,j) => {
            if(typeof(email) != "string") {
                j("Email paramter not a string");
                return;
            }
            const domain = email.split('@')[1];
            if(!domain) return j("Not an email");
            // Perform MX Record Lookup
            dns.resolveMx(domain, (err, addresses) => {
                if (err) {
                    console.error('Error resolving MX records:', err);
                    j(err);
                    return;
                }
                
                // Sort the MX records by priority
                addresses.sort((a, b) => a.priority - b.priority);
                r(addresses);
                return addresses
            });
        })
        
    }


    /**
     * Determines the next command to send to the server.
     * @returns {string} - The next command.
     */
    getNextCommand() {
        const commandOrder = [
            'START',
            'EHLO', 
            'STARTTLS', // Add STARTTLS to the command order
            'EHLO',
            'MAIL FROM', 
            'RCPT TO', 
            'DATA', 
            'END OF DATA'
        ];

        console.log("Current previousCommand:", this.previousCommand);


        const currentIndex = commandOrder.indexOf(this.previousCommand);
    
        if (currentIndex === -1) {
            return commandOrder[0]; 
        }
    
        if (currentIndex + 1 >= commandOrder.length) {
            throw new Error('No more commands to send.');
        }
    
        // If the previous command was STARTTLS, return EHLO to be resent over the secure connection
        if (this.previousCommand === 'STARTTLS') {
            return 'EHLO';
        }


        var nextCommand = commandOrder[currentIndex + 1]
        console.log("Next command: ",nextCommand)
        return  nextCommand ;
    }
    
    
    /**
     * Handles the SMTP response from the server.
     * @param {string} lineOrMultiline - The response line from the server.
     * @param {net.Socket} client - The socket connected to the server.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} emailData - The email data.
     */
    
    handleSMTPResponse({
        lineOrMultiline, 
        client, 
        sender, 
        recipient, 
        emailData
    } = {}) {
        console.log('Server Response:', lineOrMultiline);
    
        this.handleErrorCode(lineOrMultiline);
    
        var isMultiline = lineOrMultiline.charAt(3) === '-';
        var lastLine = lineOrMultiline;
        var lines;
        if(isMultiline) {
            lines =  lineOrMultiline.split(CRLF)
            lastLine = lines[lines.length - 1]
        }
    
        console.log("Got full response: ",  lines, lastLine.toString("utf-8"))
        this.multiLineResponse = ''; // Reset accumulated multiline response.
    
        try {
            let nextCommand = this.getNextCommand();
            
            if (lastLine.includes('250-STARTTLS')) {
                console.log('Ready to send STARTTLS...');
            } else if (lastLine.startsWith('220 ') && lastLine.includes('Ready to start TLS')) {
                console.log('Ready to initiate TLS...');
                // TLS handshake has been completed, send EHLO again.
                nextCommand = 'STARTTLS';
            } else if (this.previousCommand === 'STARTTLS' && lastLine.startsWith('250 ')) {
                console.log('Successfully received EHLO response after STARTTLS');
                // Proceed with the next command after validating EHLO response.
                // Additional checks here to validate the EHLO response if needed.
                this.previousCommand = 'EHLO'; // Update previousCommand here
            } else if (this.previousCommand === 'EHLO' && lastLine.startsWith('250 ')) {
                console.log('Successfully received EHLO response');
                nextCommand = 'MAIL FROM';
            }
    
    
            const handler = this.commandHandlers[nextCommand];
            if (!handler) {
                throw new Error(`Unknown next command: ${nextCommand}`);
            }
    
            handler({
                client,
                sender,
                recipient,
                emailData,
                lineOrMultiline
            });
            if (nextCommand !== 'DATA') this.previousCommand = nextCommand; // Update previousCommand here for commands other than 'DATA'
        } catch (e) {
            console.error(e.message);
            client.end();
        } 
    }
    
    

    

    /**
     * Handles error codes in the server response.
     * @param {string} line - The response line from the server.
     */
    handleErrorCode(line) {
        if (line.startsWith('4') || line.startsWith('5')) {
            throw new Error(line);
        }
    }

    /**
     * Sends an email.
     * @param {string} sender - The sender email address.
     * @param {string} recipient - The recipient email address.
     * @param {string} subject - The subject of the email.
     * @param {string} body - The body of the email.
     * @returns {Promise} - A promise that resolves when the email is sent.
     */
    async sendMail(sender, recipient, subject, body) {
        return new Promise(async (resolve, reject) => {
            console.log("Getting DNS records..");
            var addresses = await this.getDNSRecords(recipient);
            console.log("Got addresses", addresses);
            var primary = addresses[0].exchange;
            

            console.log("Primary DNS of recepient: ", primary)
            this.smtpServer = primary;
            
           
           
            this.socket = net.createConnection(
                this.port, this.smtpServer
            );
            
            
            this.socket.setEncoding('utf-8');
            

            const emailData = `From: ${sender}${CRLF}To: ${recipient}${CRLF}Subject: ${subject}${CRLF}${CRLF}${body}`;
            const domain = 'awtsmoos.one';
            const selector = 'selector';
            var dataToSend=emailData
            if(this. privateKey) {
                const dkimSignature = this.signEmail(
                    domain, selector, this.privateKey, emailData
                );
                const signedEmailData = `DKIM-Signature: ${dkimSignature}${CRLF}${emailData}`;
                dataToSend=signedEmailData;
                console.log("Just DKIM signed the email. Data: ", signedEmailData)
            }

            this.socket.on('connect', () => {
                console.log(
                    "Connected, waiting for first server response (220)"
                )
            });


            try {
                this.handleClientData({
                    client: this.socket,
                    sender,
                    recipient,
                    dataToSend
                });
            } catch(e) {
                reject(e);
            }
            


            this.socket.on('end', () => {
                this.socket.removeAllListeners();
                this.previousCommand = ''
                resolve()
            });

            this.socket.on('error', (e)=>{
                this.socket.removeAllListeners();
                console.error("Client error: ",e)
                this.previousCommand = ''
                reject("Error: " + e)
            });

            this.socket.on('close', () => {
                this.socket.removeAllListeners();
                if (this.previousCommand !== 'END OF DATA') {
                    reject(new Error('Connection closed prematurely'));
                } else {
                    this.previousCommand = ''
                    resolve();
                }
            });
        });
    }

    /**
     * 
     * @param {Object} 
     *  @method handleClientData
     * @description binds the data event
     * to the client socket, useful for switching
     * between net and tls sockets.
     * 
     * @param {NET or TLS socket} clientSocket 
     * @param {String <email>} sender 
     * @param {String <email>} recipient 
     * @param {String <email body>} dataToSend 
     * 
     *  
     */
    handleClientData({
        client,
        sender,
        recipient,
        dataToSend
    } = {}) {
        var firstData = false;

        let buffer = '';
        let multiLineBuffer = ''; // Buffer for accumulating multi-line response
        let isMultiLine = false; // Flag for tracking multi-line status
        let currentStatusCode = ''; // Store the current status code for multi-line responses

        client.on('data', (data) => {
            buffer += data;
            let index;

            while ((index = buffer.indexOf(CRLF)) !== -1) {
                const line = buffer.substring(0, index).trim();
                buffer = buffer.substring(index + CRLF.length);

                if (!firstData) {
                    firstData = true;
                    console.log("First time connected, should wait for 220");
                }

                const potentialStatusCode = line.substring(0, 3); // Extract the first three characters
                const fourthChar = line.charAt(3); // Get the 4th character

                // If the line's 4th character is a '-', it's a part of a multi-line response
                if (fourthChar === '-') {
                    isMultiLine = true;
                    currentStatusCode = potentialStatusCode;
                    multiLineBuffer += line + CRLF; // Remove the status code and '-' and add to buffer
                    
                    continue; // Continue to the next iteration to keep collecting multi-line response
                }

                // If this line has the same status code as a previous line but no '-', then it is the end of a multi-line response
                if (isMultiLine && currentStatusCode === potentialStatusCode && fourthChar === ' ') {
                    const fullLine = multiLineBuffer + line; // Remove the status code and space
                    multiLineBuffer = ''; // Reset the buffer
                    isMultiLine = false; // Reset the multi-line flag
                    currentStatusCode = ''; // Reset the status code

                    try {
                        console.log("Handling complete multi-line response:", fullLine);
                        this.handleSMTPResponse({
                            lineOrMultiline: fullLine, 
                            client, 
                            sender, 
                            recipient, 
                            emailData: dataToSend,
                            multiline:true
                        });
                    } catch (err) {
                        client.end();
                        
                        this.previousCommand = ''
                        throw new Error(err);
                    }
                } else if (!isMultiLine) {
                    // Single-line response
                    try {
                        console.log("Handling single-line response:", line);
                        this.handleSMTPResponse({
                            lineOrMultiline: line, 
                            client, 
                            sender, 
                            recipient, 
                            emailData: dataToSend
                        });
                    } catch (err) {
                        client.end();
                        this.previousCommand = ''
                        throw new Error(err);
                    }
                }
            }
        });
    }
    
    /**
     * Canonicalizes headers and body in relaxed mode.
     * @param {string} headers - The headers of the email.
     * @param {string} body - The body of the email.
     * @returns {Object} - The canonicalized headers and body.
     */
    canonicalizeRelaxed(headers, body) {
        const canonicalizedHeaders = headers.split(CRLF)
        .map(line => {
            const [key, ...value] = line.split(':');
            return key + ':' + value.join(':').trim();
        })
        .join(CRLF);


        const canonicalizedBody = body.split(CRLF)
            .map(line => line.split(/\s+/).join(' ').trimEnd())
            .join(CRLF).trimEnd();

        return { canonicalizedHeaders, canonicalizedBody };
    }

    /**
     * Signs the email using DKIM.
     * @param {string} domain - The sender's domain.
     * @param {string} selector - The selector.
     * @param {string} privateKey - The private key.
     * @param {string} emailData - The email data.
     * @returns {string} - The DKIM signature.
     */
    signEmail(domain, selector, privateKey, emailData) {
        try {
            const [headers, ...bodyParts] = emailData.split(CRLF + CRLF);
            const body = bodyParts.join(CRLF + CRLF);
        
            const { canonicalizedHeaders, canonicalizedBody } = 
            this.canonicalizeRelaxed(headers, body);
            const bodyHash = crypto.createHash('sha256')
            .update(canonicalizedBody).digest('base64');
        
            const headerFields = canonicalizedHeaders
            .split(CRLF).map(line => line.split(':')[0]).join(':');
            const dkimHeader = `v=1;a=rsa-sha256;c=relaxed/relaxed;d=${domain};s=${selector};bh=${bodyHash};h=${headerFields};`;
        
            const signature = crypto.createSign('SHA256').update(dkimHeader + CRLF + canonicalizedHeaders).sign(privateKey, 'base64');
        
            return `${dkimHeader}b=${signature}`;
        } catch(e) {
            console.error("There was an error", e);
            console.log("The private key is: ", this.privateKey, privateKey)
            return emailData;
        }
        
    }

}


/**
 * determine if we can use TLS by checking
 * if our cert and key exist.
 */





const smtpClient = new AwtsmoosEmailClient(
);

async function main() {
    try {
        await smtpClient.sendMail('[email protected]', 
        '[email protected]', 'B"H', 
        'This is a test email! The time is: ' + Date.now() 
        + " Which is " + 
        (new Date()));
        console.log('Email sent successfully');
    } catch (err) {
        console.error('Failed to send email:', err);
    }
}

main();

module.exports = AwtsmoosEmailClient;

Server pretty much the same.

DKIM record something like:

selector._domainkey TXT v=DKIM1; k=rsa; p=MIIBCg..(Your public key)

Inversely answered 27/9, 2023 at 5:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.