zhuha 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of zhuha might be problematic. Click here for more details.
- package/06-02.html +81 -0
- package/06-02.js +72 -0
- package/06-03.js +7 -0
- package/06-04.js +7 -0
- package/AnswersLW5.pdf +0 -0
- package/m0603/m0603.js +30 -0
- package/m0603/node_modules/.package-lock.json +16 -0
- package/m0603/node_modules/nodemailer/.gitattributes +6 -0
- package/m0603/node_modules/nodemailer/.prettierrc.js +8 -0
- package/m0603/node_modules/nodemailer/CHANGELOG.md +725 -0
- package/m0603/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
- package/m0603/node_modules/nodemailer/CONTRIBUTING.md +67 -0
- package/m0603/node_modules/nodemailer/LICENSE +16 -0
- package/m0603/node_modules/nodemailer/README.md +97 -0
- package/m0603/node_modules/nodemailer/SECURITY.txt +22 -0
- package/m0603/node_modules/nodemailer/lib/addressparser/index.js +313 -0
- package/m0603/node_modules/nodemailer/lib/base64/index.js +142 -0
- package/m0603/node_modules/nodemailer/lib/dkim/index.js +251 -0
- package/m0603/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
- package/m0603/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
- package/m0603/node_modules/nodemailer/lib/dkim/sign.js +117 -0
- package/m0603/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
- package/m0603/node_modules/nodemailer/lib/fetch/index.js +274 -0
- package/m0603/node_modules/nodemailer/lib/json-transport/index.js +82 -0
- package/m0603/node_modules/nodemailer/lib/mail-composer/index.js +558 -0
- package/m0603/node_modules/nodemailer/lib/mailer/index.js +427 -0
- package/m0603/node_modules/nodemailer/lib/mailer/mail-message.js +315 -0
- package/m0603/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
- package/m0603/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2102 -0
- package/m0603/node_modules/nodemailer/lib/mime-node/index.js +1290 -0
- package/m0603/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
- package/m0603/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
- package/m0603/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
- package/m0603/node_modules/nodemailer/lib/nodemailer.js +143 -0
- package/m0603/node_modules/nodemailer/lib/qp/index.js +219 -0
- package/m0603/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
- package/m0603/node_modules/nodemailer/lib/ses-transport/index.js +349 -0
- package/m0603/node_modules/nodemailer/lib/shared/index.js +638 -0
- package/m0603/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
- package/m0603/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
- package/m0603/node_modules/nodemailer/lib/smtp-connection/index.js +1796 -0
- package/m0603/node_modules/nodemailer/lib/smtp-pool/index.js +648 -0
- package/m0603/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +253 -0
- package/m0603/node_modules/nodemailer/lib/smtp-transport/index.js +416 -0
- package/m0603/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
- package/m0603/node_modules/nodemailer/lib/well-known/index.js +47 -0
- package/m0603/node_modules/nodemailer/lib/well-known/services.json +286 -0
- package/m0603/node_modules/nodemailer/lib/xoauth2/index.js +376 -0
- package/m0603/node_modules/nodemailer/package.json +46 -0
- package/m0603/node_modules/nodemailer/postinstall.js +101 -0
- package/m0603/package-lock.json +31 -0
- package/m0603/package.json +15 -0
- package/package.json +16 -0
@@ -0,0 +1,1796 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const packageInfo = require('../../package.json');
|
4
|
+
const EventEmitter = require('events').EventEmitter;
|
5
|
+
const net = require('net');
|
6
|
+
const tls = require('tls');
|
7
|
+
const os = require('os');
|
8
|
+
const crypto = require('crypto');
|
9
|
+
const DataStream = require('./data-stream');
|
10
|
+
const PassThrough = require('stream').PassThrough;
|
11
|
+
const shared = require('../shared');
|
12
|
+
|
13
|
+
// default timeout values in ms
|
14
|
+
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
15
|
+
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
16
|
+
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
|
17
|
+
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Generates a SMTP connection object
|
21
|
+
*
|
22
|
+
* Optional options object takes the following possible properties:
|
23
|
+
*
|
24
|
+
* * **port** - is the port to connect to (defaults to 587 or 465)
|
25
|
+
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
26
|
+
* * **secure** - use SSL
|
27
|
+
* * **ignoreTLS** - ignore server support for STARTTLS
|
28
|
+
* * **requireTLS** - forces the client to use STARTTLS
|
29
|
+
* * **name** - the name of the client server
|
30
|
+
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
31
|
+
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
|
32
|
+
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
33
|
+
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
|
34
|
+
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
35
|
+
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
36
|
+
* * **logger** - bunyan compatible logger interface
|
37
|
+
* * **debug** - if true pass SMTP traffic to the logger
|
38
|
+
* * **tls** - options for createCredentials
|
39
|
+
* * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
|
40
|
+
* * **secured** - boolean indicates that the provided socket has already been upgraded to tls
|
41
|
+
*
|
42
|
+
* @constructor
|
43
|
+
* @namespace SMTP Client module
|
44
|
+
* @param {Object} [options] Option properties
|
45
|
+
*/
|
46
|
+
class SMTPConnection extends EventEmitter {
|
47
|
+
constructor(options) {
|
48
|
+
super(options);
|
49
|
+
|
50
|
+
this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
|
51
|
+
this.stage = 'init';
|
52
|
+
|
53
|
+
this.options = options || {};
|
54
|
+
|
55
|
+
this.secureConnection = !!this.options.secure;
|
56
|
+
this.alreadySecured = !!this.options.secured;
|
57
|
+
|
58
|
+
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
59
|
+
this.host = this.options.host || 'localhost';
|
60
|
+
|
61
|
+
this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
|
62
|
+
|
63
|
+
if (typeof this.options.secure === 'undefined' && this.port === 465) {
|
64
|
+
// if secure option is not set but port is 465, then default to secure
|
65
|
+
this.secureConnection = true;
|
66
|
+
}
|
67
|
+
|
68
|
+
this.name = this.options.name || this._getHostname();
|
69
|
+
|
70
|
+
this.logger = shared.getLogger(this.options, {
|
71
|
+
component: this.options.component || 'smtp-connection',
|
72
|
+
sid: this.id
|
73
|
+
});
|
74
|
+
|
75
|
+
this.customAuth = new Map();
|
76
|
+
Object.keys(this.options.customAuth || {}).forEach(key => {
|
77
|
+
let mapKey = (key || '').toString().trim().toUpperCase();
|
78
|
+
if (!mapKey) {
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
this.customAuth.set(mapKey, this.options.customAuth[key]);
|
82
|
+
});
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Expose version nr, just for the reference
|
86
|
+
* @type {String}
|
87
|
+
*/
|
88
|
+
this.version = packageInfo.version;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* If true, then the user is authenticated
|
92
|
+
* @type {Boolean}
|
93
|
+
*/
|
94
|
+
this.authenticated = false;
|
95
|
+
|
96
|
+
/**
|
97
|
+
* If set to true, this instance is no longer active
|
98
|
+
* @private
|
99
|
+
*/
|
100
|
+
this.destroyed = false;
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Defines if the current connection is secure or not. If not,
|
104
|
+
* STARTTLS can be used if available
|
105
|
+
* @private
|
106
|
+
*/
|
107
|
+
this.secure = !!this.secureConnection;
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Store incomplete messages coming from the server
|
111
|
+
* @private
|
112
|
+
*/
|
113
|
+
this._remainder = '';
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Unprocessed responses from the server
|
117
|
+
* @type {Array}
|
118
|
+
*/
|
119
|
+
this._responseQueue = [];
|
120
|
+
|
121
|
+
this.lastServerResponse = false;
|
122
|
+
|
123
|
+
/**
|
124
|
+
* The socket connecting to the server
|
125
|
+
* @publick
|
126
|
+
*/
|
127
|
+
this._socket = false;
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Lists supported auth mechanisms
|
131
|
+
* @private
|
132
|
+
*/
|
133
|
+
this._supportedAuth = [];
|
134
|
+
|
135
|
+
/**
|
136
|
+
* Set to true, if EHLO response includes "AUTH".
|
137
|
+
* If false then authentication is not tried
|
138
|
+
*/
|
139
|
+
this.allowsAuth = false;
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Includes current envelope (from, to)
|
143
|
+
* @private
|
144
|
+
*/
|
145
|
+
this._envelope = false;
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Lists supported extensions
|
149
|
+
* @private
|
150
|
+
*/
|
151
|
+
this._supportedExtensions = [];
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Defines the maximum allowed size for a single message
|
155
|
+
* @private
|
156
|
+
*/
|
157
|
+
this._maxAllowedSize = 0;
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Function queue to run if a data chunk comes from the server
|
161
|
+
* @private
|
162
|
+
*/
|
163
|
+
this._responseActions = [];
|
164
|
+
this._recipientQueue = [];
|
165
|
+
|
166
|
+
/**
|
167
|
+
* Timeout variable for waiting the greeting
|
168
|
+
* @private
|
169
|
+
*/
|
170
|
+
this._greetingTimeout = false;
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Timeout variable for waiting the connection to start
|
174
|
+
* @private
|
175
|
+
*/
|
176
|
+
this._connectionTimeout = false;
|
177
|
+
|
178
|
+
/**
|
179
|
+
* If the socket is deemed already closed
|
180
|
+
* @private
|
181
|
+
*/
|
182
|
+
this._destroyed = false;
|
183
|
+
|
184
|
+
/**
|
185
|
+
* If the socket is already being closed
|
186
|
+
* @private
|
187
|
+
*/
|
188
|
+
this._closing = false;
|
189
|
+
|
190
|
+
/**
|
191
|
+
* Callbacks for socket's listeners
|
192
|
+
*/
|
193
|
+
this._onSocketData = chunk => this._onData(chunk);
|
194
|
+
this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
|
195
|
+
this._onSocketClose = () => this._onClose();
|
196
|
+
this._onSocketEnd = () => this._onEnd();
|
197
|
+
this._onSocketTimeout = () => this._onTimeout();
|
198
|
+
}
|
199
|
+
|
200
|
+
/**
|
201
|
+
* Creates a connection to a SMTP server and sets up connection
|
202
|
+
* listener
|
203
|
+
*/
|
204
|
+
connect(connectCallback) {
|
205
|
+
if (typeof connectCallback === 'function') {
|
206
|
+
this.once('connect', () => {
|
207
|
+
this.logger.debug(
|
208
|
+
{
|
209
|
+
tnx: 'smtp'
|
210
|
+
},
|
211
|
+
'SMTP handshake finished'
|
212
|
+
);
|
213
|
+
connectCallback();
|
214
|
+
});
|
215
|
+
|
216
|
+
const isDestroyedMessage = this._isDestroyedMessage('connect');
|
217
|
+
if (isDestroyedMessage) {
|
218
|
+
return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
let opts = {
|
223
|
+
port: this.port,
|
224
|
+
host: this.host,
|
225
|
+
allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
|
226
|
+
timeout: this.options.dnsTimeout || DNS_TIMEOUT
|
227
|
+
};
|
228
|
+
|
229
|
+
if (this.options.localAddress) {
|
230
|
+
opts.localAddress = this.options.localAddress;
|
231
|
+
}
|
232
|
+
|
233
|
+
let setupConnectionHandlers = () => {
|
234
|
+
this._connectionTimeout = setTimeout(() => {
|
235
|
+
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
|
236
|
+
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
237
|
+
|
238
|
+
this._socket.on('error', this._onSocketError);
|
239
|
+
};
|
240
|
+
|
241
|
+
if (this.options.connection) {
|
242
|
+
// connection is already opened
|
243
|
+
this._socket = this.options.connection;
|
244
|
+
if (this.secureConnection && !this.alreadySecured) {
|
245
|
+
setImmediate(() =>
|
246
|
+
this._upgradeConnection(err => {
|
247
|
+
if (err) {
|
248
|
+
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
|
249
|
+
return;
|
250
|
+
}
|
251
|
+
this._onConnect();
|
252
|
+
})
|
253
|
+
);
|
254
|
+
} else {
|
255
|
+
setImmediate(() => this._onConnect());
|
256
|
+
}
|
257
|
+
return;
|
258
|
+
} else if (this.options.socket) {
|
259
|
+
// socket object is set up but not yet connected
|
260
|
+
this._socket = this.options.socket;
|
261
|
+
return shared.resolveHostname(opts, (err, resolved) => {
|
262
|
+
if (err) {
|
263
|
+
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
264
|
+
}
|
265
|
+
this.logger.debug(
|
266
|
+
{
|
267
|
+
tnx: 'dns',
|
268
|
+
source: opts.host,
|
269
|
+
resolved: resolved.host,
|
270
|
+
cached: !!resolved.cached
|
271
|
+
},
|
272
|
+
'Resolved %s as %s [cache %s]',
|
273
|
+
opts.host,
|
274
|
+
resolved.host,
|
275
|
+
resolved.cached ? 'hit' : 'miss'
|
276
|
+
);
|
277
|
+
Object.keys(resolved).forEach(key => {
|
278
|
+
if (key.charAt(0) !== '_' && resolved[key]) {
|
279
|
+
opts[key] = resolved[key];
|
280
|
+
}
|
281
|
+
});
|
282
|
+
try {
|
283
|
+
this._socket.connect(this.port, this.host, () => {
|
284
|
+
this._socket.setKeepAlive(true);
|
285
|
+
this._onConnect();
|
286
|
+
});
|
287
|
+
setupConnectionHandlers();
|
288
|
+
} catch (E) {
|
289
|
+
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
290
|
+
}
|
291
|
+
});
|
292
|
+
} else if (this.secureConnection) {
|
293
|
+
// connect using tls
|
294
|
+
if (this.options.tls) {
|
295
|
+
Object.keys(this.options.tls).forEach(key => {
|
296
|
+
opts[key] = this.options.tls[key];
|
297
|
+
});
|
298
|
+
}
|
299
|
+
return shared.resolveHostname(opts, (err, resolved) => {
|
300
|
+
if (err) {
|
301
|
+
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
302
|
+
}
|
303
|
+
this.logger.debug(
|
304
|
+
{
|
305
|
+
tnx: 'dns',
|
306
|
+
source: opts.host,
|
307
|
+
resolved: resolved.host,
|
308
|
+
cached: !!resolved.cached
|
309
|
+
},
|
310
|
+
'Resolved %s as %s [cache %s]',
|
311
|
+
opts.host,
|
312
|
+
resolved.host,
|
313
|
+
resolved.cached ? 'hit' : 'miss'
|
314
|
+
);
|
315
|
+
Object.keys(resolved).forEach(key => {
|
316
|
+
if (key.charAt(0) !== '_' && resolved[key]) {
|
317
|
+
opts[key] = resolved[key];
|
318
|
+
}
|
319
|
+
});
|
320
|
+
try {
|
321
|
+
this._socket = tls.connect(opts, () => {
|
322
|
+
this._socket.setKeepAlive(true);
|
323
|
+
this._onConnect();
|
324
|
+
});
|
325
|
+
setupConnectionHandlers();
|
326
|
+
} catch (E) {
|
327
|
+
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
328
|
+
}
|
329
|
+
});
|
330
|
+
} else {
|
331
|
+
// connect using plaintext
|
332
|
+
return shared.resolveHostname(opts, (err, resolved) => {
|
333
|
+
if (err) {
|
334
|
+
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
335
|
+
}
|
336
|
+
this.logger.debug(
|
337
|
+
{
|
338
|
+
tnx: 'dns',
|
339
|
+
source: opts.host,
|
340
|
+
resolved: resolved.host,
|
341
|
+
cached: !!resolved.cached
|
342
|
+
},
|
343
|
+
'Resolved %s as %s [cache %s]',
|
344
|
+
opts.host,
|
345
|
+
resolved.host,
|
346
|
+
resolved.cached ? 'hit' : 'miss'
|
347
|
+
);
|
348
|
+
Object.keys(resolved).forEach(key => {
|
349
|
+
if (key.charAt(0) !== '_' && resolved[key]) {
|
350
|
+
opts[key] = resolved[key];
|
351
|
+
}
|
352
|
+
});
|
353
|
+
try {
|
354
|
+
this._socket = net.connect(opts, () => {
|
355
|
+
this._socket.setKeepAlive(true);
|
356
|
+
this._onConnect();
|
357
|
+
});
|
358
|
+
setupConnectionHandlers();
|
359
|
+
} catch (E) {
|
360
|
+
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
361
|
+
}
|
362
|
+
});
|
363
|
+
}
|
364
|
+
}
|
365
|
+
|
366
|
+
/**
|
367
|
+
* Sends QUIT
|
368
|
+
*/
|
369
|
+
quit() {
|
370
|
+
this._sendCommand('QUIT');
|
371
|
+
this._responseActions.push(this.close);
|
372
|
+
}
|
373
|
+
|
374
|
+
/**
|
375
|
+
* Closes the connection to the server
|
376
|
+
*/
|
377
|
+
close() {
|
378
|
+
clearTimeout(this._connectionTimeout);
|
379
|
+
clearTimeout(this._greetingTimeout);
|
380
|
+
this._responseActions = [];
|
381
|
+
|
382
|
+
// allow to run this function only once
|
383
|
+
if (this._closing) {
|
384
|
+
return;
|
385
|
+
}
|
386
|
+
this._closing = true;
|
387
|
+
|
388
|
+
let closeMethod = 'end';
|
389
|
+
|
390
|
+
if (this.stage === 'init') {
|
391
|
+
// Close the socket immediately when connection timed out
|
392
|
+
closeMethod = 'destroy';
|
393
|
+
}
|
394
|
+
|
395
|
+
this.logger.debug(
|
396
|
+
{
|
397
|
+
tnx: 'smtp'
|
398
|
+
},
|
399
|
+
'Closing connection to the server using "%s"',
|
400
|
+
closeMethod
|
401
|
+
);
|
402
|
+
|
403
|
+
let socket = (this._socket && this._socket.socket) || this._socket;
|
404
|
+
|
405
|
+
if (socket && !socket.destroyed) {
|
406
|
+
try {
|
407
|
+
this._socket[closeMethod]();
|
408
|
+
} catch (E) {
|
409
|
+
// just ignore
|
410
|
+
}
|
411
|
+
}
|
412
|
+
|
413
|
+
this._destroy();
|
414
|
+
}
|
415
|
+
|
416
|
+
/**
|
417
|
+
* Authenticate user
|
418
|
+
*/
|
419
|
+
login(authData, callback) {
|
420
|
+
const isDestroyedMessage = this._isDestroyedMessage('login');
|
421
|
+
if (isDestroyedMessage) {
|
422
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
423
|
+
}
|
424
|
+
|
425
|
+
this._auth = authData || {};
|
426
|
+
// Select SASL authentication method
|
427
|
+
this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
|
428
|
+
|
429
|
+
if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
|
430
|
+
this._authMethod = 'XOAUTH2';
|
431
|
+
} else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
|
432
|
+
// use first supported
|
433
|
+
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
|
434
|
+
}
|
435
|
+
|
436
|
+
if (this._authMethod !== 'XOAUTH2' && (!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) {
|
437
|
+
if (this._auth.user && this._auth.pass) {
|
438
|
+
this._auth.credentials = {
|
439
|
+
user: this._auth.user,
|
440
|
+
pass: this._auth.pass,
|
441
|
+
options: this._auth.options
|
442
|
+
};
|
443
|
+
} else {
|
444
|
+
return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
|
445
|
+
}
|
446
|
+
}
|
447
|
+
|
448
|
+
if (this.customAuth.has(this._authMethod)) {
|
449
|
+
let handler = this.customAuth.get(this._authMethod);
|
450
|
+
let lastResponse;
|
451
|
+
let returned = false;
|
452
|
+
|
453
|
+
let resolve = () => {
|
454
|
+
if (returned) {
|
455
|
+
return;
|
456
|
+
}
|
457
|
+
returned = true;
|
458
|
+
this.logger.info(
|
459
|
+
{
|
460
|
+
tnx: 'smtp',
|
461
|
+
username: this._auth.user,
|
462
|
+
action: 'authenticated',
|
463
|
+
method: this._authMethod
|
464
|
+
},
|
465
|
+
'User %s authenticated',
|
466
|
+
JSON.stringify(this._auth.user)
|
467
|
+
);
|
468
|
+
this.authenticated = true;
|
469
|
+
callback(null, true);
|
470
|
+
};
|
471
|
+
|
472
|
+
let reject = err => {
|
473
|
+
if (returned) {
|
474
|
+
return;
|
475
|
+
}
|
476
|
+
returned = true;
|
477
|
+
callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
|
478
|
+
};
|
479
|
+
|
480
|
+
let handlerResponse = handler({
|
481
|
+
auth: this._auth,
|
482
|
+
method: this._authMethod,
|
483
|
+
|
484
|
+
extensions: [].concat(this._supportedExtensions),
|
485
|
+
authMethods: [].concat(this._supportedAuth),
|
486
|
+
maxAllowedSize: this._maxAllowedSize || false,
|
487
|
+
|
488
|
+
sendCommand: (cmd, done) => {
|
489
|
+
let promise;
|
490
|
+
|
491
|
+
if (!done) {
|
492
|
+
promise = new Promise((resolve, reject) => {
|
493
|
+
done = shared.callbackPromise(resolve, reject);
|
494
|
+
});
|
495
|
+
}
|
496
|
+
|
497
|
+
this._responseActions.push(str => {
|
498
|
+
lastResponse = str;
|
499
|
+
|
500
|
+
let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
|
501
|
+
let data = {
|
502
|
+
command: cmd,
|
503
|
+
response: str
|
504
|
+
};
|
505
|
+
if (codes) {
|
506
|
+
data.status = Number(codes[1]) || 0;
|
507
|
+
if (codes[2]) {
|
508
|
+
data.code = codes[2];
|
509
|
+
}
|
510
|
+
data.text = str.substr(codes[0].length);
|
511
|
+
} else {
|
512
|
+
data.text = str;
|
513
|
+
data.status = 0; // just in case we need to perform numeric comparisons
|
514
|
+
}
|
515
|
+
done(null, data);
|
516
|
+
});
|
517
|
+
setImmediate(() => this._sendCommand(cmd));
|
518
|
+
|
519
|
+
return promise;
|
520
|
+
},
|
521
|
+
|
522
|
+
resolve,
|
523
|
+
reject
|
524
|
+
});
|
525
|
+
|
526
|
+
if (handlerResponse && typeof handlerResponse.catch === 'function') {
|
527
|
+
// a promise was returned
|
528
|
+
handlerResponse.then(resolve).catch(reject);
|
529
|
+
}
|
530
|
+
|
531
|
+
return;
|
532
|
+
}
|
533
|
+
|
534
|
+
switch (this._authMethod) {
|
535
|
+
case 'XOAUTH2':
|
536
|
+
this._handleXOauth2Token(false, callback);
|
537
|
+
return;
|
538
|
+
case 'LOGIN':
|
539
|
+
this._responseActions.push(str => {
|
540
|
+
this._actionAUTH_LOGIN_USER(str, callback);
|
541
|
+
});
|
542
|
+
this._sendCommand('AUTH LOGIN');
|
543
|
+
return;
|
544
|
+
case 'PLAIN':
|
545
|
+
this._responseActions.push(str => {
|
546
|
+
this._actionAUTHComplete(str, callback);
|
547
|
+
});
|
548
|
+
this._sendCommand(
|
549
|
+
'AUTH PLAIN ' +
|
550
|
+
Buffer.from(
|
551
|
+
//this._auth.user+'\u0000'+
|
552
|
+
'\u0000' + // skip authorization identity as it causes problems with some servers
|
553
|
+
this._auth.credentials.user +
|
554
|
+
'\u0000' +
|
555
|
+
this._auth.credentials.pass,
|
556
|
+
'utf-8'
|
557
|
+
).toString('base64'),
|
558
|
+
// log entry without passwords
|
559
|
+
'AUTH PLAIN ' +
|
560
|
+
Buffer.from(
|
561
|
+
//this._auth.user+'\u0000'+
|
562
|
+
'\u0000' + // skip authorization identity as it causes problems with some servers
|
563
|
+
this._auth.credentials.user +
|
564
|
+
'\u0000' +
|
565
|
+
'/* secret */',
|
566
|
+
'utf-8'
|
567
|
+
).toString('base64')
|
568
|
+
);
|
569
|
+
return;
|
570
|
+
case 'CRAM-MD5':
|
571
|
+
this._responseActions.push(str => {
|
572
|
+
this._actionAUTH_CRAM_MD5(str, callback);
|
573
|
+
});
|
574
|
+
this._sendCommand('AUTH CRAM-MD5');
|
575
|
+
return;
|
576
|
+
}
|
577
|
+
|
578
|
+
return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
|
579
|
+
}
|
580
|
+
|
581
|
+
/**
|
582
|
+
* Sends a message
|
583
|
+
*
|
584
|
+
* @param {Object} envelope Envelope object, {from: addr, to: [addr]}
|
585
|
+
* @param {Object} message String, Buffer or a Stream
|
586
|
+
* @param {Function} callback Callback to return once sending is completed
|
587
|
+
*/
|
588
|
+
send(envelope, message, done) {
|
589
|
+
if (!message) {
|
590
|
+
return done(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
|
591
|
+
}
|
592
|
+
|
593
|
+
const isDestroyedMessage = this._isDestroyedMessage('send message');
|
594
|
+
if (isDestroyedMessage) {
|
595
|
+
return done(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
596
|
+
}
|
597
|
+
|
598
|
+
// reject larger messages than allowed
|
599
|
+
if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
|
600
|
+
return setImmediate(() => {
|
601
|
+
done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
|
602
|
+
});
|
603
|
+
}
|
604
|
+
|
605
|
+
// ensure that callback is only called once
|
606
|
+
let returned = false;
|
607
|
+
let callback = function () {
|
608
|
+
if (returned) {
|
609
|
+
return;
|
610
|
+
}
|
611
|
+
returned = true;
|
612
|
+
|
613
|
+
done(...arguments);
|
614
|
+
};
|
615
|
+
|
616
|
+
if (typeof message.on === 'function') {
|
617
|
+
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
|
618
|
+
}
|
619
|
+
|
620
|
+
let startTime = Date.now();
|
621
|
+
this._setEnvelope(envelope, (err, info) => {
|
622
|
+
if (err) {
|
623
|
+
return callback(err);
|
624
|
+
}
|
625
|
+
let envelopeTime = Date.now();
|
626
|
+
let stream = this._createSendStream((err, str) => {
|
627
|
+
if (err) {
|
628
|
+
return callback(err);
|
629
|
+
}
|
630
|
+
|
631
|
+
info.envelopeTime = envelopeTime - startTime;
|
632
|
+
info.messageTime = Date.now() - envelopeTime;
|
633
|
+
info.messageSize = stream.outByteCount;
|
634
|
+
info.response = str;
|
635
|
+
|
636
|
+
return callback(null, info);
|
637
|
+
});
|
638
|
+
if (typeof message.pipe === 'function') {
|
639
|
+
message.pipe(stream);
|
640
|
+
} else {
|
641
|
+
stream.write(message);
|
642
|
+
stream.end();
|
643
|
+
}
|
644
|
+
});
|
645
|
+
}
|
646
|
+
|
647
|
+
/**
|
648
|
+
* Resets connection state
|
649
|
+
*
|
650
|
+
* @param {Function} callback Callback to return once connection is reset
|
651
|
+
*/
|
652
|
+
reset(callback) {
|
653
|
+
this._sendCommand('RSET');
|
654
|
+
this._responseActions.push(str => {
|
655
|
+
if (str.charAt(0) !== '2') {
|
656
|
+
return callback(this._formatError('Could not reset session state. response=' + str, 'EPROTOCOL', str, 'RSET'));
|
657
|
+
}
|
658
|
+
this._envelope = false;
|
659
|
+
return callback(null, true);
|
660
|
+
});
|
661
|
+
}
|
662
|
+
|
663
|
+
/**
|
664
|
+
* Connection listener that is run when the connection to
|
665
|
+
* the server is opened
|
666
|
+
*
|
667
|
+
* @event
|
668
|
+
*/
|
669
|
+
_onConnect() {
|
670
|
+
clearTimeout(this._connectionTimeout);
|
671
|
+
|
672
|
+
this.logger.info(
|
673
|
+
{
|
674
|
+
tnx: 'network',
|
675
|
+
localAddress: this._socket.localAddress,
|
676
|
+
localPort: this._socket.localPort,
|
677
|
+
remoteAddress: this._socket.remoteAddress,
|
678
|
+
remotePort: this._socket.remotePort
|
679
|
+
},
|
680
|
+
'%s established to %s:%s',
|
681
|
+
this.secure ? 'Secure connection' : 'Connection',
|
682
|
+
this._socket.remoteAddress,
|
683
|
+
this._socket.remotePort
|
684
|
+
);
|
685
|
+
|
686
|
+
if (this._destroyed) {
|
687
|
+
// Connection was established after we already had canceled it
|
688
|
+
this.close();
|
689
|
+
return;
|
690
|
+
}
|
691
|
+
|
692
|
+
this.stage = 'connected';
|
693
|
+
|
694
|
+
// clear existing listeners for the socket
|
695
|
+
this._socket.removeListener('data', this._onSocketData);
|
696
|
+
this._socket.removeListener('timeout', this._onSocketTimeout);
|
697
|
+
this._socket.removeListener('close', this._onSocketClose);
|
698
|
+
this._socket.removeListener('end', this._onSocketEnd);
|
699
|
+
|
700
|
+
this._socket.on('data', this._onSocketData);
|
701
|
+
this._socket.once('close', this._onSocketClose);
|
702
|
+
this._socket.once('end', this._onSocketEnd);
|
703
|
+
|
704
|
+
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
|
705
|
+
this._socket.on('timeout', this._onSocketTimeout);
|
706
|
+
|
707
|
+
this._greetingTimeout = setTimeout(() => {
|
708
|
+
// if still waiting for greeting, give up
|
709
|
+
if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
|
710
|
+
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
|
711
|
+
}
|
712
|
+
}, this.options.greetingTimeout || GREETING_TIMEOUT);
|
713
|
+
|
714
|
+
this._responseActions.push(this._actionGreeting);
|
715
|
+
|
716
|
+
// we have a 'data' listener set up so resume socket if it was paused
|
717
|
+
this._socket.resume();
|
718
|
+
}
|
719
|
+
|
720
|
+
/**
|
721
|
+
* 'data' listener for data coming from the server
|
722
|
+
*
|
723
|
+
* @event
|
724
|
+
* @param {Buffer} chunk Data chunk coming from the server
|
725
|
+
*/
|
726
|
+
_onData(chunk) {
|
727
|
+
if (this._destroyed || !chunk || !chunk.length) {
|
728
|
+
return;
|
729
|
+
}
|
730
|
+
|
731
|
+
let data = (chunk || '').toString('binary');
|
732
|
+
let lines = (this._remainder + data).split(/\r?\n/);
|
733
|
+
let lastline;
|
734
|
+
|
735
|
+
this._remainder = lines.pop();
|
736
|
+
|
737
|
+
for (let i = 0, len = lines.length; i < len; i++) {
|
738
|
+
if (this._responseQueue.length) {
|
739
|
+
lastline = this._responseQueue[this._responseQueue.length - 1];
|
740
|
+
if (/^\d+-/.test(lastline.split('\n').pop())) {
|
741
|
+
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
742
|
+
continue;
|
743
|
+
}
|
744
|
+
}
|
745
|
+
this._responseQueue.push(lines[i]);
|
746
|
+
}
|
747
|
+
|
748
|
+
if (this._responseQueue.length) {
|
749
|
+
lastline = this._responseQueue[this._responseQueue.length - 1];
|
750
|
+
if (/^\d+-/.test(lastline.split('\n').pop())) {
|
751
|
+
return;
|
752
|
+
}
|
753
|
+
}
|
754
|
+
|
755
|
+
this._processResponse();
|
756
|
+
}
|
757
|
+
|
758
|
+
/**
|
759
|
+
* 'error' listener for the socket
|
760
|
+
*
|
761
|
+
* @event
|
762
|
+
* @param {Error} err Error object
|
763
|
+
* @param {String} type Error name
|
764
|
+
*/
|
765
|
+
_onError(err, type, data, command) {
|
766
|
+
clearTimeout(this._connectionTimeout);
|
767
|
+
clearTimeout(this._greetingTimeout);
|
768
|
+
|
769
|
+
if (this._destroyed) {
|
770
|
+
// just ignore, already closed
|
771
|
+
// this might happen when a socket is canceled because of reached timeout
|
772
|
+
// but the socket timeout error itself receives only after
|
773
|
+
return;
|
774
|
+
}
|
775
|
+
|
776
|
+
err = this._formatError(err, type, data, command);
|
777
|
+
|
778
|
+
this.logger.error(data, err.message);
|
779
|
+
|
780
|
+
this.emit('error', err);
|
781
|
+
this.close();
|
782
|
+
}
|
783
|
+
|
784
|
+
_formatError(message, type, response, command) {
|
785
|
+
let err;
|
786
|
+
|
787
|
+
if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
|
788
|
+
err = message;
|
789
|
+
} else {
|
790
|
+
err = new Error(message);
|
791
|
+
}
|
792
|
+
|
793
|
+
if (type && type !== 'Error') {
|
794
|
+
err.code = type;
|
795
|
+
}
|
796
|
+
|
797
|
+
if (response) {
|
798
|
+
err.response = response;
|
799
|
+
err.message += ': ' + response;
|
800
|
+
}
|
801
|
+
|
802
|
+
let responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
803
|
+
if (responseCode) {
|
804
|
+
err.responseCode = responseCode;
|
805
|
+
}
|
806
|
+
|
807
|
+
if (command) {
|
808
|
+
err.command = command;
|
809
|
+
}
|
810
|
+
|
811
|
+
return err;
|
812
|
+
}
|
813
|
+
|
814
|
+
/**
|
815
|
+
* 'close' listener for the socket
|
816
|
+
*
|
817
|
+
* @event
|
818
|
+
*/
|
819
|
+
_onClose() {
|
820
|
+
this.logger.info(
|
821
|
+
{
|
822
|
+
tnx: 'network'
|
823
|
+
},
|
824
|
+
'Connection closed'
|
825
|
+
);
|
826
|
+
|
827
|
+
if (this.upgrading && !this._destroyed) {
|
828
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', false, 'CONN');
|
829
|
+
} else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
|
830
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
|
831
|
+
}
|
832
|
+
|
833
|
+
this._destroy();
|
834
|
+
}
|
835
|
+
|
836
|
+
/**
|
837
|
+
* 'end' listener for the socket
|
838
|
+
*
|
839
|
+
* @event
|
840
|
+
*/
|
841
|
+
_onEnd() {
|
842
|
+
if (this._socket && !this._socket.destroyed) {
|
843
|
+
this._socket.destroy();
|
844
|
+
}
|
845
|
+
}
|
846
|
+
|
847
|
+
/**
|
848
|
+
* 'timeout' listener for the socket
|
849
|
+
*
|
850
|
+
* @event
|
851
|
+
*/
|
852
|
+
_onTimeout() {
|
853
|
+
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
|
854
|
+
}
|
855
|
+
|
856
|
+
/**
|
857
|
+
* Destroys the client, emits 'end'
|
858
|
+
*/
|
859
|
+
_destroy() {
|
860
|
+
if (this._destroyed) {
|
861
|
+
return;
|
862
|
+
}
|
863
|
+
this._destroyed = true;
|
864
|
+
this.emit('end');
|
865
|
+
}
|
866
|
+
|
867
|
+
/**
|
868
|
+
* Upgrades the connection to TLS
|
869
|
+
*
|
870
|
+
* @param {Function} callback Callback function to run when the connection
|
871
|
+
* has been secured
|
872
|
+
*/
|
873
|
+
_upgradeConnection(callback) {
|
874
|
+
// do not remove all listeners or it breaks node v0.10 as there's
|
875
|
+
// apparently a 'finish' event set that would be cleared as well
|
876
|
+
|
877
|
+
// we can safely keep 'error', 'end', 'close' etc. events
|
878
|
+
this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
|
879
|
+
this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
|
880
|
+
|
881
|
+
let socketPlain = this._socket;
|
882
|
+
let opts = {
|
883
|
+
socket: this._socket,
|
884
|
+
host: this.host
|
885
|
+
};
|
886
|
+
|
887
|
+
Object.keys(this.options.tls || {}).forEach(key => {
|
888
|
+
opts[key] = this.options.tls[key];
|
889
|
+
});
|
890
|
+
|
891
|
+
this.upgrading = true;
|
892
|
+
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
893
|
+
try {
|
894
|
+
this._socket = tls.connect(opts, () => {
|
895
|
+
this.secure = true;
|
896
|
+
this.upgrading = false;
|
897
|
+
this._socket.on('data', this._onSocketData);
|
898
|
+
|
899
|
+
socketPlain.removeListener('close', this._onSocketClose);
|
900
|
+
socketPlain.removeListener('end', this._onSocketEnd);
|
901
|
+
|
902
|
+
return callback(null, true);
|
903
|
+
});
|
904
|
+
} catch (err) {
|
905
|
+
return callback(err);
|
906
|
+
}
|
907
|
+
|
908
|
+
this._socket.on('error', this._onSocketError);
|
909
|
+
this._socket.once('close', this._onSocketClose);
|
910
|
+
this._socket.once('end', this._onSocketEnd);
|
911
|
+
|
912
|
+
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
|
913
|
+
this._socket.on('timeout', this._onSocketTimeout);
|
914
|
+
|
915
|
+
// resume in case the socket was paused
|
916
|
+
socketPlain.resume();
|
917
|
+
}
|
918
|
+
|
919
|
+
/**
|
920
|
+
* Processes queued responses from the server
|
921
|
+
*
|
922
|
+
* @param {Boolean} force If true, ignores _processing flag
|
923
|
+
*/
|
924
|
+
_processResponse() {
|
925
|
+
if (!this._responseQueue.length) {
|
926
|
+
return false;
|
927
|
+
}
|
928
|
+
|
929
|
+
let str = (this.lastServerResponse = (this._responseQueue.shift() || '').toString());
|
930
|
+
|
931
|
+
if (/^\d+-/.test(str.split('\n').pop())) {
|
932
|
+
// keep waiting for the final part of multiline response
|
933
|
+
return;
|
934
|
+
}
|
935
|
+
|
936
|
+
if (this.options.debug || this.options.transactionLog) {
|
937
|
+
this.logger.debug(
|
938
|
+
{
|
939
|
+
tnx: 'server'
|
940
|
+
},
|
941
|
+
str.replace(/\r?\n$/, '')
|
942
|
+
);
|
943
|
+
}
|
944
|
+
|
945
|
+
if (!str.trim()) {
|
946
|
+
// skip unexpected empty lines
|
947
|
+
setImmediate(() => this._processResponse(true));
|
948
|
+
}
|
949
|
+
|
950
|
+
let action = this._responseActions.shift();
|
951
|
+
|
952
|
+
if (typeof action === 'function') {
|
953
|
+
action.call(this, str);
|
954
|
+
setImmediate(() => this._processResponse(true));
|
955
|
+
} else {
|
956
|
+
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
|
957
|
+
}
|
958
|
+
}
|
959
|
+
|
960
|
+
/**
|
961
|
+
* Send a command to the server, append \r\n
|
962
|
+
*
|
963
|
+
* @param {String} str String to be sent to the server
|
964
|
+
* @param {String} logStr Optional string to be used for logging instead of the actual string
|
965
|
+
*/
|
966
|
+
_sendCommand(str, logStr) {
|
967
|
+
if (this._destroyed) {
|
968
|
+
// Connection already closed, can't send any more data
|
969
|
+
return;
|
970
|
+
}
|
971
|
+
|
972
|
+
if (this._socket.destroyed) {
|
973
|
+
return this.close();
|
974
|
+
}
|
975
|
+
|
976
|
+
if (this.options.debug || this.options.transactionLog) {
|
977
|
+
this.logger.debug(
|
978
|
+
{
|
979
|
+
tnx: 'client'
|
980
|
+
},
|
981
|
+
(logStr || str || '').toString().replace(/\r?\n$/, '')
|
982
|
+
);
|
983
|
+
}
|
984
|
+
|
985
|
+
this._socket.write(Buffer.from(str + '\r\n', 'utf-8'));
|
986
|
+
}
|
987
|
+
|
988
|
+
/**
|
989
|
+
* Initiates a new message by submitting envelope data, starting with
|
990
|
+
* MAIL FROM: command
|
991
|
+
*
|
992
|
+
* @param {Object} envelope Envelope object in the form of
|
993
|
+
* {from:'...', to:['...']}
|
994
|
+
* or
|
995
|
+
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
996
|
+
*/
|
997
|
+
_setEnvelope(envelope, callback) {
|
998
|
+
let args = [];
|
999
|
+
let useSmtpUtf8 = false;
|
1000
|
+
|
1001
|
+
this._envelope = envelope || {};
|
1002
|
+
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
1003
|
+
|
1004
|
+
this._envelope.to = [].concat(this._envelope.to || []).map(to => ((to && to.address) || to || '').toString().trim());
|
1005
|
+
|
1006
|
+
if (!this._envelope.to.length) {
|
1007
|
+
return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
|
1011
|
+
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
|
1012
|
+
}
|
1013
|
+
|
1014
|
+
// check if the sender address uses only ASCII characters,
|
1015
|
+
// otherwise require usage of SMTPUTF8 extension
|
1016
|
+
if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
|
1017
|
+
useSmtpUtf8 = true;
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
for (let i = 0, len = this._envelope.to.length; i < len; i++) {
|
1021
|
+
if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
|
1022
|
+
return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
|
1023
|
+
}
|
1024
|
+
|
1025
|
+
// check if the recipients addresses use only ASCII characters,
|
1026
|
+
// otherwise require usage of SMTPUTF8 extension
|
1027
|
+
if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
|
1028
|
+
useSmtpUtf8 = true;
|
1029
|
+
}
|
1030
|
+
}
|
1031
|
+
|
1032
|
+
// clone the recipients array for latter manipulation
|
1033
|
+
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
|
1034
|
+
this._envelope.rejected = [];
|
1035
|
+
this._envelope.rejectedErrors = [];
|
1036
|
+
this._envelope.accepted = [];
|
1037
|
+
|
1038
|
+
if (this._envelope.dsn) {
|
1039
|
+
try {
|
1040
|
+
this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
|
1041
|
+
} catch (err) {
|
1042
|
+
return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
|
1043
|
+
}
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
this._responseActions.push(str => {
|
1047
|
+
this._actionMAIL(str, callback);
|
1048
|
+
});
|
1049
|
+
|
1050
|
+
// If the server supports SMTPUTF8 and the envelope includes an internationalized
|
1051
|
+
// email address then append SMTPUTF8 keyword to the MAIL FROM command
|
1052
|
+
if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
|
1053
|
+
args.push('SMTPUTF8');
|
1054
|
+
this._usingSmtpUtf8 = true;
|
1055
|
+
}
|
1056
|
+
|
1057
|
+
// If the server supports 8BITMIME and the message might contain non-ascii bytes
|
1058
|
+
// then append the 8BITMIME keyword to the MAIL FROM command
|
1059
|
+
if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
|
1060
|
+
args.push('BODY=8BITMIME');
|
1061
|
+
this._using8BitMime = true;
|
1062
|
+
}
|
1063
|
+
|
1064
|
+
if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
|
1065
|
+
args.push('SIZE=' + this._envelope.size);
|
1066
|
+
}
|
1067
|
+
|
1068
|
+
// If the server supports DSN and the envelope includes an DSN prop
|
1069
|
+
// then append DSN params to the MAIL FROM command
|
1070
|
+
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
1071
|
+
if (this._envelope.dsn.ret) {
|
1072
|
+
args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret));
|
1073
|
+
}
|
1074
|
+
if (this._envelope.dsn.envid) {
|
1075
|
+
args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
|
1076
|
+
}
|
1077
|
+
}
|
1078
|
+
|
1079
|
+
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
|
1080
|
+
}
|
1081
|
+
|
1082
|
+
_setDsnEnvelope(params) {
|
1083
|
+
let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
|
1084
|
+
if (ret) {
|
1085
|
+
switch (ret) {
|
1086
|
+
case 'HDRS':
|
1087
|
+
case 'HEADERS':
|
1088
|
+
ret = 'HDRS';
|
1089
|
+
break;
|
1090
|
+
case 'FULL':
|
1091
|
+
case 'BODY':
|
1092
|
+
ret = 'FULL';
|
1093
|
+
break;
|
1094
|
+
}
|
1095
|
+
}
|
1096
|
+
|
1097
|
+
if (ret && !['FULL', 'HDRS'].includes(ret)) {
|
1098
|
+
throw new Error('ret: ' + JSON.stringify(ret));
|
1099
|
+
}
|
1100
|
+
|
1101
|
+
let envid = (params.envid || params.id || '').toString() || null;
|
1102
|
+
|
1103
|
+
let notify = params.notify || null;
|
1104
|
+
if (notify) {
|
1105
|
+
if (typeof notify === 'string') {
|
1106
|
+
notify = notify.split(',');
|
1107
|
+
}
|
1108
|
+
notify = notify.map(n => n.trim().toUpperCase());
|
1109
|
+
let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
|
1110
|
+
let invaliNotify = notify.filter(n => !validNotify.includes(n));
|
1111
|
+
if (invaliNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
|
1112
|
+
throw new Error('notify: ' + JSON.stringify(notify.join(',')));
|
1113
|
+
}
|
1114
|
+
notify = notify.join(',');
|
1115
|
+
}
|
1116
|
+
|
1117
|
+
let orcpt = (params.recipient || params.orcpt || '').toString() || null;
|
1118
|
+
if (orcpt && orcpt.indexOf(';') < 0) {
|
1119
|
+
orcpt = 'rfc822;' + orcpt;
|
1120
|
+
}
|
1121
|
+
|
1122
|
+
return {
|
1123
|
+
ret,
|
1124
|
+
envid,
|
1125
|
+
notify,
|
1126
|
+
orcpt
|
1127
|
+
};
|
1128
|
+
}
|
1129
|
+
|
1130
|
+
_getDsnRcptToArgs() {
|
1131
|
+
let args = [];
|
1132
|
+
// If the server supports DSN and the envelope includes an DSN prop
|
1133
|
+
// then append DSN params to the RCPT TO command
|
1134
|
+
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
1135
|
+
if (this._envelope.dsn.notify) {
|
1136
|
+
args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify));
|
1137
|
+
}
|
1138
|
+
if (this._envelope.dsn.orcpt) {
|
1139
|
+
args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt));
|
1140
|
+
}
|
1141
|
+
}
|
1142
|
+
return args.length ? ' ' + args.join(' ') : '';
|
1143
|
+
}
|
1144
|
+
|
1145
|
+
_createSendStream(callback) {
|
1146
|
+
let dataStream = new DataStream();
|
1147
|
+
let logStream;
|
1148
|
+
|
1149
|
+
if (this.options.lmtp) {
|
1150
|
+
this._envelope.accepted.forEach((recipient, i) => {
|
1151
|
+
let final = i === this._envelope.accepted.length - 1;
|
1152
|
+
this._responseActions.push(str => {
|
1153
|
+
this._actionLMTPStream(recipient, final, str, callback);
|
1154
|
+
});
|
1155
|
+
});
|
1156
|
+
} else {
|
1157
|
+
this._responseActions.push(str => {
|
1158
|
+
this._actionSMTPStream(str, callback);
|
1159
|
+
});
|
1160
|
+
}
|
1161
|
+
|
1162
|
+
dataStream.pipe(this._socket, {
|
1163
|
+
end: false
|
1164
|
+
});
|
1165
|
+
|
1166
|
+
if (this.options.debug) {
|
1167
|
+
logStream = new PassThrough();
|
1168
|
+
logStream.on('readable', () => {
|
1169
|
+
let chunk;
|
1170
|
+
while ((chunk = logStream.read())) {
|
1171
|
+
this.logger.debug(
|
1172
|
+
{
|
1173
|
+
tnx: 'message'
|
1174
|
+
},
|
1175
|
+
chunk.toString('binary').replace(/\r?\n$/, '')
|
1176
|
+
);
|
1177
|
+
}
|
1178
|
+
});
|
1179
|
+
dataStream.pipe(logStream);
|
1180
|
+
}
|
1181
|
+
|
1182
|
+
dataStream.once('end', () => {
|
1183
|
+
this.logger.info(
|
1184
|
+
{
|
1185
|
+
tnx: 'message',
|
1186
|
+
inByteCount: dataStream.inByteCount,
|
1187
|
+
outByteCount: dataStream.outByteCount
|
1188
|
+
},
|
1189
|
+
'<%s bytes encoded mime message (source size %s bytes)>',
|
1190
|
+
dataStream.outByteCount,
|
1191
|
+
dataStream.inByteCount
|
1192
|
+
);
|
1193
|
+
});
|
1194
|
+
|
1195
|
+
return dataStream;
|
1196
|
+
}
|
1197
|
+
|
1198
|
+
/** ACTIONS **/
|
1199
|
+
|
1200
|
+
/**
|
1201
|
+
* Will be run after the connection is created and the server sends
|
1202
|
+
* a greeting. If the incoming message starts with 220 initiate
|
1203
|
+
* SMTP session by sending EHLO command
|
1204
|
+
*
|
1205
|
+
* @param {String} str Message from the server
|
1206
|
+
*/
|
1207
|
+
_actionGreeting(str) {
|
1208
|
+
clearTimeout(this._greetingTimeout);
|
1209
|
+
|
1210
|
+
if (str.substr(0, 3) !== '220') {
|
1211
|
+
this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
|
1212
|
+
return;
|
1213
|
+
}
|
1214
|
+
|
1215
|
+
if (this.options.lmtp) {
|
1216
|
+
this._responseActions.push(this._actionLHLO);
|
1217
|
+
this._sendCommand('LHLO ' + this.name);
|
1218
|
+
} else {
|
1219
|
+
this._responseActions.push(this._actionEHLO);
|
1220
|
+
this._sendCommand('EHLO ' + this.name);
|
1221
|
+
}
|
1222
|
+
}
|
1223
|
+
|
1224
|
+
/**
|
1225
|
+
* Handles server response for LHLO command. If it yielded in
|
1226
|
+
* error, emit 'error', otherwise treat this as an EHLO response
|
1227
|
+
*
|
1228
|
+
* @param {String} str Message from the server
|
1229
|
+
*/
|
1230
|
+
_actionLHLO(str) {
|
1231
|
+
if (str.charAt(0) !== '2') {
|
1232
|
+
this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
|
1233
|
+
return;
|
1234
|
+
}
|
1235
|
+
|
1236
|
+
this._actionEHLO(str);
|
1237
|
+
}
|
1238
|
+
|
1239
|
+
/**
|
1240
|
+
* Handles server response for EHLO command. If it yielded in
|
1241
|
+
* error, try HELO instead, otherwise initiate TLS negotiation
|
1242
|
+
* if STARTTLS is supported by the server or move into the
|
1243
|
+
* authentication phase.
|
1244
|
+
*
|
1245
|
+
* @param {String} str Message from the server
|
1246
|
+
*/
|
1247
|
+
_actionEHLO(str) {
|
1248
|
+
let match;
|
1249
|
+
|
1250
|
+
if (str.substr(0, 3) === '421') {
|
1251
|
+
this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
|
1252
|
+
return;
|
1253
|
+
}
|
1254
|
+
|
1255
|
+
if (str.charAt(0) !== '2') {
|
1256
|
+
if (this.options.requireTLS) {
|
1257
|
+
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
|
1258
|
+
return;
|
1259
|
+
}
|
1260
|
+
|
1261
|
+
// Try HELO instead
|
1262
|
+
this._responseActions.push(this._actionHELO);
|
1263
|
+
this._sendCommand('HELO ' + this.name);
|
1264
|
+
return;
|
1265
|
+
}
|
1266
|
+
|
1267
|
+
this._ehloLines = str
|
1268
|
+
.split(/\r?\n/)
|
1269
|
+
.map(line => line.replace(/^\d+[ -]/, '').trim())
|
1270
|
+
.filter(line => line)
|
1271
|
+
.slice(1);
|
1272
|
+
|
1273
|
+
// Detect if the server supports STARTTLS
|
1274
|
+
if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
|
1275
|
+
this._sendCommand('STARTTLS');
|
1276
|
+
this._responseActions.push(this._actionSTARTTLS);
|
1277
|
+
return;
|
1278
|
+
}
|
1279
|
+
|
1280
|
+
// Detect if the server supports SMTPUTF8
|
1281
|
+
if (/[ -]SMTPUTF8\b/im.test(str)) {
|
1282
|
+
this._supportedExtensions.push('SMTPUTF8');
|
1283
|
+
}
|
1284
|
+
|
1285
|
+
// Detect if the server supports DSN
|
1286
|
+
if (/[ -]DSN\b/im.test(str)) {
|
1287
|
+
this._supportedExtensions.push('DSN');
|
1288
|
+
}
|
1289
|
+
|
1290
|
+
// Detect if the server supports 8BITMIME
|
1291
|
+
if (/[ -]8BITMIME\b/im.test(str)) {
|
1292
|
+
this._supportedExtensions.push('8BITMIME');
|
1293
|
+
}
|
1294
|
+
|
1295
|
+
// Detect if the server supports PIPELINING
|
1296
|
+
if (/[ -]PIPELINING\b/im.test(str)) {
|
1297
|
+
this._supportedExtensions.push('PIPELINING');
|
1298
|
+
}
|
1299
|
+
|
1300
|
+
// Detect if the server supports AUTH
|
1301
|
+
if (/[ -]AUTH\b/i.test(str)) {
|
1302
|
+
this.allowsAuth = true;
|
1303
|
+
}
|
1304
|
+
|
1305
|
+
// Detect if the server supports PLAIN auth
|
1306
|
+
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
|
1307
|
+
this._supportedAuth.push('PLAIN');
|
1308
|
+
}
|
1309
|
+
|
1310
|
+
// Detect if the server supports LOGIN auth
|
1311
|
+
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
|
1312
|
+
this._supportedAuth.push('LOGIN');
|
1313
|
+
}
|
1314
|
+
|
1315
|
+
// Detect if the server supports CRAM-MD5 auth
|
1316
|
+
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
|
1317
|
+
this._supportedAuth.push('CRAM-MD5');
|
1318
|
+
}
|
1319
|
+
|
1320
|
+
// Detect if the server supports XOAUTH2 auth
|
1321
|
+
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
|
1322
|
+
this._supportedAuth.push('XOAUTH2');
|
1323
|
+
}
|
1324
|
+
|
1325
|
+
// Detect if the server supports SIZE extensions (and the max allowed size)
|
1326
|
+
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
1327
|
+
this._supportedExtensions.push('SIZE');
|
1328
|
+
this._maxAllowedSize = Number(match[1]) || 0;
|
1329
|
+
}
|
1330
|
+
|
1331
|
+
this.emit('connect');
|
1332
|
+
}
|
1333
|
+
|
1334
|
+
/**
|
1335
|
+
* Handles server response for HELO command. If it yielded in
|
1336
|
+
* error, emit 'error', otherwise move into the authentication phase.
|
1337
|
+
*
|
1338
|
+
* @param {String} str Message from the server
|
1339
|
+
*/
|
1340
|
+
_actionHELO(str) {
|
1341
|
+
if (str.charAt(0) !== '2') {
|
1342
|
+
this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
|
1343
|
+
return;
|
1344
|
+
}
|
1345
|
+
|
1346
|
+
// assume that authentication is enabled (most probably is not though)
|
1347
|
+
this.allowsAuth = true;
|
1348
|
+
|
1349
|
+
this.emit('connect');
|
1350
|
+
}
|
1351
|
+
|
1352
|
+
/**
|
1353
|
+
* Handles server response for STARTTLS command. If there's an error
|
1354
|
+
* try HELO instead, otherwise initiate TLS upgrade. If the upgrade
|
1355
|
+
* succeedes restart the EHLO
|
1356
|
+
*
|
1357
|
+
* @param {String} str Message from the server
|
1358
|
+
*/
|
1359
|
+
_actionSTARTTLS(str) {
|
1360
|
+
if (str.charAt(0) !== '2') {
|
1361
|
+
if (this.options.opportunisticTLS) {
|
1362
|
+
this.logger.info(
|
1363
|
+
{
|
1364
|
+
tnx: 'smtp'
|
1365
|
+
},
|
1366
|
+
'Failed STARTTLS upgrade, continuing unencrypted'
|
1367
|
+
);
|
1368
|
+
return this.emit('connect');
|
1369
|
+
}
|
1370
|
+
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
|
1371
|
+
return;
|
1372
|
+
}
|
1373
|
+
|
1374
|
+
this._upgradeConnection((err, secured) => {
|
1375
|
+
if (err) {
|
1376
|
+
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
|
1377
|
+
return;
|
1378
|
+
}
|
1379
|
+
|
1380
|
+
this.logger.info(
|
1381
|
+
{
|
1382
|
+
tnx: 'smtp'
|
1383
|
+
},
|
1384
|
+
'Connection upgraded with STARTTLS'
|
1385
|
+
);
|
1386
|
+
|
1387
|
+
if (secured) {
|
1388
|
+
// restart session
|
1389
|
+
if (this.options.lmtp) {
|
1390
|
+
this._responseActions.push(this._actionLHLO);
|
1391
|
+
this._sendCommand('LHLO ' + this.name);
|
1392
|
+
} else {
|
1393
|
+
this._responseActions.push(this._actionEHLO);
|
1394
|
+
this._sendCommand('EHLO ' + this.name);
|
1395
|
+
}
|
1396
|
+
} else {
|
1397
|
+
this.emit('connect');
|
1398
|
+
}
|
1399
|
+
});
|
1400
|
+
}
|
1401
|
+
|
1402
|
+
/**
|
1403
|
+
* Handle the response for AUTH LOGIN command. We are expecting
|
1404
|
+
* '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
|
1405
|
+
* response needs to be base64 encoded username. We do not need
|
1406
|
+
* exact match but settle with 334 response in general as some
|
1407
|
+
* hosts invalidly use a longer message than VXNlcm5hbWU6
|
1408
|
+
*
|
1409
|
+
* @param {String} str Message from the server
|
1410
|
+
*/
|
1411
|
+
_actionAUTH_LOGIN_USER(str, callback) {
|
1412
|
+
if (!/^334[ -]/.test(str)) {
|
1413
|
+
// expecting '334 VXNlcm5hbWU6'
|
1414
|
+
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
|
1415
|
+
return;
|
1416
|
+
}
|
1417
|
+
|
1418
|
+
this._responseActions.push(str => {
|
1419
|
+
this._actionAUTH_LOGIN_PASS(str, callback);
|
1420
|
+
});
|
1421
|
+
|
1422
|
+
this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
|
1423
|
+
}
|
1424
|
+
|
1425
|
+
/**
|
1426
|
+
* Handle the response for AUTH CRAM-MD5 command. We are expecting
|
1427
|
+
* '334 <challenge string>'. Data to be sent as response needs to be
|
1428
|
+
* base64 decoded challenge string, MD5 hashed using the password as
|
1429
|
+
* a HMAC key, prefixed by the username and a space, and finally all
|
1430
|
+
* base64 encoded again.
|
1431
|
+
*
|
1432
|
+
* @param {String} str Message from the server
|
1433
|
+
*/
|
1434
|
+
_actionAUTH_CRAM_MD5(str, callback) {
|
1435
|
+
let challengeMatch = str.match(/^334\s+(.+)$/);
|
1436
|
+
let challengeString = '';
|
1437
|
+
|
1438
|
+
if (!challengeMatch) {
|
1439
|
+
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
|
1440
|
+
} else {
|
1441
|
+
challengeString = challengeMatch[1];
|
1442
|
+
}
|
1443
|
+
|
1444
|
+
// Decode from base64
|
1445
|
+
let base64decoded = Buffer.from(challengeString, 'base64').toString('ascii'),
|
1446
|
+
hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
|
1447
|
+
|
1448
|
+
hmacMD5.update(base64decoded);
|
1449
|
+
|
1450
|
+
let prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
|
1451
|
+
|
1452
|
+
this._responseActions.push(str => {
|
1453
|
+
this._actionAUTH_CRAM_MD5_PASS(str, callback);
|
1454
|
+
});
|
1455
|
+
|
1456
|
+
this._sendCommand(
|
1457
|
+
Buffer.from(prepended).toString('base64'),
|
1458
|
+
// hidden hash for logs
|
1459
|
+
Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64')
|
1460
|
+
);
|
1461
|
+
}
|
1462
|
+
|
1463
|
+
/**
|
1464
|
+
* Handles the response to CRAM-MD5 authentication, if there's no error,
|
1465
|
+
* the user can be considered logged in. Start waiting for a message to send
|
1466
|
+
*
|
1467
|
+
* @param {String} str Message from the server
|
1468
|
+
*/
|
1469
|
+
_actionAUTH_CRAM_MD5_PASS(str, callback) {
|
1470
|
+
if (!str.match(/^235\s+/)) {
|
1471
|
+
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
|
1472
|
+
}
|
1473
|
+
|
1474
|
+
this.logger.info(
|
1475
|
+
{
|
1476
|
+
tnx: 'smtp',
|
1477
|
+
username: this._auth.user,
|
1478
|
+
action: 'authenticated',
|
1479
|
+
method: this._authMethod
|
1480
|
+
},
|
1481
|
+
'User %s authenticated',
|
1482
|
+
JSON.stringify(this._auth.user)
|
1483
|
+
);
|
1484
|
+
this.authenticated = true;
|
1485
|
+
callback(null, true);
|
1486
|
+
}
|
1487
|
+
|
1488
|
+
/**
|
1489
|
+
* Handle the response for AUTH LOGIN command. We are expecting
|
1490
|
+
* '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
|
1491
|
+
* response needs to be base64 encoded password.
|
1492
|
+
*
|
1493
|
+
* @param {String} str Message from the server
|
1494
|
+
*/
|
1495
|
+
_actionAUTH_LOGIN_PASS(str, callback) {
|
1496
|
+
if (!/^334[ -]/.test(str)) {
|
1497
|
+
// expecting '334 UGFzc3dvcmQ6'
|
1498
|
+
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
|
1499
|
+
}
|
1500
|
+
|
1501
|
+
this._responseActions.push(str => {
|
1502
|
+
this._actionAUTHComplete(str, callback);
|
1503
|
+
});
|
1504
|
+
|
1505
|
+
this._sendCommand(
|
1506
|
+
Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
|
1507
|
+
// Hidden pass for logs
|
1508
|
+
Buffer.from('/* secret */', 'utf-8').toString('base64')
|
1509
|
+
);
|
1510
|
+
}
|
1511
|
+
|
1512
|
+
/**
|
1513
|
+
* Handles the response for authentication, if there's no error,
|
1514
|
+
* the user can be considered logged in. Start waiting for a message to send
|
1515
|
+
*
|
1516
|
+
* @param {String} str Message from the server
|
1517
|
+
*/
|
1518
|
+
_actionAUTHComplete(str, isRetry, callback) {
|
1519
|
+
if (!callback && typeof isRetry === 'function') {
|
1520
|
+
callback = isRetry;
|
1521
|
+
isRetry = false;
|
1522
|
+
}
|
1523
|
+
|
1524
|
+
if (str.substr(0, 3) === '334') {
|
1525
|
+
this._responseActions.push(str => {
|
1526
|
+
if (isRetry || this._authMethod !== 'XOAUTH2') {
|
1527
|
+
this._actionAUTHComplete(str, true, callback);
|
1528
|
+
} else {
|
1529
|
+
// fetch a new OAuth2 access token
|
1530
|
+
setImmediate(() => this._handleXOauth2Token(true, callback));
|
1531
|
+
}
|
1532
|
+
});
|
1533
|
+
this._sendCommand('');
|
1534
|
+
return;
|
1535
|
+
}
|
1536
|
+
|
1537
|
+
if (str.charAt(0) !== '2') {
|
1538
|
+
this.logger.info(
|
1539
|
+
{
|
1540
|
+
tnx: 'smtp',
|
1541
|
+
username: this._auth.user,
|
1542
|
+
action: 'authfail',
|
1543
|
+
method: this._authMethod
|
1544
|
+
},
|
1545
|
+
'User %s failed to authenticate',
|
1546
|
+
JSON.stringify(this._auth.user)
|
1547
|
+
);
|
1548
|
+
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
|
1549
|
+
}
|
1550
|
+
|
1551
|
+
this.logger.info(
|
1552
|
+
{
|
1553
|
+
tnx: 'smtp',
|
1554
|
+
username: this._auth.user,
|
1555
|
+
action: 'authenticated',
|
1556
|
+
method: this._authMethod
|
1557
|
+
},
|
1558
|
+
'User %s authenticated',
|
1559
|
+
JSON.stringify(this._auth.user)
|
1560
|
+
);
|
1561
|
+
this.authenticated = true;
|
1562
|
+
callback(null, true);
|
1563
|
+
}
|
1564
|
+
|
1565
|
+
/**
|
1566
|
+
* Handle response for a MAIL FROM: command
|
1567
|
+
*
|
1568
|
+
* @param {String} str Message from the server
|
1569
|
+
*/
|
1570
|
+
_actionMAIL(str, callback) {
|
1571
|
+
let message, curRecipient;
|
1572
|
+
if (Number(str.charAt(0)) !== 2) {
|
1573
|
+
if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) {
|
1574
|
+
message = 'Internationalized mailbox name not allowed';
|
1575
|
+
} else {
|
1576
|
+
message = 'Mail command failed';
|
1577
|
+
}
|
1578
|
+
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
|
1579
|
+
}
|
1580
|
+
|
1581
|
+
if (!this._envelope.rcptQueue.length) {
|
1582
|
+
return callback(this._formatError('Can\x27t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
|
1583
|
+
} else {
|
1584
|
+
this._recipientQueue = [];
|
1585
|
+
|
1586
|
+
if (this._supportedExtensions.includes('PIPELINING')) {
|
1587
|
+
while (this._envelope.rcptQueue.length) {
|
1588
|
+
curRecipient = this._envelope.rcptQueue.shift();
|
1589
|
+
this._recipientQueue.push(curRecipient);
|
1590
|
+
this._responseActions.push(str => {
|
1591
|
+
this._actionRCPT(str, callback);
|
1592
|
+
});
|
1593
|
+
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
1594
|
+
}
|
1595
|
+
} else {
|
1596
|
+
curRecipient = this._envelope.rcptQueue.shift();
|
1597
|
+
this._recipientQueue.push(curRecipient);
|
1598
|
+
this._responseActions.push(str => {
|
1599
|
+
this._actionRCPT(str, callback);
|
1600
|
+
});
|
1601
|
+
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
1602
|
+
}
|
1603
|
+
}
|
1604
|
+
}
|
1605
|
+
|
1606
|
+
/**
|
1607
|
+
* Handle response for a RCPT TO: command
|
1608
|
+
*
|
1609
|
+
* @param {String} str Message from the server
|
1610
|
+
*/
|
1611
|
+
_actionRCPT(str, callback) {
|
1612
|
+
let message,
|
1613
|
+
err,
|
1614
|
+
curRecipient = this._recipientQueue.shift();
|
1615
|
+
if (Number(str.charAt(0)) !== 2) {
|
1616
|
+
// this is a soft error
|
1617
|
+
if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) {
|
1618
|
+
message = 'Internationalized mailbox name not allowed';
|
1619
|
+
} else {
|
1620
|
+
message = 'Recipient command failed';
|
1621
|
+
}
|
1622
|
+
this._envelope.rejected.push(curRecipient);
|
1623
|
+
// store error for the failed recipient
|
1624
|
+
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
|
1625
|
+
err.recipient = curRecipient;
|
1626
|
+
this._envelope.rejectedErrors.push(err);
|
1627
|
+
} else {
|
1628
|
+
this._envelope.accepted.push(curRecipient);
|
1629
|
+
}
|
1630
|
+
|
1631
|
+
if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
|
1632
|
+
if (this._envelope.rejected.length < this._envelope.to.length) {
|
1633
|
+
this._responseActions.push(str => {
|
1634
|
+
this._actionDATA(str, callback);
|
1635
|
+
});
|
1636
|
+
this._sendCommand('DATA');
|
1637
|
+
} else {
|
1638
|
+
err = this._formatError('Can\x27t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
|
1639
|
+
err.rejected = this._envelope.rejected;
|
1640
|
+
err.rejectedErrors = this._envelope.rejectedErrors;
|
1641
|
+
return callback(err);
|
1642
|
+
}
|
1643
|
+
} else if (this._envelope.rcptQueue.length) {
|
1644
|
+
curRecipient = this._envelope.rcptQueue.shift();
|
1645
|
+
this._recipientQueue.push(curRecipient);
|
1646
|
+
this._responseActions.push(str => {
|
1647
|
+
this._actionRCPT(str, callback);
|
1648
|
+
});
|
1649
|
+
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
1650
|
+
}
|
1651
|
+
}
|
1652
|
+
|
1653
|
+
/**
|
1654
|
+
* Handle response for a DATA command
|
1655
|
+
*
|
1656
|
+
* @param {String} str Message from the server
|
1657
|
+
*/
|
1658
|
+
_actionDATA(str, callback) {
|
1659
|
+
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
|
1660
|
+
// some servers might use 250 instead, so lets check for 2 or 3 as the first digit
|
1661
|
+
if (!/^[23]/.test(str)) {
|
1662
|
+
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
|
1663
|
+
}
|
1664
|
+
|
1665
|
+
let response = {
|
1666
|
+
accepted: this._envelope.accepted,
|
1667
|
+
rejected: this._envelope.rejected
|
1668
|
+
};
|
1669
|
+
|
1670
|
+
if (this._ehloLines && this._ehloLines.length) {
|
1671
|
+
response.ehlo = this._ehloLines;
|
1672
|
+
}
|
1673
|
+
|
1674
|
+
if (this._envelope.rejectedErrors.length) {
|
1675
|
+
response.rejectedErrors = this._envelope.rejectedErrors;
|
1676
|
+
}
|
1677
|
+
|
1678
|
+
callback(null, response);
|
1679
|
+
}
|
1680
|
+
|
1681
|
+
/**
|
1682
|
+
* Handle response for a DATA stream when using SMTP
|
1683
|
+
* We expect a single response that defines if the sending succeeded or failed
|
1684
|
+
*
|
1685
|
+
* @param {String} str Message from the server
|
1686
|
+
*/
|
1687
|
+
_actionSMTPStream(str, callback) {
|
1688
|
+
if (Number(str.charAt(0)) !== 2) {
|
1689
|
+
// Message failed
|
1690
|
+
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
|
1691
|
+
} else {
|
1692
|
+
// Message sent succesfully
|
1693
|
+
return callback(null, str);
|
1694
|
+
}
|
1695
|
+
}
|
1696
|
+
|
1697
|
+
/**
|
1698
|
+
* Handle response for a DATA stream
|
1699
|
+
* We expect a separate response for every recipient. All recipients can either
|
1700
|
+
* succeed or fail separately
|
1701
|
+
*
|
1702
|
+
* @param {String} recipient The recipient this response applies to
|
1703
|
+
* @param {Boolean} final Is this the final recipient?
|
1704
|
+
* @param {String} str Message from the server
|
1705
|
+
*/
|
1706
|
+
_actionLMTPStream(recipient, final, str, callback) {
|
1707
|
+
let err;
|
1708
|
+
if (Number(str.charAt(0)) !== 2) {
|
1709
|
+
// Message failed
|
1710
|
+
err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
|
1711
|
+
err.recipient = recipient;
|
1712
|
+
this._envelope.rejected.push(recipient);
|
1713
|
+
this._envelope.rejectedErrors.push(err);
|
1714
|
+
for (let i = 0, len = this._envelope.accepted.length; i < len; i++) {
|
1715
|
+
if (this._envelope.accepted[i] === recipient) {
|
1716
|
+
this._envelope.accepted.splice(i, 1);
|
1717
|
+
}
|
1718
|
+
}
|
1719
|
+
}
|
1720
|
+
if (final) {
|
1721
|
+
return callback(null, str);
|
1722
|
+
}
|
1723
|
+
}
|
1724
|
+
|
1725
|
+
_handleXOauth2Token(isRetry, callback) {
|
1726
|
+
this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
|
1727
|
+
if (err) {
|
1728
|
+
this.logger.info(
|
1729
|
+
{
|
1730
|
+
tnx: 'smtp',
|
1731
|
+
username: this._auth.user,
|
1732
|
+
action: 'authfail',
|
1733
|
+
method: this._authMethod
|
1734
|
+
},
|
1735
|
+
'User %s failed to authenticate',
|
1736
|
+
JSON.stringify(this._auth.user)
|
1737
|
+
);
|
1738
|
+
return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
|
1739
|
+
}
|
1740
|
+
this._responseActions.push(str => {
|
1741
|
+
this._actionAUTHComplete(str, isRetry, callback);
|
1742
|
+
});
|
1743
|
+
this._sendCommand(
|
1744
|
+
'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
|
1745
|
+
// Hidden for logs
|
1746
|
+
'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */')
|
1747
|
+
);
|
1748
|
+
});
|
1749
|
+
}
|
1750
|
+
|
1751
|
+
/**
|
1752
|
+
*
|
1753
|
+
* @param {string} command
|
1754
|
+
* @private
|
1755
|
+
*/
|
1756
|
+
_isDestroyedMessage(command) {
|
1757
|
+
if (this._destroyed) {
|
1758
|
+
return 'Cannot ' + command + ' - smtp connection is already destroyed.';
|
1759
|
+
}
|
1760
|
+
|
1761
|
+
if (this._socket) {
|
1762
|
+
if (this._socket.destroyed) {
|
1763
|
+
return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
|
1764
|
+
}
|
1765
|
+
|
1766
|
+
if (!this._socket.writable) {
|
1767
|
+
return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
|
1768
|
+
}
|
1769
|
+
}
|
1770
|
+
}
|
1771
|
+
|
1772
|
+
_getHostname() {
|
1773
|
+
// defaul hostname is machine hostname or [IP]
|
1774
|
+
let defaultHostname;
|
1775
|
+
try {
|
1776
|
+
defaultHostname = os.hostname() || '';
|
1777
|
+
} catch (err) {
|
1778
|
+
// fails on windows 7
|
1779
|
+
defaultHostname = 'localhost';
|
1780
|
+
}
|
1781
|
+
|
1782
|
+
// ignore if not FQDN
|
1783
|
+
if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
|
1784
|
+
defaultHostname = '[127.0.0.1]';
|
1785
|
+
}
|
1786
|
+
|
1787
|
+
// IP should be enclosed in []
|
1788
|
+
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
1789
|
+
defaultHostname = '[' + defaultHostname + ']';
|
1790
|
+
}
|
1791
|
+
|
1792
|
+
return defaultHostname;
|
1793
|
+
}
|
1794
|
+
}
|
1795
|
+
|
1796
|
+
module.exports = SMTPConnection;
|