polikolog 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.idea/5lab.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +10 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/06-02.js +48 -0
- package/06-03.js +22 -0
- package/06-04.js +22 -0
- package/index.html +41 -0
- package/m0603.js +28 -0
- package/mypackage/m0603.js +28 -0
- package/mypackage/node_modules/.package-lock.json +24 -0
- package/mypackage/node_modules/nodemailer/.gitattributes +6 -0
- package/mypackage/node_modules/nodemailer/.prettierrc.js +8 -0
- package/mypackage/node_modules/nodemailer/CHANGELOG.md +725 -0
- package/mypackage/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
- package/mypackage/node_modules/nodemailer/CONTRIBUTING.md +67 -0
- package/mypackage/node_modules/nodemailer/LICENSE +16 -0
- package/mypackage/node_modules/nodemailer/README.md +97 -0
- package/mypackage/node_modules/nodemailer/SECURITY.txt +22 -0
- package/mypackage/node_modules/nodemailer/lib/addressparser/index.js +313 -0
- package/mypackage/node_modules/nodemailer/lib/base64/index.js +142 -0
- package/mypackage/node_modules/nodemailer/lib/dkim/index.js +251 -0
- package/mypackage/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
- package/mypackage/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
- package/mypackage/node_modules/nodemailer/lib/dkim/sign.js +117 -0
- package/mypackage/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
- package/mypackage/node_modules/nodemailer/lib/fetch/index.js +274 -0
- package/mypackage/node_modules/nodemailer/lib/json-transport/index.js +82 -0
- package/mypackage/node_modules/nodemailer/lib/mail-composer/index.js +558 -0
- package/mypackage/node_modules/nodemailer/lib/mailer/index.js +427 -0
- package/mypackage/node_modules/nodemailer/lib/mailer/mail-message.js +315 -0
- package/mypackage/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
- package/mypackage/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2102 -0
- package/mypackage/node_modules/nodemailer/lib/mime-node/index.js +1290 -0
- package/mypackage/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
- package/mypackage/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
- package/mypackage/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
- package/mypackage/node_modules/nodemailer/lib/nodemailer.js +143 -0
- package/mypackage/node_modules/nodemailer/lib/qp/index.js +219 -0
- package/mypackage/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
- package/mypackage/node_modules/nodemailer/lib/ses-transport/index.js +349 -0
- package/mypackage/node_modules/nodemailer/lib/shared/index.js +638 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-connection/index.js +1796 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-pool/index.js +648 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +253 -0
- package/mypackage/node_modules/nodemailer/lib/smtp-transport/index.js +416 -0
- package/mypackage/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
- package/mypackage/node_modules/nodemailer/lib/well-known/index.js +47 -0
- package/mypackage/node_modules/nodemailer/lib/well-known/services.json +286 -0
- package/mypackage/node_modules/nodemailer/lib/xoauth2/index.js +376 -0
- package/mypackage/node_modules/nodemailer/package.json +46 -0
- package/mypackage/node_modules/nodemailer/postinstall.js +101 -0
- package/mypackage/package.json +15 -0
- package/package.json +15 -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;
|