UNPKG

maildove

Version:

Send emails using Node.js only

307 lines 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MailDove = void 0; const dns_1 = require("dns"); const tls_1 = require("tls"); const net_1 = require("net"); const dkim_signer_1 = require("dkim-signer"); const address_utils_1 = require("./address-utils"); const mail_composer_1 = __importDefault(require("nodemailer/lib/mail-composer")); const email_addresses_1 = __importDefault(require("email-addresses")); const logging_1 = __importDefault(require("./logging")); const resolver = new dns_1.promises.Resolver(); const addressUtils = new address_utils_1.AddressUtils(); const CRLF = '\r\n'; const smtpCodes = { ServiceReady: 220, Bye: 221, AuthSuccess: 235, OperationOK: 250, ForwardNonLocalUser: 251, StartMailBody: 354, ServerChallenge: 334, NegativeCompletion: 400, }; class MailDove { constructor(options) { this.step = 0; this.queue = []; // TODO: May crash in multi domain scenario // Where one of the domains is not TLS capable. this.upgraded = false; this.isUpgradeInProgress = false; this.writeToSocket = (s, domain) => { logging_1.default.debug(`SEND ${domain}> ${s}`); this.sock.write(s + CRLF); }; this.dkimEnabled = options.dkimEnabled || false; this.dkimKeySelector = options.dkimKeySelector || ''; this.dkimPrivateKey = options.dkimPrivateKey || ''; this.smtpPort = options.smtpPort || 25; this.smtpHost = options.smtpHost; this.startTLS = options.startTLS || false; this.rejectUnauthorized = options.rejectUnauthorized || true; this.tls = options.tls || { key: '', cert: '' }; } /** * Resolve MX records by domain. * @param {string} domain * @returns {Promise<MXRecord[]>} */ async resolveMX(domain) { if (this.smtpHost !== '' && this.smtpHost) { return ([{ exchange: this.smtpHost, priority: 1 }]); } try { const resolvedMX = await resolver.resolveMx(domain); resolvedMX.sort(function (a, b) { return a.priority - b.priority; }); return resolvedMX; } catch (ex) { throw Error(`Failed to resolve MX for ${domain}: ${ex}`); } } // /*** // * Send email using SMTP. // * @param {string} domain `to` address. // * @param {string} srcHost Source hostname. // * @param {string} from Source from address. // * @param {string[]}recipients Recipients list. // * @param {string} body Email body // * @returns {Promise<string>} // */ async sendToSMTP(domain, srcHost, from, recipients, body) { const resolvedMX = await this.resolveMX(domain); logging_1.default.log("debug", "Resolved MX %O", resolvedMX); await new Promise((resolve, reject) => { let isConnected = false; let connectedExchange; for (const mx of resolvedMX) { if (isConnected) { break; } this.sock = (0, net_1.createConnection)(this.smtpPort, mx.exchange); // eslint-disable-next-line no-loop-func this.sock.on('connect', () => { logging_1.default.info('Connected to exchange: ' + mx.exchange); connectedExchange = mx.exchange; this.sock.removeAllListeners('error'); }); this.sock.on('error', function (err) { logging_1.default.error(`Could not connect to exchange ${mx}, ${err}`); }); } this.sock.setEncoding('utf8'); this.sock.on('data', (chunk) => { // Convert RCVD to an array const received = chunk.toString().split(CRLF); // Remove last element(whitespace) from received received.pop(); for (const line of received) { this.parseInputAndRespond(line, domain, srcHost, body, connectedExchange, resolve); } }); this.sock.on('error', (err) => { reject(`Failed to connect to ${domain}: ${err}`); return; }); // Build rest of the SMTP exchange queue this.queue.push('MAIL FROM:<' + from + '>'); const recipientsLength = recipients.length; for (let i = 0; i < recipientsLength; i++) { this.queue.push('RCPT TO:<' + recipients[i] + '>'); } logging_1.default.log("verbose", "RCPTS %O", recipients); this.queue.push('DATA'); this.queue.push('QUIT'); this.queue.push(''); }); return domain; } parseInputAndRespond(line, domain, srcHost, body, connectedExchange, resolve) { logging_1.default.debug('RECV ' + domain + '>' + line); const message = line + CRLF; if (line[3] === ' ') { // 250 - Requested mail action okay, completed. const lineNumber = parseInt(line.substring(0, 4)); this.handleResponse(lineNumber, message, domain, srcHost, body, connectedExchange, resolve); } } handleResponse(code, msg, domain, srcHost, body, connectedExchange, resolve) { switch (code) { case smtpCodes.ServiceReady: //220 - On <domain> Service ready // Check if TLS upgrade is in progress if (this.isUpgradeInProgress === true) { this.sock.removeAllListeners('data'); const original = this.sock; // Pause the original socket and copy some options from it // to create a new socket. original.pause(); const opts = { socket: this.sock, host: connectedExchange, rejectUnauthorized: this.rejectUnauthorized, }; if (this.startTLS) { opts["secureContext"] = (0, tls_1.createSecureContext)({ cert: this.tls.cert, key: this.tls.key, }); } // Connect to the new socket with the copied options + secureContext. this.sock = (0, tls_1.connect)(opts, () => { this.sock.on('data', (chunk) => { // Convert RCVD to an array const received = chunk.toString().split(CRLF); for (const line of received) { this.parseInputAndRespond(line, domain, srcHost, body, connectedExchange, resolve); } }); this.sock.removeAllListeners('close'); this.sock.removeAllListeners('end'); }); this.sock.on('error', function (err) { console.warn('Could not upgrade to TLS:', err, 'Falling back to plaintext'); }); // TLS Unsuccessful -> Resume plaintext connection original.resume(); this.upgraded = true; this.writeToSocket('EHLO ' + srcHost, domain); break; } else { let helloCommand; // check for ESMTP/ignore-case if (/\besmtp\b/i.test(msg)) { // TODO: determine AUTH type for relay; auth consolein, auth crm-md5, auth plain helloCommand = 'EHLO'; } else { // SMTP Only, hence don't check for STARTTLS this.upgraded = true; helloCommand = 'HELO'; } this.writeToSocket(`${helloCommand} ${srcHost}`, domain); break; } case smtpCodes.Bye: // BYE this.sock.end(); // Reset step counter this.step = 0; // Clear the command queue // https://es5.github.io/x15.4.html#x15.4 // whenever the length property is changed, every property // whose name is an array index whose value is not smaller // than the new length is automatically deleted this.queue.length = 0; this.sock.removeAllListeners('close'); this.sock.removeAllListeners('end'); resolve(domain); return; case smtpCodes.AuthSuccess: // AUTH-Verify OK case smtpCodes.OperationOK: // Operation OK if (this.upgraded !== true) { // check for STARTTLS/ignore-case if (/\bSTARTTLS\b/i.test(msg) && this.startTLS) { console.debug('Server supports STARTTLS, continuing'); this.writeToSocket('STARTTLS', domain); this.isUpgradeInProgress = true; break; } else { this.upgraded = true; logging_1.default.debug('No STARTTLS support or ignored, continuing'); } } this.writeToSocket(this.queue[this.step], domain); this.step++; break; case smtpCodes.ForwardNonLocalUser: // User not local; will forward. if (this.step === this.queue.length - 1) { console.info('OK:', code, msg); return; } this.writeToSocket(this.queue[this.step], domain); this.step++; break; case smtpCodes.StartMailBody: // Start mail input // Inform end by `<CR><LF>.<CR><LF>` this.writeToSocket(body, domain); this.writeToSocket('', domain); this.writeToSocket('.', domain); break; case smtpCodes.ServerChallenge: // Send consolein details [for relay] // TODO: support login. // writeToSocket(login[loginStep]); // loginStep++; break; default: if (code >= smtpCodes.NegativeCompletion) { console.error('SMTP server responds with error code', code); this.sock.end(); resolve(`SMTP server responded with code: ${code} + ${msg}`); } } } ; /** * Send Mail directly * @param mail Mail object containing message, to/from etc. * Complete attributes reference: https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields * @returns {Promise<string[]>} */ async sendmail(mail) { // TODO: return void on success or error let recipients = []; const successOutboundRecipients = []; if (mail.to) { recipients = recipients.concat(addressUtils.getAddressesFromString(String(mail.to))); } if (mail.cc) { recipients = recipients.concat(addressUtils.getAddressesFromString(String(mail.cc))); } if (mail.bcc) { recipients = recipients.concat(addressUtils.getAddressesFromString(String(mail.bcc))); } const groups = addressUtils.groupRecipientsByDomain(recipients); const parsedEmail = email_addresses_1.default.parseOneAddress(String(mail.from)); if ((parsedEmail === null || parsedEmail === void 0 ? void 0 : parsedEmail.type) === 'mailbox') { let message = await new mail_composer_1.default(mail).compile().build(); if (this.dkimEnabled) { // eslint-disable-next-line new-cap const signature = (0, dkim_signer_1.DKIMSign)(message, { privateKey: this.dkimPrivateKey, keySelector: this.dkimKeySelector, domainName: parsedEmail.domain, }); message = Buffer.from(signature + CRLF + message, 'utf8'); } // eslint-disable-next-line guard-for-in for (const domain in groups) { try { logging_1.default.info(`DOMN: Group: ${groups[domain]}`); successOutboundRecipients.push(await this.sendToSMTP(domain, parsedEmail.domain, parsedEmail.address, groups[domain], message.toString())); } catch (ex) { logging_1.default.error(`Could not send email to ${domain}: ${ex}`); } } } if (!successOutboundRecipients.length) { throw "Could not send mails to any of the recipients"; } return successOutboundRecipients; } } exports.MailDove = MailDove; //# sourceMappingURL=maildove.js.map