UNPKG

10.8 kBJavaScriptView Raw
1const {createConnection} = require('net');
2const {resolveMx} = require('dns');
3const {DKIMSign} = require('dkim-signer');
4const CRLF = '\r\n';
5
6function dummy () {}
7module.exports = function (options) {
8 options = options || {};
9 const logger = options.logger || (options.silent && {
10 debug: dummy,
11 info: dummy,
12 warn: dummy,
13 error: dummy
14 } || {
15 debug: console.log,
16 info: console.info,
17 warn: console.warn,
18 error: console.error
19 });
20 const dkimPrivateKey = (options.dkim || {}).privateKey;
21 const dkimKeySelector = (options.dkim || {}).keySelector || 'dkim';
22 const devPort = options.devPort || -1;
23 const devHost = options.devHost || 'localhost';
24 const smtpPort = options.smtpPort || 25
25 const smtpHost = options.smtpHost || -1
26 /*
27 * 邮件服务返回代码含义 Mail service return code Meaning
28 * 500 格式错误,命令不可识别(此错误也包括命令行过长)format error, command unrecognized (This error also includes command line too long)
29 * 501 参数格式错误 parameter format error
30 * 502 命令不可实现 command can not be achieved
31 * 503 错误的命令序列 Bad sequence of commands
32 * 504 命令参数不可实现 command parameter can not be achieved
33 * 211 系统状态或系统帮助响应 System status, or system help response
34 * 214 帮助信息 help
35 * 220 服务就绪 Services Ready
36 * 221 服务关闭传输信道 Service closing transmission channel
37 * 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)
38 * 250 要求的邮件操作完成 requested mail action completed
39 * 251 用户非本地,将转发向 non-local users will be forwarded to
40 * 450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)Mail the required operation 450 unfinished, mailbox unavailable (for example, mailbox busy)
41 * 550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)Mail action not completed the required 550 mailbox unavailable (eg, mailbox not found, no access)
42 * 451 放弃要求的操作;处理过程中出错 waiver operation; processing error
43 * 551 用户非本地,请尝试 non-local user, please try
44 * 452 系统存储不足,要求的操作未执行 Less than 452 storage system, requiring action not taken
45 * 552 过量的存储分配,要求的操作未执行 excess storage allocation requires action not taken
46 * 553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误) mailbox name is not available, that the requested operation is not performed (for example, mailbox format error)
47 * 354 开始邮件输入,以.结束 Start Mail input to. End
48 * 554 操作失败 The operation failed
49 * 535 用户验证失败 User authentication failed
50 * 235 用户验证成功 user authentication is successful
51 * 334 等待用户输入验证信息 waits for the user to enter authentication information
52 */
53
54 function getHost (email) {
55 const m = /[^@]+@([\w\d\-\.]+)/.exec(email);
56 return m && m[1];
57 }
58
59 function groupRecipients (recipients) {
60 let groups = {};
61 let host;
62 const recipients_length = recipients.length;
63 for (let i = 0; i < recipients_length; i++) {
64 host = getHost(recipients[i]);
65 (groups[host] || (groups[host] = [])).push(recipients[i])
66 }
67 return groups
68 }
69
70 /**
71 * connect to domain by Mx record
72 */
73 function connectMx (domain, callback) {
74 if (devPort === -1) { // not in development mode -> search the MX
75 resolveMx(domain, function (err, data) {
76 if (err) {
77 return callback(err)
78 }
79
80 data.sort(function (a, b) { return a.priority > b.priority });
81 logger.debug('mx resolved: ', data);
82
83 if (!data || data.length === 0) {
84 return callback(new Error('can not resolve Mx of <' + domain + '>'))
85 }
86 if(smtpHost !== -1)data.push({exchange:smtpHost})
87 function tryConnect (i) {
88 if (i >= data.length) return callback(new Error('can not connect to any SMTP server'));
89
90 const sock = createConnection(smtpPort, data[i].exchange);
91
92 sock.on('error', function (err) {
93 logger.error('Error on connectMx for: ', data[i], err);
94 tryConnect(++i)
95 });
96
97 sock.on('connect', function () {
98 logger.debug('MX connection created: ', data[i].exchange);
99 sock.removeAllListeners('error');
100 callback(null, sock)
101 })
102 }
103
104 tryConnect(0)
105 })
106 } else { // development mode -> connect to the specified devPort on devHost
107 const sock = createConnection(devPort, devHost);
108
109 sock.on('error', function (err) {
110 callback(new Error('Error on connectMx (development) for "'+ devHost +':' + devPort + '": ' + err))
111 });
112
113 sock.on('connect', function () {
114 logger.debug('MX (development) connection created: '+ devHost +':' + devPort);
115 sock.removeAllListeners('error');
116 callback(null, sock)
117 })
118 }
119 }
120
121 function sendToSMTP (domain, srcHost, from, recipients, body, cb) {
122 const callback = (typeof cb === 'function') ? cb : function () {};
123 connectMx(domain, function (err, sock) {
124 if (err) {
125 logger.error('error on connectMx', err.stack);
126 return callback(err)
127 }
128
129 function w (s) {
130 logger.debug('send ' + domain + '>' + s);
131 sock.write(s + CRLF)
132 }
133
134 sock.setEncoding('utf8');
135
136 sock.on('data', function (chunk) {
137 data += chunk;
138 parts = data.split(CRLF);
139 const parts_length = parts.length - 1;
140 for (let i = 0, len = parts_length; i < len; i++) {
141 onLine(parts[i])
142 }
143 data = parts[parts.length - 1]
144 });
145
146 sock.on('error', function (err) {
147 logger.error('fail to connect ' + domain)
148 callback(err)
149 });
150
151 let data = '';
152 let step = 0;
153 let loginStep = 0;
154 const queue = [];
155 const login = [];
156 let parts;
157 let cmd;
158
159 /*
160 if(mail.user && mail.pass){
161 queue.push('AUTH LOGIN');
162 login.push(new Buffer(mail.user).toString("base64"));
163 login.push(new Buffer(mail.pass).toString("base64"));
164 }
165 */
166
167 queue.push('MAIL FROM:<' + from + '>');
168 const recipients_length = recipients.length;
169 for (let i = 0; i < recipients_length; i++) {
170 queue.push('RCPT TO:<' + recipients[i] + '>')
171 }
172 queue.push('DATA');
173 queue.push('QUIT');
174 queue.push('');
175
176 function response (code, msg) {
177 switch (code) {
178 case 220:
179 //* 220 on server ready
180 //* 220 服务就绪
181 if (/\besmtp\b/i.test(msg)) {
182 // TODO: determin AUTH type; auth login, auth crm-md5, auth plain
183 cmd = 'EHLO'
184 } else {
185 cmd = 'HELO'
186 }
187 w(cmd + ' ' + srcHost);
188 break;
189
190 case 221: // bye
191 case 235: // verify ok
192 case 250: // operation OK
193 case 251: // foward
194 if (step === queue.length - 1) {
195 logger.info('OK:', code, msg);
196 callback(null, msg)
197 }
198 w(queue[step]);
199 step++;
200 break;
201
202 case 354: // start input end with . (dot)
203 logger.info('sending mail', body);
204 w(body);
205 w('');
206 w('.');
207 break;
208
209 case 334: // input login
210 w(login[loginStep]);
211 loginStep++;
212 break;
213
214 default:
215 if (code >= 400) {
216 logger.warn('SMTP responds error code', code);
217 callback(new Error('SMTP code:' + code + ' msg:' + msg));
218 sock.end();
219 }
220 }
221 }
222
223 let msg = '';
224
225 function onLine (line) {
226 logger.debug('recv ' + domain + '>' + line);
227
228 msg += (line + CRLF);
229
230 if (line[3] === ' ') {
231 // 250-information dash is not complete.
232 // 250 OK. space is complete.
233 let lineNumber = parseInt(line);
234 response(lineNumber, msg);
235 msg = '';
236 }
237 }
238 })
239 }
240
241 function getAddress (address) {
242 return address.replace(/^.+</, '').replace(/>\s*$/, '').trim();
243 }
244
245 function getAddresses (addresses) {
246 const results = [];
247 if (!Array.isArray(addresses)) {
248 addresses = addresses.split(',');
249 }
250
251 const addresses_length = addresses.length;
252 for (let i = 0; i < addresses_length; i++) {
253 results.push(getAddress(addresses[i]));
254 }
255 return results
256 }
257
258 /**
259 * sendmail directly
260 *
261 * @param mail {object}
262 * from
263 * to
264 * cc
265 * bcc
266 * replyTo
267 * returnTo
268 * subject
269 * type default 'text/plain', 'text/html'
270 * charset default 'utf-8'
271 * encoding default 'base64'
272 * id default timestamp+from
273 * headers object
274 * content
275 * attachments
276 * [{
277 * type
278 * filename
279 * content
280 * }].
281 *
282 * @param callback function(err, domain).
283 *
284 */
285 function sendmail (mail, callback) {
286 const mailcomposer = require('mailcomposer');
287 const mailMe = mailcomposer(mail);
288 let recipients = [];
289 let groups;
290 let srcHost;
291 if (mail.to) {
292 recipients = recipients.concat(getAddresses(mail.to))
293 }
294
295 if (mail.cc) {
296 recipients = recipients.concat(getAddresses(mail.cc))
297 }
298
299 if (mail.bcc) {
300 recipients = recipients.concat(getAddresses(mail.bcc))
301 }
302
303 groups = groupRecipients(recipients);
304
305 const from = getAddress(mail.from);
306 srcHost = getHost(from);
307
308 mailMe.build(function (err, message) {
309 if (err) {
310 logger.error('Error on creating message : ', err)
311 callback(err, null);
312 return
313 }
314 if (dkimPrivateKey) {
315 const signature = DKIMSign(message, {
316 privateKey: dkimPrivateKey,
317 keySelector: dkimKeySelector,
318 domainName: srcHost
319 });
320 message = signature + '\r\n' + message
321 }
322 for (let domain in groups) {
323 sendToSMTP(domain, srcHost, from, groups[domain], message, callback)
324 }
325 });
326 }
327 return sendmail
328};