1 | const {createConnection} = require('net');
|
2 | const {resolveMx} = require('dns');
|
3 | const {DKIMSign} = require('dkim-signer');
|
4 | const CRLF = '\r\n';
|
5 |
|
6 | function dummy () {}
|
7 | module.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 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
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 |
|
72 |
|
73 | function connectMx (domain, callback) {
|
74 | if (devPort === -1) {
|
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 {
|
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 |
|
161 |
|
162 |
|
163 |
|
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 |
|
180 |
|
181 | if (/\besmtp\b/i.test(msg)) {
|
182 |
|
183 | cmd = 'EHLO'
|
184 | } else {
|
185 | cmd = 'HELO'
|
186 | }
|
187 | w(cmd + ' ' + srcHost);
|
188 | break;
|
189 |
|
190 | case 221:
|
191 | case 235:
|
192 | case 250:
|
193 | case 251:
|
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:
|
203 | logger.info('sending mail', body);
|
204 | w(body);
|
205 | w('');
|
206 | w('.');
|
207 | break;
|
208 |
|
209 | case 334:
|
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 |
|
232 |
|
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 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
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 | };
|