328 lines
10 KiB
JavaScript
Executable File
328 lines
10 KiB
JavaScript
Executable File
const {createConnection} = require('net');
|
||
const {resolveMx} = require('dns');
|
||
const {DKIMSign} = require('dkim-signer');
|
||
const CRLF = '\r\n';
|
||
|
||
function dummy () {}
|
||
module.exports = function (options) {
|
||
options = options || {};
|
||
const logger = options.logger || (options.silent && {
|
||
debug: dummy,
|
||
info: dummy,
|
||
warn: dummy,
|
||
error: dummy
|
||
} || {
|
||
debug: console.log,
|
||
info: console.info,
|
||
warn: console.warn,
|
||
error: console.error
|
||
});
|
||
const dkimPrivateKey = (options.dkim || {}).privateKey;
|
||
const dkimKeySelector = (options.dkim || {}).keySelector || 'dkim';
|
||
const devPort = options.devPort || -1;
|
||
const devHost = options.devHost || 'localhost';
|
||
const smtpPort = options.smtpPort || 25
|
||
const smtpHost = options.smtpHost || -1
|
||
/*
|
||
* 邮件服务返回代码含义 Mail service return code Meaning
|
||
* 500 格式错误,命令不可识别(此错误也包括命令行过长)format error, command unrecognized (This error also includes command line too long)
|
||
* 501 参数格式错误 parameter format error
|
||
* 502 命令不可实现 command can not be achieved
|
||
* 503 错误的命令序列 Bad sequence of commands
|
||
* 504 命令参数不可实现 command parameter can not be achieved
|
||
* 211 系统状态或系统帮助响应 System status, or system help response
|
||
* 214 帮助信息 help
|
||
* 220 服务就绪 Services Ready
|
||
* 221 服务关闭传输信道 Service closing transmission channel
|
||
* 421 服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)service is not ready to close the transmission channel (when it is necessary to close, this response may be in response to any command)
|
||
* 250 要求的邮件操作完成 requested mail action completed
|
||
* 251 用户非本地,将转发向 non-local users will be forwarded to
|
||
* 450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)Mail the required operation 450 unfinished, mailbox unavailable (for example, mailbox busy)
|
||
* 550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)Mail action not completed the required 550 mailbox unavailable (eg, mailbox not found, no access)
|
||
* 451 放弃要求的操作;处理过程中出错 waiver operation; processing error
|
||
* 551 用户非本地,请尝试 non-local user, please try
|
||
* 452 系统存储不足,要求的操作未执行 Less than 452 storage system, requiring action not taken
|
||
* 552 过量的存储分配,要求的操作未执行 excess storage allocation requires action not taken
|
||
* 553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误) mailbox name is not available, that the requested operation is not performed (for example, mailbox format error)
|
||
* 354 开始邮件输入,以.结束 Start Mail input to. End
|
||
* 554 操作失败 The operation failed
|
||
* 535 用户验证失败 User authentication failed
|
||
* 235 用户验证成功 user authentication is successful
|
||
* 334 等待用户输入验证信息 waits for the user to enter authentication information
|
||
*/
|
||
|
||
function getHost (email) {
|
||
const m = /[^@]+@([\w\d\-\.]+)/.exec(email);
|
||
return m && m[1];
|
||
}
|
||
|
||
function groupRecipients (recipients) {
|
||
let groups = {};
|
||
let host;
|
||
const recipients_length = recipients.length;
|
||
for (let i = 0; i < recipients_length; i++) {
|
||
host = getHost(recipients[i]);
|
||
(groups[host] || (groups[host] = [])).push(recipients[i])
|
||
}
|
||
return groups
|
||
}
|
||
|
||
/**
|
||
* connect to domain by Mx record
|
||
*/
|
||
function connectMx (domain, callback) {
|
||
if (devPort === -1) { // not in development mode -> search the MX
|
||
resolveMx(domain, function (err, data) {
|
||
if (err) {
|
||
return callback(err)
|
||
}
|
||
|
||
data.sort(function (a, b) { return a.priority < b.priority });
|
||
logger.debug('mx resolved: ', data);
|
||
|
||
if (!data || data.length === 0) {
|
||
return callback(new Error('can not resolve Mx of <' + domain + '>'))
|
||
}
|
||
if(smtpHost !== -1)data.push({exchange:smtpHost})
|
||
function tryConnect (i) {
|
||
if (i >= data.length) return callback(new Error('can not connect to any SMTP server'));
|
||
|
||
const sock = createConnection(smtpPort, data[i].exchange);
|
||
|
||
sock.on('error', function (err) {
|
||
logger.error('Error on connectMx for: ', data[i], err);
|
||
tryConnect(++i)
|
||
});
|
||
|
||
sock.on('connect', function () {
|
||
logger.debug('MX connection created: ', data[i].exchange);
|
||
sock.removeAllListeners('error');
|
||
callback(null, sock)
|
||
})
|
||
}
|
||
|
||
tryConnect(0)
|
||
})
|
||
} else { // development mode -> connect to the specified devPort on devHost
|
||
const sock = createConnection(devPort, devHost);
|
||
|
||
sock.on('error', function (err) {
|
||
callback(new Error('Error on connectMx (development) for "'+ devHost +':' + devPort + '": ' + err))
|
||
});
|
||
|
||
sock.on('connect', function () {
|
||
logger.debug('MX (development) connection created: '+ devHost +':' + devPort);
|
||
sock.removeAllListeners('error');
|
||
callback(null, sock)
|
||
})
|
||
}
|
||
}
|
||
|
||
function sendToSMTP (domain, srcHost, from, recipients, body, cb) {
|
||
const callback = (typeof cb === 'function') ? cb : function () {};
|
||
connectMx(domain, function (err, sock) {
|
||
if (err) {
|
||
logger.error('error on connectMx', err.stack);
|
||
return callback(err)
|
||
}
|
||
|
||
function w (s) {
|
||
logger.debug('send ' + domain + '>' + s);
|
||
sock.write(s + CRLF)
|
||
}
|
||
|
||
sock.setEncoding('utf8');
|
||
|
||
sock.on('data', function (chunk) {
|
||
data += chunk;
|
||
parts = data.split(CRLF);
|
||
const parts_length = parts.length - 1;
|
||
for (let i = 0, len = parts_length; i < len; i++) {
|
||
onLine(parts[i])
|
||
}
|
||
data = parts[parts.length - 1]
|
||
});
|
||
|
||
sock.on('error', function (err) {
|
||
logger.error('fail to connect ' + domain)
|
||
callback(err)
|
||
});
|
||
|
||
let data = '';
|
||
let step = 0;
|
||
let loginStep = 0;
|
||
const queue = [];
|
||
const login = [];
|
||
let parts;
|
||
let cmd;
|
||
|
||
/*
|
||
if(mail.user && mail.pass){
|
||
queue.push('AUTH LOGIN');
|
||
login.push(new Buffer(mail.user).toString("base64"));
|
||
login.push(new Buffer(mail.pass).toString("base64"));
|
||
}
|
||
*/
|
||
|
||
queue.push('MAIL FROM:<' + from + '>');
|
||
const recipients_length = recipients.length;
|
||
for (let i = 0; i < recipients_length; i++) {
|
||
queue.push('RCPT TO:<' + recipients[i] + '>')
|
||
}
|
||
queue.push('DATA');
|
||
queue.push('QUIT');
|
||
queue.push('');
|
||
|
||
function response (code, msg) {
|
||
switch (code) {
|
||
case 220:
|
||
//* 220 on server ready
|
||
//* 220 服务就绪
|
||
if (/\besmtp\b/i.test(msg)) {
|
||
// TODO: determin AUTH type; auth login, auth crm-md5, auth plain
|
||
cmd = 'EHLO'
|
||
} else {
|
||
cmd = 'HELO'
|
||
}
|
||
w(cmd + ' ' + srcHost);
|
||
break;
|
||
|
||
case 221: // bye
|
||
case 235: // verify ok
|
||
case 250: // operation OK
|
||
case 251: // foward
|
||
if (step === queue.length - 1) {
|
||
logger.info('OK:', code, msg);
|
||
callback(null, msg)
|
||
}
|
||
w(queue[step]);
|
||
step++;
|
||
break;
|
||
|
||
case 354: // start input end with . (dot)
|
||
logger.info('sending mail', body);
|
||
w(body);
|
||
w('');
|
||
w('.');
|
||
break;
|
||
|
||
case 334: // input login
|
||
w(login[loginStep]);
|
||
loginStep++;
|
||
break;
|
||
|
||
default:
|
||
if (code >= 400) {
|
||
logger.warn('SMTP responds error code', code);
|
||
callback(new Error('SMTP code:' + code + ' msg:' + msg));
|
||
sock.end();
|
||
}
|
||
}
|
||
}
|
||
|
||
let msg = '';
|
||
|
||
function onLine (line) {
|
||
logger.debug('recv ' + domain + '>' + line);
|
||
|
||
msg += (line + CRLF);
|
||
|
||
if (line[3] === ' ') {
|
||
// 250-information dash is not complete.
|
||
// 250 OK. space is complete.
|
||
response(parseInt(line), msg);
|
||
msg = '';
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function getAddress (address) {
|
||
return address.replace(/^.+</, '').replace(/>\s*$/, '').trim();
|
||
}
|
||
|
||
function getAddresses (addresses) {
|
||
const results = [];
|
||
if (!Array.isArray(addresses)) {
|
||
addresses = addresses.split(',');
|
||
}
|
||
|
||
const addresses_length = addresses.length;
|
||
for (let i = 0; i < addresses_length; i++) {
|
||
results.push(getAddress(addresses[i]));
|
||
}
|
||
return results
|
||
}
|
||
|
||
/**
|
||
* sendmail directly
|
||
*
|
||
* @param mail {object}
|
||
* from
|
||
* to
|
||
* cc
|
||
* bcc
|
||
* replyTo
|
||
* returnTo
|
||
* subject
|
||
* type default 'text/plain', 'text/html'
|
||
* charset default 'utf-8'
|
||
* encoding default 'base64'
|
||
* id default timestamp+from
|
||
* headers object
|
||
* content
|
||
* attachments
|
||
* [{
|
||
* type
|
||
* filename
|
||
* content
|
||
* }].
|
||
*
|
||
* @param callback function(err, domain).
|
||
*
|
||
*/
|
||
function sendmail (mail, callback) {
|
||
const mailcomposer = require('mailcomposer');
|
||
const mailMe = mailcomposer(mail);
|
||
let recipients = [];
|
||
let groups;
|
||
let srcHost;
|
||
if (mail.to) {
|
||
recipients = recipients.concat(getAddresses(mail.to))
|
||
}
|
||
|
||
if (mail.cc) {
|
||
recipients = recipients.concat(getAddresses(mail.cc))
|
||
}
|
||
|
||
if (mail.bcc) {
|
||
recipients = recipients.concat(getAddresses(mail.bcc))
|
||
}
|
||
|
||
groups = groupRecipients(recipients);
|
||
|
||
const from = getAddress(mail.from);
|
||
srcHost = getHost(from);
|
||
|
||
mailMe.build(function (err, message) {
|
||
if (err) {
|
||
logger.error('Error on creating message : ', err)
|
||
callback(err, null);
|
||
return
|
||
}
|
||
if (dkimPrivateKey) {
|
||
const signature = DKIMSign(message, {
|
||
privateKey: dkimPrivateKey,
|
||
keySelector: dkimKeySelector,
|
||
domainName: srcHost
|
||
});
|
||
message = signature + '\r\n' + message
|
||
}
|
||
for (let domain in groups) {
|
||
sendToSMTP(domain, srcHost, from, groups[domain], message, callback)
|
||
}
|
||
});
|
||
}
|
||
return sendmail
|
||
};
|