node.js.netemailserversmtp

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


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 myemail@mydomain.com 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 myemail@mydomain.com, 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

/**
 * 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):

/**
 * @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('reply@awtsmoos.one', 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('me@awtsmoos.com', 'awtsmoos@gmail.com', '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;

Solution

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

    Client:

    /**
     * @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('me@awtsmoos.one', 
            'awtsmoos@gmail.com', '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)