maildove
Version:
Send emails using Node.js only
307 lines • 13.5 kB
JavaScript
"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