@nocobase/plugin-workflow-mailer 2.2.19 → 2.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/externalVersion.js +6 -6
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
- package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +62 -111
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
- package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/addressparser/index.js +361 -131
- package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +31 -56
- package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +49 -78
- package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/message-parser.js +64 -42
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/sign.js +50 -46
- package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/errors.js +26 -18
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/fetch/index.js +80 -147
- package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/json-transport/index.js +19 -39
- package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +199 -265
- package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +128 -234
- package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +62 -114
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
- package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +259 -372
- package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +6 -11
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -13
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
- package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
- package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +38 -67
- package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/sendmail-transport/index.js +51 -101
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/index.js +156 -272
- package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/objects.js +13 -17
- package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/http-proxy-client.js +49 -74
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/index.js +574 -794
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +172 -284
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +57 -97
- package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-transport/index.js +81 -179
- package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/stream-transport/index.js +36 -74
- package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +5 -16
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
- package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/xoauth2/index.js +125 -187
- package/dist/node_modules/nodemailer/{lib → dist}/well-known/services.json +9 -2
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
- package/dist/node_modules/nodemailer/.gitattributes +0 -6
- package/dist/node_modules/nodemailer/.ncurc.js +0 -9
- package/dist/node_modules/nodemailer/.prettierignore +0 -8
- package/dist/node_modules/nodemailer/.prettierrc +0 -12
- package/dist/node_modules/nodemailer/.prettierrc.js +0 -10
- package/dist/node_modules/nodemailer/.release-please-config.json +0 -9
- package/dist/node_modules/nodemailer/eslint.config.js +0 -88
- package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -276
- package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -671
- package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
- package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -266
- package/dist/node_modules/nodemailer/lib/shared/url.js +0 -151
- package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -105
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const { PassThrough } = require('stream');
|
|
11
|
-
const shared = require('../shared');
|
|
12
|
-
|
|
1
|
+
import * as packageData from '../package-info.js';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import tls from 'node:tls';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import DataStream from './data-stream.js';
|
|
8
|
+
import { PassThrough } from 'node:stream';
|
|
9
|
+
import * as shared from '../shared/index.js';
|
|
13
10
|
// default timeout values in ms
|
|
14
11
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
|
15
12
|
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
|
16
13
|
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
|
|
17
14
|
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
|
18
|
-
const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
|
|
19
|
-
|
|
15
|
+
const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
|
|
16
|
+
// how many bytes a single server response may occupy while it is still being received.
|
|
17
|
+
// Generous compared to any real reply, it only stops a peer that never completes one
|
|
18
|
+
const MAX_RESPONSE_SIZE = 1024 * 1024;
|
|
20
19
|
/**
|
|
21
20
|
* Re-interpret a server response stored in fake 8-bit byte-container form
|
|
22
21
|
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
|
@@ -38,7 +37,27 @@ function decodeServerResponse(str) {
|
|
|
38
37
|
// decoder for invalid bytes, which means we should return the original bytes intact.
|
|
39
38
|
return utf8.includes('\uFFFD') ? str : utf8;
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* True when the last line of a queued reply is a continuation ("250-..."), which means
|
|
42
|
+
* the rest of the reply is still on its way.
|
|
43
|
+
*
|
|
44
|
+
* Called with the byte-container form the queue holds (see _onData): the check only looks
|
|
45
|
+
* at leading ASCII digits and '-' of the last line, and a UTF-8 continuation byte is never
|
|
46
|
+
* 0x0A, so line boundaries and the tested prefix are the same before and after decoding.
|
|
47
|
+
* The last line is read with lastIndexOf rather than split() because a queue entry may hold
|
|
48
|
+
* a whole multiline reply and only its final line matters.
|
|
49
|
+
*/
|
|
50
|
+
function isPartialResponse(str) {
|
|
51
|
+
return isPartialLine(str.slice(str.lastIndexOf('\n') + 1));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True when a single reply line is a continuation ("250-..."). Used where the line is
|
|
55
|
+
* already known to be one line, which skips the scan isPartialResponse needs to find the
|
|
56
|
+
* last line of a whole reply.
|
|
57
|
+
*/
|
|
58
|
+
function isPartialLine(line) {
|
|
59
|
+
return /^\d+-/.test(line);
|
|
60
|
+
}
|
|
42
61
|
/**
|
|
43
62
|
* Generates a SMTP connection object
|
|
44
63
|
*
|
|
@@ -47,13 +66,14 @@ function decodeServerResponse(str) {
|
|
|
47
66
|
* * **port** - is the port to connect to (defaults to 587 or 465)
|
|
48
67
|
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
|
49
68
|
* * **secure** - use SSL
|
|
50
|
-
* * **ignoreTLS** - ignore server support for STARTTLS
|
|
51
|
-
* * **requireTLS** - forces the client to use STARTTLS
|
|
69
|
+
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
|
70
|
+
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
|
52
71
|
* * **name** - the name of the client server
|
|
53
72
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
54
73
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
55
74
|
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
|
56
75
|
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
|
76
|
+
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
|
57
77
|
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
|
58
78
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
|
59
79
|
* * **logger** - bunyan compatible logger interface
|
|
@@ -64,39 +84,34 @@ function decodeServerResponse(str) {
|
|
|
64
84
|
*
|
|
65
85
|
* @constructor
|
|
66
86
|
* @namespace SMTP Client module
|
|
67
|
-
* @param
|
|
87
|
+
* @param [options] Option properties
|
|
68
88
|
*/
|
|
69
89
|
class SMTPConnection extends EventEmitter {
|
|
70
90
|
constructor(options) {
|
|
71
91
|
super(options);
|
|
72
|
-
|
|
73
92
|
this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
|
|
74
93
|
this.stage = 'init';
|
|
75
|
-
|
|
76
94
|
this.options = options || {};
|
|
77
|
-
|
|
95
|
+
if (this.options.requireTLS && (this.options.ignoreTLS || this.options.opportunisticTLS)) {
|
|
96
|
+
// requireTLS wins, a contradictory configuration must not quietly fall back to plaintext.
|
|
97
|
+
// Copied so the caller's (possibly shared) options object is left as it was
|
|
98
|
+
this.options = Object.assign({}, this.options, { ignoreTLS: false, opportunisticTLS: false });
|
|
99
|
+
}
|
|
78
100
|
this.secureConnection = !!this.options.secure;
|
|
79
101
|
this.alreadySecured = !!this.options.secured;
|
|
80
|
-
|
|
81
102
|
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
|
82
103
|
this.host = this.options.host || 'localhost';
|
|
83
|
-
|
|
84
104
|
this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false;
|
|
85
|
-
|
|
86
105
|
this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
|
|
87
|
-
|
|
88
106
|
if (typeof this.options.secure === 'undefined' && this.port === 465) {
|
|
89
107
|
// if secure option is not set but port is 465, then default to secure
|
|
90
108
|
this.secureConnection = true;
|
|
91
109
|
}
|
|
92
|
-
|
|
93
110
|
this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
|
|
94
|
-
|
|
95
111
|
this.logger = shared.getLogger(this.options, {
|
|
96
112
|
component: this.options.component || 'smtp-connection',
|
|
97
113
|
sid: this.id
|
|
98
114
|
});
|
|
99
|
-
|
|
100
115
|
this.customAuth = new Map();
|
|
101
116
|
for (const key of Object.keys(this.options.customAuth || {})) {
|
|
102
117
|
const mapKey = (key || '').toString().trim().toUpperCase();
|
|
@@ -104,201 +119,90 @@ class SMTPConnection extends EventEmitter {
|
|
|
104
119
|
this.customAuth.set(mapKey, this.options.customAuth[key]);
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Expose version nr, just for the reference
|
|
110
|
-
* @type {String}
|
|
111
|
-
*/
|
|
112
|
-
this.version = packageInfo.version;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* If true, then the user is authenticated
|
|
116
|
-
* @type {Boolean}
|
|
117
|
-
*/
|
|
122
|
+
this.version = packageData.version;
|
|
118
123
|
this.authenticated = false;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* If set to true, this instance is no longer active
|
|
122
|
-
* @private
|
|
123
|
-
*/
|
|
124
124
|
this.destroyed = false;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Defines if the current connection is secure or not. If not,
|
|
128
|
-
* STARTTLS can be used if available
|
|
129
|
-
* @private
|
|
130
|
-
*/
|
|
131
125
|
this.secure = !!this.secureConnection;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Store incomplete messages coming from the server
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
126
|
this._remainder = '';
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Unprocessed responses from the server
|
|
141
|
-
* @type {Array}
|
|
142
|
-
*/
|
|
143
127
|
this._responseQueue = [];
|
|
144
|
-
|
|
128
|
+
this._responsePartial = false;
|
|
145
129
|
this.lastServerResponse = false;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The socket connecting to the server
|
|
149
|
-
* @public
|
|
150
|
-
*/
|
|
151
130
|
this._socket = false;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Lists supported auth mechanisms
|
|
155
|
-
* @private
|
|
156
|
-
*/
|
|
157
131
|
this._supportedAuth = [];
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Set to true, if EHLO response includes "AUTH".
|
|
161
|
-
* If false then authentication is not tried
|
|
162
|
-
*/
|
|
163
132
|
this.allowsAuth = false;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Includes current envelope (from, to)
|
|
167
|
-
* @private
|
|
168
|
-
*/
|
|
169
133
|
this._envelope = false;
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Lists supported extensions
|
|
173
|
-
* @private
|
|
174
|
-
*/
|
|
175
134
|
this._supportedExtensions = [];
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Defines the maximum allowed size for a single message
|
|
179
|
-
* @private
|
|
180
|
-
*/
|
|
181
135
|
this._maxAllowedSize = 0;
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Function queue to run if a data chunk comes from the server
|
|
185
|
-
* @private
|
|
186
|
-
*/
|
|
187
136
|
this._responseActions = [];
|
|
188
137
|
this._recipientQueue = [];
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Timeout variable for waiting the greeting
|
|
192
|
-
* @private
|
|
193
|
-
*/
|
|
194
138
|
this._greetingTimeout = false;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Timeout variable for waiting the connection to start
|
|
198
|
-
* @private
|
|
199
|
-
*/
|
|
200
139
|
this._connectionTimeout = false;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* If the socket is deemed already closed
|
|
204
|
-
* @private
|
|
205
|
-
*/
|
|
206
140
|
this._destroyed = false;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* If the socket is already being closed
|
|
210
|
-
* @private
|
|
211
|
-
*/
|
|
212
141
|
this._closing = false;
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Message DATA stream currently piped to the socket, if any. Tracked so
|
|
216
|
-
* close() can unpipe it before tearing the socket down.
|
|
217
|
-
* @private
|
|
218
|
-
*/
|
|
219
142
|
this._currentDataStream = false;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* Callbacks for socket's listeners
|
|
223
|
-
*/
|
|
143
|
+
this._pendingSend = false;
|
|
144
|
+
this._connectCallback = false;
|
|
224
145
|
this._onSocketData = chunk => this._onData(chunk);
|
|
225
146
|
this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
|
|
226
147
|
this._onSocketClose = () => this._onClose();
|
|
227
148
|
this._onSocketEnd = () => this._onEnd();
|
|
228
149
|
this._onSocketTimeout = () => this._onTimeout();
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Connection-phase error handler (supports fallback to alternative addresses)
|
|
232
|
-
*/
|
|
233
150
|
this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Connection attempt counter for fallback race condition protection
|
|
237
|
-
* @private
|
|
238
|
-
*/
|
|
239
151
|
this._connectionAttemptId = 0;
|
|
240
152
|
}
|
|
241
|
-
|
|
242
153
|
/**
|
|
243
154
|
* Creates a connection to a SMTP server and sets up connection
|
|
244
155
|
* listener
|
|
245
156
|
*/
|
|
246
157
|
connect(connectCallback) {
|
|
247
158
|
if (typeof connectCallback === 'function') {
|
|
159
|
+
this._connectCallback = connectCallback;
|
|
248
160
|
this.once('connect', () => {
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
'SMTP handshake finished'
|
|
254
|
-
);
|
|
161
|
+
this._connectCallback = false;
|
|
162
|
+
this.logger.debug({
|
|
163
|
+
tnx: 'smtp'
|
|
164
|
+
}, 'SMTP handshake finished');
|
|
255
165
|
connectCallback();
|
|
256
166
|
});
|
|
257
|
-
|
|
258
167
|
const isDestroyedMessage = this._isDestroyedMessage('connect');
|
|
259
168
|
if (isDestroyedMessage) {
|
|
260
169
|
return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
|
|
261
170
|
}
|
|
262
171
|
}
|
|
263
|
-
|
|
264
172
|
let opts = {
|
|
265
173
|
port: this.port,
|
|
266
174
|
host: this.host,
|
|
267
175
|
allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
|
|
268
176
|
timeout: this.options.dnsTimeout || DNS_TIMEOUT
|
|
269
177
|
};
|
|
270
|
-
|
|
271
178
|
if (this.options.localAddress) {
|
|
272
179
|
opts.localAddress = this.options.localAddress;
|
|
273
180
|
}
|
|
274
|
-
|
|
275
181
|
if (this.options.connection) {
|
|
276
182
|
// connection is already opened
|
|
277
183
|
this._socket = this.options.connection;
|
|
278
184
|
this._setupConnectionHandlers();
|
|
279
|
-
|
|
280
185
|
if (this.secureConnection && !this.alreadySecured) {
|
|
281
|
-
setImmediate(() =>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
} else {
|
|
186
|
+
setImmediate(() => this._upgradeConnection(err => {
|
|
187
|
+
if (err) {
|
|
188
|
+
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this._onConnect();
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
291
195
|
setImmediate(() => this._onConnect());
|
|
292
196
|
}
|
|
293
197
|
return;
|
|
294
|
-
}
|
|
198
|
+
}
|
|
199
|
+
else if (this.options.socket) {
|
|
295
200
|
// socket object is set up but not yet connected
|
|
296
201
|
this._socket = this.options.socket;
|
|
297
202
|
return this._resolveAndConnect(opts, _resolved => {
|
|
298
203
|
try {
|
|
299
204
|
this._socket.connect(this.port, this.host, () => {
|
|
300
205
|
this._socket.setKeepAlive(true);
|
|
301
|
-
|
|
302
206
|
// a `secure` connection over a caller-provided socket must still
|
|
303
207
|
// perform the TLS handshake, otherwise AUTH and the message body
|
|
304
208
|
// would be sent in cleartext despite the caller requesting TLS
|
|
@@ -311,58 +215,51 @@ class SMTPConnection extends EventEmitter {
|
|
|
311
215
|
this._onConnect();
|
|
312
216
|
});
|
|
313
217
|
}
|
|
314
|
-
|
|
315
218
|
this._onConnect();
|
|
316
219
|
});
|
|
317
220
|
this._setupConnectionHandlers();
|
|
318
|
-
}
|
|
319
|
-
|
|
221
|
+
}
|
|
222
|
+
catch (E) {
|
|
223
|
+
setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
224
|
+
return;
|
|
320
225
|
}
|
|
321
226
|
});
|
|
322
|
-
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
323
229
|
if (this.secureConnection) {
|
|
324
230
|
Object.assign(opts, this.options.tls || {});
|
|
325
|
-
|
|
326
231
|
// ensure servername for SNI
|
|
327
232
|
if (this.servername && !opts.servername) {
|
|
328
233
|
opts.servername = this.servername;
|
|
329
234
|
}
|
|
330
235
|
}
|
|
331
|
-
|
|
332
236
|
return this._resolveAndConnect(opts, resolved => {
|
|
333
237
|
// Store fallback addresses for retry on connection failure
|
|
334
238
|
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
|
335
239
|
this._connectOpts = Object.assign({}, opts);
|
|
336
|
-
|
|
337
240
|
this._connectToHost(opts, this.secureConnection);
|
|
338
241
|
});
|
|
339
242
|
}
|
|
340
243
|
}
|
|
341
|
-
|
|
342
244
|
/**
|
|
343
245
|
* Resolves the hostname and applies resolved values to opts,
|
|
344
246
|
* then calls the provided callback with the resolved data
|
|
345
247
|
*
|
|
346
|
-
* @param
|
|
347
|
-
* @param
|
|
248
|
+
* @param opts Connection options (modified in place)
|
|
249
|
+
* @param callback Called with resolved data on success
|
|
250
|
+
* @internal
|
|
348
251
|
*/
|
|
349
252
|
_resolveAndConnect(opts, callback) {
|
|
350
253
|
return shared.resolveHostname(opts, (err, resolved) => {
|
|
351
254
|
if (err) {
|
|
352
255
|
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
|
353
256
|
}
|
|
354
|
-
this.logger.debug(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
},
|
|
361
|
-
'Resolved %s as %s [cache %s]',
|
|
362
|
-
opts.host,
|
|
363
|
-
resolved.host,
|
|
364
|
-
resolved.cached ? 'hit' : 'miss'
|
|
365
|
-
);
|
|
257
|
+
this.logger.debug({
|
|
258
|
+
tnx: 'dns',
|
|
259
|
+
source: opts.host,
|
|
260
|
+
resolved: resolved.host,
|
|
261
|
+
cached: !!resolved.cached
|
|
262
|
+
}, 'Resolved %s as %s [cache %s]', opts.host, resolved.host, resolved.cached ? 'hit' : 'miss');
|
|
366
263
|
for (const key of Object.keys(resolved)) {
|
|
367
264
|
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
368
265
|
opts[key] = resolved[key];
|
|
@@ -371,26 +268,26 @@ class SMTPConnection extends EventEmitter {
|
|
|
371
268
|
callback(resolved);
|
|
372
269
|
});
|
|
373
270
|
}
|
|
374
|
-
|
|
375
271
|
/**
|
|
376
272
|
* Attempts to connect to the specified host address
|
|
377
273
|
*
|
|
378
|
-
* @param
|
|
379
|
-
* @param
|
|
274
|
+
* @param opts Connection options
|
|
275
|
+
* @param secure Whether to use TLS
|
|
276
|
+
* @internal
|
|
380
277
|
*/
|
|
381
278
|
_connectToHost(opts, secure) {
|
|
382
279
|
// If the client was closed while DNS resolution was in flight, do not open
|
|
383
280
|
// a socket here: close() ran with this._socket still unset and so had
|
|
384
281
|
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
|
385
|
-
// _closing is set
|
|
282
|
+
// _closing is set, the freshly connected socket would leak.
|
|
386
283
|
if (this._destroyed || this._closing) {
|
|
387
284
|
return;
|
|
388
285
|
}
|
|
389
|
-
|
|
390
286
|
this._connectionAttemptId++;
|
|
391
287
|
const currentAttemptId = this._connectionAttemptId;
|
|
392
|
-
|
|
393
|
-
|
|
288
|
+
const connectFn = secure
|
|
289
|
+
? tls.connect
|
|
290
|
+
: net.connect;
|
|
394
291
|
try {
|
|
395
292
|
this._socket = connectFn(opts, () => {
|
|
396
293
|
// Ignore callback if this is a stale connection attempt
|
|
@@ -401,54 +298,45 @@ class SMTPConnection extends EventEmitter {
|
|
|
401
298
|
this._onConnect();
|
|
402
299
|
});
|
|
403
300
|
this._setupConnectionHandlers();
|
|
404
|
-
}
|
|
405
|
-
|
|
301
|
+
}
|
|
302
|
+
catch (E) {
|
|
303
|
+
setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
304
|
+
return;
|
|
406
305
|
}
|
|
407
306
|
}
|
|
408
|
-
|
|
409
307
|
/**
|
|
410
308
|
* Sets up connection timeout and error handlers
|
|
309
|
+
* @internal
|
|
411
310
|
*/
|
|
412
311
|
_setupConnectionHandlers() {
|
|
413
312
|
this._connectionTimeout = setTimeout(() => {
|
|
414
313
|
this._onConnectionError('Connection timeout', 'ETIMEDOUT');
|
|
415
314
|
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
416
|
-
|
|
417
315
|
this._socket.on('error', this._onConnectionSocketError);
|
|
418
316
|
}
|
|
419
|
-
|
|
420
317
|
/**
|
|
421
318
|
* Handles connection errors with fallback to alternative addresses
|
|
422
319
|
*
|
|
423
|
-
* @param
|
|
424
|
-
* @param
|
|
320
|
+
* @param err Error object or message
|
|
321
|
+
* @param code Error code
|
|
322
|
+
* @internal
|
|
425
323
|
*/
|
|
426
324
|
_onConnectionError(err, code) {
|
|
427
325
|
clearTimeout(this._connectionTimeout);
|
|
428
|
-
|
|
429
326
|
// Check if we have fallback addresses to try
|
|
430
327
|
const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
|
|
431
|
-
|
|
432
328
|
if (!canFallback) {
|
|
433
329
|
// No more fallback addresses, report the error
|
|
434
330
|
this._onError(err, code, false, 'CONN');
|
|
435
331
|
return;
|
|
436
332
|
}
|
|
437
|
-
|
|
438
333
|
const nextHost = this._fallbackAddresses.shift();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
error: err.message || err
|
|
446
|
-
},
|
|
447
|
-
'Connection to %s failed, trying %s',
|
|
448
|
-
this._connectOpts.host,
|
|
449
|
-
nextHost
|
|
450
|
-
);
|
|
451
|
-
|
|
334
|
+
this.logger.info({
|
|
335
|
+
tnx: 'network',
|
|
336
|
+
failedHost: this._connectOpts.host,
|
|
337
|
+
nextHost,
|
|
338
|
+
error: err.message || err
|
|
339
|
+
}, 'Connection to %s failed, trying %s', this._connectOpts.host, nextHost);
|
|
452
340
|
// Clean up current socket
|
|
453
341
|
if (this._socket) {
|
|
454
342
|
try {
|
|
@@ -457,17 +345,16 @@ class SMTPConnection extends EventEmitter {
|
|
|
457
345
|
// after destroy), mirroring the guard used in close()
|
|
458
346
|
this._socket.on('error', TEARDOWN_NOOP);
|
|
459
347
|
this._socket.destroy();
|
|
460
|
-
}
|
|
348
|
+
}
|
|
349
|
+
catch (_E) {
|
|
461
350
|
// ignore
|
|
462
351
|
}
|
|
463
352
|
this._socket = null;
|
|
464
353
|
}
|
|
465
|
-
|
|
466
354
|
// Update host and retry
|
|
467
355
|
this._connectOpts.host = nextHost;
|
|
468
356
|
this._connectToHost(this._connectOpts, this.secureConnection);
|
|
469
357
|
}
|
|
470
|
-
|
|
471
358
|
/**
|
|
472
359
|
* Sends QUIT
|
|
473
360
|
*/
|
|
@@ -475,7 +362,6 @@ class SMTPConnection extends EventEmitter {
|
|
|
475
362
|
this._sendCommand('QUIT');
|
|
476
363
|
this._responseActions.push(this.close);
|
|
477
364
|
}
|
|
478
|
-
|
|
479
365
|
/**
|
|
480
366
|
* Closes the connection to the server
|
|
481
367
|
*/
|
|
@@ -483,36 +369,37 @@ class SMTPConnection extends EventEmitter {
|
|
|
483
369
|
clearTimeout(this._connectionTimeout);
|
|
484
370
|
clearTimeout(this._greetingTimeout);
|
|
485
371
|
this._responseActions = [];
|
|
486
|
-
|
|
487
372
|
// allow to run this function only once
|
|
488
373
|
if (this._closing) {
|
|
489
374
|
return;
|
|
490
375
|
}
|
|
491
376
|
this._closing = true;
|
|
492
|
-
|
|
493
377
|
const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
tnx: 'smtp'
|
|
498
|
-
},
|
|
499
|
-
'Closing connection to the server using "%s"',
|
|
500
|
-
closeMethod
|
|
501
|
-
);
|
|
502
|
-
|
|
378
|
+
this.logger.debug({
|
|
379
|
+
tnx: 'smtp'
|
|
380
|
+
}, 'Closing connection to the server using "%s"', closeMethod);
|
|
503
381
|
const socket = (this._socket && this._socket.socket) || this._socket;
|
|
504
|
-
|
|
505
382
|
// Detach any in-flight DATA stream from the socket so the source stream
|
|
506
383
|
// can be garbage-collected once the socket is gone.
|
|
507
384
|
if (this._currentDataStream) {
|
|
508
385
|
try {
|
|
509
386
|
this._currentDataStream.unpipe(this._socket);
|
|
510
|
-
}
|
|
387
|
+
}
|
|
388
|
+
catch (_E) {
|
|
511
389
|
// ignore
|
|
512
390
|
}
|
|
513
391
|
this._currentDataStream = false;
|
|
514
392
|
}
|
|
515
|
-
|
|
393
|
+
// Detach from the message stream as well. The listener is swapped for a no-op rather than
|
|
394
|
+
// removed, a stream destroyed with an error later on would otherwise throw it as unhandled
|
|
395
|
+
if (this._pendingSend) {
|
|
396
|
+
const { stream, onStreamError } = this._pendingSend;
|
|
397
|
+
if (stream) {
|
|
398
|
+
stream.removeListener('error', onStreamError);
|
|
399
|
+
stream.on('error', TEARDOWN_NOOP);
|
|
400
|
+
}
|
|
401
|
+
this._pendingSend = false;
|
|
402
|
+
}
|
|
516
403
|
if (socket && !socket.destroyed) {
|
|
517
404
|
try {
|
|
518
405
|
// Clear socket timeout to prevent timer leaks
|
|
@@ -528,14 +415,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
528
415
|
// sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
|
|
529
416
|
socket.on('error', TEARDOWN_NOOP);
|
|
530
417
|
socket[closeMethod]();
|
|
531
|
-
}
|
|
418
|
+
}
|
|
419
|
+
catch (_E) {
|
|
532
420
|
// just ignore
|
|
533
421
|
}
|
|
534
422
|
}
|
|
535
|
-
|
|
536
423
|
this._destroy();
|
|
537
424
|
}
|
|
538
|
-
|
|
539
425
|
/**
|
|
540
426
|
* Authenticate user
|
|
541
427
|
*/
|
|
@@ -544,116 +430,108 @@ class SMTPConnection extends EventEmitter {
|
|
|
544
430
|
if (isDestroyedMessage) {
|
|
545
431
|
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
546
432
|
}
|
|
547
|
-
|
|
548
433
|
this._auth = authData || {};
|
|
549
434
|
// Select SASL authentication method
|
|
550
435
|
this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
|
|
551
|
-
|
|
436
|
+
// XOAUTH2 needs a token generator or a custom handler, without either the method
|
|
437
|
+
// can not be run even when it is the only one the server advertised
|
|
438
|
+
const canUseXOAuth2 = !!this._auth.oauth2 || this.customAuth.has('XOAUTH2');
|
|
552
439
|
if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
|
|
553
440
|
this._authMethod = 'XOAUTH2';
|
|
554
|
-
} else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
|
|
555
|
-
// use first supported
|
|
556
|
-
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
|
|
557
441
|
}
|
|
558
|
-
|
|
559
|
-
|
|
442
|
+
else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
|
|
443
|
+
// use the first supported method that can be run
|
|
444
|
+
const supported = this._supportedAuth.find(method => method !== 'XOAUTH2' || canUseXOAuth2);
|
|
445
|
+
this._authMethod = (supported || 'PLAIN').toUpperCase().trim();
|
|
446
|
+
}
|
|
447
|
+
// a token login needs no credentials, every other method and every custom handler
|
|
448
|
+
// gets them filled in from the user and pass values
|
|
449
|
+
if ((this._authMethod !== 'XOAUTH2' || this.customAuth.has('XOAUTH2')) &&
|
|
450
|
+
(!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) {
|
|
560
451
|
if ((this._auth.user && this._auth.pass) || this.customAuth.has(this._authMethod)) {
|
|
561
452
|
this._auth.credentials = {
|
|
562
453
|
user: this._auth.user,
|
|
563
454
|
pass: this._auth.pass,
|
|
564
455
|
options: this._auth.options
|
|
565
456
|
};
|
|
566
|
-
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
567
459
|
return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
|
|
568
460
|
}
|
|
569
461
|
}
|
|
570
|
-
|
|
571
462
|
if (this.customAuth.has(this._authMethod)) {
|
|
572
463
|
const handler = this.customAuth.get(this._authMethod);
|
|
573
464
|
let lastResponse;
|
|
574
465
|
let returned = false;
|
|
575
|
-
|
|
576
466
|
const resolve = () => {
|
|
577
467
|
if (returned) {
|
|
578
468
|
return;
|
|
579
469
|
}
|
|
580
470
|
returned = true;
|
|
581
|
-
this.logger.info(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
},
|
|
588
|
-
'User %s authenticated',
|
|
589
|
-
JSON.stringify(this._auth.user)
|
|
590
|
-
);
|
|
471
|
+
this.logger.info({
|
|
472
|
+
tnx: 'smtp',
|
|
473
|
+
username: this._auth.user,
|
|
474
|
+
action: 'authenticated',
|
|
475
|
+
method: this._authMethod
|
|
476
|
+
}, 'User %s authenticated', JSON.stringify(this._auth.user));
|
|
591
477
|
this.authenticated = true;
|
|
592
478
|
callback(null, true);
|
|
593
479
|
};
|
|
594
|
-
|
|
595
|
-
const reject = err => {
|
|
480
|
+
const reject = (err) => {
|
|
596
481
|
if (returned) {
|
|
597
482
|
return;
|
|
598
483
|
}
|
|
599
484
|
returned = true;
|
|
600
485
|
callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
|
|
601
486
|
};
|
|
602
|
-
|
|
487
|
+
// one implementation serves both sendCommand overloads, the promise is returned
|
|
488
|
+
// exactly when no callback was given
|
|
489
|
+
const sendCommand = (cmd, done) => {
|
|
490
|
+
let promise;
|
|
491
|
+
if (!done) {
|
|
492
|
+
promise = new Promise((resolve, reject) => {
|
|
493
|
+
done = shared.callbackPromise(resolve, reject);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
this._responseActions.push(str => {
|
|
497
|
+
lastResponse = str;
|
|
498
|
+
let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
|
|
499
|
+
let data = {
|
|
500
|
+
command: cmd,
|
|
501
|
+
response: str
|
|
502
|
+
};
|
|
503
|
+
if (codes) {
|
|
504
|
+
data.status = Number(codes[1]) || 0;
|
|
505
|
+
if (codes[2]) {
|
|
506
|
+
data.code = codes[2];
|
|
507
|
+
}
|
|
508
|
+
data.text = str.substr(codes[0].length);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
data.text = str;
|
|
512
|
+
data.status = 0; // just in case we need to perform numeric comparisons
|
|
513
|
+
}
|
|
514
|
+
done(null, data);
|
|
515
|
+
});
|
|
516
|
+
setImmediate(() => this._sendCommand(cmd));
|
|
517
|
+
return promise;
|
|
518
|
+
};
|
|
603
519
|
const handlerResponse = handler({
|
|
604
520
|
auth: this._auth,
|
|
605
521
|
method: this._authMethod,
|
|
606
|
-
|
|
607
522
|
extensions: [].concat(this._supportedExtensions),
|
|
608
523
|
authMethods: [].concat(this._supportedAuth),
|
|
609
524
|
maxAllowedSize: this._maxAllowedSize || false,
|
|
610
|
-
|
|
611
|
-
sendCommand: (cmd, done) => {
|
|
612
|
-
let promise;
|
|
613
|
-
|
|
614
|
-
if (!done) {
|
|
615
|
-
promise = new Promise((resolve, reject) => {
|
|
616
|
-
done = shared.callbackPromise(resolve, reject);
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
this._responseActions.push(str => {
|
|
621
|
-
lastResponse = str;
|
|
622
|
-
|
|
623
|
-
let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
|
|
624
|
-
let data = {
|
|
625
|
-
command: cmd,
|
|
626
|
-
response: str
|
|
627
|
-
};
|
|
628
|
-
if (codes) {
|
|
629
|
-
data.status = Number(codes[1]) || 0;
|
|
630
|
-
if (codes[2]) {
|
|
631
|
-
data.code = codes[2];
|
|
632
|
-
}
|
|
633
|
-
data.text = str.substr(codes[0].length);
|
|
634
|
-
} else {
|
|
635
|
-
data.text = str;
|
|
636
|
-
data.status = 0; // just in case we need to perform numeric comparisons
|
|
637
|
-
}
|
|
638
|
-
done(null, data);
|
|
639
|
-
});
|
|
640
|
-
setImmediate(() => this._sendCommand(cmd));
|
|
641
|
-
|
|
642
|
-
return promise;
|
|
643
|
-
},
|
|
644
|
-
|
|
525
|
+
sendCommand: sendCommand,
|
|
645
526
|
resolve,
|
|
646
527
|
reject
|
|
647
528
|
});
|
|
648
|
-
|
|
649
529
|
if (handlerResponse && typeof handlerResponse.catch === 'function') {
|
|
650
530
|
// a promise was returned
|
|
651
531
|
handlerResponse.then(resolve).catch(reject);
|
|
652
532
|
}
|
|
653
|
-
|
|
654
533
|
return;
|
|
655
534
|
}
|
|
656
|
-
|
|
657
535
|
switch (this._authMethod) {
|
|
658
536
|
case 'XOAUTH2':
|
|
659
537
|
this._handleXOauth2Token(false, callback);
|
|
@@ -668,27 +546,21 @@ class SMTPConnection extends EventEmitter {
|
|
|
668
546
|
this._responseActions.push(str => {
|
|
669
547
|
this._actionAUTHComplete(str, callback);
|
|
670
548
|
});
|
|
671
|
-
this._sendCommand(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
'
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
this._auth.credentials.user +
|
|
687
|
-
'\u0000' +
|
|
688
|
-
'/* secret */',
|
|
689
|
-
'utf-8'
|
|
690
|
-
).toString('base64')
|
|
691
|
-
);
|
|
549
|
+
this._sendCommand('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, 'utf-8').toString('base64'),
|
|
556
|
+
// log entry without passwords
|
|
557
|
+
'AUTH PLAIN ' +
|
|
558
|
+
Buffer.from(
|
|
559
|
+
//this._auth.user+'\u0000'+
|
|
560
|
+
'\u0000' + // skip authorization identity as it causes problems with some servers
|
|
561
|
+
this._auth.credentials.user +
|
|
562
|
+
'\u0000' +
|
|
563
|
+
'/* secret */', 'utf-8').toString('base64'));
|
|
692
564
|
return;
|
|
693
565
|
case 'CRAM-MD5':
|
|
694
566
|
this._responseActions.push(str => {
|
|
@@ -697,49 +569,53 @@ class SMTPConnection extends EventEmitter {
|
|
|
697
569
|
this._sendCommand('AUTH CRAM-MD5');
|
|
698
570
|
return;
|
|
699
571
|
}
|
|
700
|
-
|
|
701
572
|
return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
|
|
702
573
|
}
|
|
703
|
-
|
|
704
574
|
/**
|
|
705
575
|
* Sends a message
|
|
706
576
|
*
|
|
707
|
-
* @param
|
|
708
|
-
* @param
|
|
709
|
-
* @param
|
|
577
|
+
* @param envelope Envelope object, {from: addr, to: [addr]}
|
|
578
|
+
* @param message String, Buffer or a Stream
|
|
579
|
+
* @param done Callback to return once sending is completed
|
|
710
580
|
*/
|
|
711
581
|
send(envelope, message, done) {
|
|
582
|
+
// ensure that the callback is only called once. The public callback type has a
|
|
583
|
+
// required result, the error paths hand over the error alone
|
|
584
|
+
let returned = false;
|
|
585
|
+
const callback = (err, info) => {
|
|
586
|
+
if (returned) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
returned = true;
|
|
590
|
+
if (this._pendingSend && this._pendingSend.callback === callback) {
|
|
591
|
+
this._pendingSend = false;
|
|
592
|
+
}
|
|
593
|
+
done(err, info);
|
|
594
|
+
};
|
|
712
595
|
if (!message) {
|
|
713
|
-
return
|
|
596
|
+
return callback(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
|
|
714
597
|
}
|
|
715
|
-
|
|
716
598
|
const isDestroyedMessage = this._isDestroyedMessage('send message');
|
|
717
599
|
if (isDestroyedMessage) {
|
|
718
|
-
return
|
|
600
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
719
601
|
}
|
|
720
|
-
|
|
721
602
|
// reject larger messages than allowed
|
|
722
603
|
if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
|
|
723
|
-
|
|
724
|
-
|
|
604
|
+
setImmediate(() => {
|
|
605
|
+
callback(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
|
|
725
606
|
});
|
|
607
|
+
return;
|
|
726
608
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (returned) {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
returned = true;
|
|
735
|
-
|
|
736
|
-
done(...arguments);
|
|
609
|
+
const pendingSend = {
|
|
610
|
+
callback,
|
|
611
|
+
stream: false,
|
|
612
|
+
onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
|
|
737
613
|
};
|
|
738
|
-
|
|
739
614
|
if (typeof message.on === 'function') {
|
|
740
|
-
|
|
615
|
+
pendingSend.stream = message;
|
|
616
|
+
pendingSend.stream.on('error', pendingSend.onStreamError);
|
|
741
617
|
}
|
|
742
|
-
|
|
618
|
+
this._pendingSend = pendingSend;
|
|
743
619
|
const startTime = Date.now();
|
|
744
620
|
this._setEnvelope(envelope, (err, info) => {
|
|
745
621
|
if (err) {
|
|
@@ -747,11 +623,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
747
623
|
const stream = new PassThrough();
|
|
748
624
|
if (typeof message.pipe === 'function') {
|
|
749
625
|
message.pipe(stream);
|
|
750
|
-
}
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
751
628
|
stream.write(message);
|
|
752
629
|
stream.end();
|
|
753
630
|
}
|
|
754
|
-
|
|
755
631
|
return callback(err);
|
|
756
632
|
}
|
|
757
633
|
const envelopeTime = Date.now();
|
|
@@ -759,34 +635,33 @@ class SMTPConnection extends EventEmitter {
|
|
|
759
635
|
if (err) {
|
|
760
636
|
return callback(err);
|
|
761
637
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return callback(null,
|
|
638
|
+
// the envelope info becomes the send result once the timings are on it
|
|
639
|
+
const result = info;
|
|
640
|
+
result.envelopeTime = envelopeTime - startTime;
|
|
641
|
+
result.messageTime = Date.now() - envelopeTime;
|
|
642
|
+
result.messageSize = stream.outByteCount;
|
|
643
|
+
result.response = str;
|
|
644
|
+
return callback(null, result);
|
|
769
645
|
});
|
|
770
646
|
if (typeof message.pipe === 'function') {
|
|
771
647
|
message.pipe(stream);
|
|
772
|
-
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
773
650
|
stream.write(message);
|
|
774
651
|
stream.end();
|
|
775
652
|
}
|
|
776
653
|
});
|
|
777
654
|
}
|
|
778
|
-
|
|
779
655
|
/**
|
|
780
656
|
* Resets connection state
|
|
781
657
|
*
|
|
782
|
-
* @param
|
|
658
|
+
* @param callback Callback to return once connection is reset
|
|
783
659
|
*/
|
|
784
660
|
reset(callback) {
|
|
785
661
|
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
|
786
662
|
if (isDestroyedMessage) {
|
|
787
663
|
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
788
664
|
}
|
|
789
|
-
|
|
790
665
|
this._sendCommand('RSET');
|
|
791
666
|
this._responseActions.push(str => {
|
|
792
667
|
if (str.charAt(0) !== '2') {
|
|
@@ -796,231 +671,245 @@ class SMTPConnection extends EventEmitter {
|
|
|
796
671
|
return callback(null, true);
|
|
797
672
|
});
|
|
798
673
|
}
|
|
799
|
-
|
|
800
674
|
/**
|
|
801
675
|
* Connection listener that is run when the connection to
|
|
802
676
|
* the server is opened
|
|
803
677
|
*
|
|
804
678
|
* @event
|
|
679
|
+
* @internal
|
|
805
680
|
*/
|
|
806
681
|
_onConnect() {
|
|
682
|
+
const socket = this._socket;
|
|
807
683
|
clearTimeout(this._connectionTimeout);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
remotePort: this._socket.remotePort
|
|
816
|
-
},
|
|
817
|
-
'%s established to %s:%s',
|
|
818
|
-
this.secure ? 'Secure connection' : 'Connection',
|
|
819
|
-
this._socket.remoteAddress,
|
|
820
|
-
this._socket.remotePort
|
|
821
|
-
);
|
|
822
|
-
|
|
684
|
+
this.logger.info({
|
|
685
|
+
tnx: 'network',
|
|
686
|
+
localAddress: socket.localAddress,
|
|
687
|
+
localPort: socket.localPort,
|
|
688
|
+
remoteAddress: socket.remoteAddress,
|
|
689
|
+
remotePort: socket.remotePort
|
|
690
|
+
}, '%s established to %s:%s', this.secure ? 'Secure connection' : 'Connection', socket.remoteAddress, socket.remotePort);
|
|
823
691
|
if (this._destroyed) {
|
|
824
692
|
// Connection was established after we already had canceled it
|
|
825
693
|
this.close();
|
|
826
694
|
return;
|
|
827
695
|
}
|
|
828
|
-
|
|
829
696
|
this.stage = 'connected';
|
|
830
|
-
|
|
831
697
|
// clear existing listeners for the socket
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
698
|
+
socket.removeListener('data', this._onSocketData);
|
|
699
|
+
socket.removeListener('timeout', this._onSocketTimeout);
|
|
700
|
+
socket.removeListener('close', this._onSocketClose);
|
|
701
|
+
socket.removeListener('end', this._onSocketEnd);
|
|
836
702
|
// Switch from connection-phase error handler to normal error handler
|
|
837
|
-
|
|
703
|
+
socket.removeListener('error', this._onConnectionSocketError);
|
|
838
704
|
// _upgradeConnection (options.connection + secure) may already have attached
|
|
839
705
|
// the normal handler; remove it first so we never end up with a duplicate
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.
|
|
846
|
-
|
|
847
|
-
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
|
|
848
|
-
this._socket.on('timeout', this._onSocketTimeout);
|
|
849
|
-
|
|
706
|
+
socket.removeListener('error', this._onSocketError);
|
|
707
|
+
socket.on('error', this._onSocketError);
|
|
708
|
+
socket.on('data', this._onSocketData);
|
|
709
|
+
socket.once('close', this._onSocketClose);
|
|
710
|
+
socket.once('end', this._onSocketEnd);
|
|
711
|
+
socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
|
|
712
|
+
socket.on('timeout', this._onSocketTimeout);
|
|
850
713
|
this._greetingTimeout = setTimeout(() => {
|
|
851
714
|
// if still waiting for greeting, give up
|
|
852
715
|
if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
|
|
853
716
|
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
|
|
854
717
|
}
|
|
855
718
|
}, this.options.greetingTimeout || GREETING_TIMEOUT);
|
|
856
|
-
|
|
857
719
|
this._responseActions.push(this._actionGreeting);
|
|
858
|
-
|
|
859
720
|
// we have a 'data' listener set up so resume socket if it was paused
|
|
860
|
-
|
|
721
|
+
socket.resume();
|
|
861
722
|
}
|
|
862
|
-
|
|
863
723
|
/**
|
|
864
724
|
* 'data' listener for data coming from the server
|
|
865
725
|
*
|
|
866
726
|
* @event
|
|
867
|
-
* @param
|
|
727
|
+
* @param chunk Data chunk coming from the server
|
|
728
|
+
* @internal
|
|
868
729
|
*/
|
|
869
730
|
_onData(chunk) {
|
|
870
731
|
if (this._destroyed || !chunk || !chunk.length) {
|
|
871
732
|
return;
|
|
872
733
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
734
|
+
const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
|
|
735
|
+
const data = chunk.toString('binary');
|
|
736
|
+
// A chunk without a line break only extends the line currently being received, so
|
|
737
|
+
// keep it in the remainder and leave that string unflattened. Splitting the whole
|
|
738
|
+
// remainder again on every chunk would rescan everything buffered for that line so
|
|
739
|
+
// far, which is quadratic in the length of a line the peer never terminates
|
|
740
|
+
if (!data.includes('\n')) {
|
|
741
|
+
this._remainder += data;
|
|
742
|
+
if (this._remainder.length > maxResponseSize) {
|
|
743
|
+
return this._onResponseTooLarge();
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const lines = (this._remainder + data).split(/\r?\n/);
|
|
878
748
|
this._remainder = lines.pop();
|
|
879
|
-
|
|
880
749
|
for (let i = 0, len = lines.length; i < len; i++) {
|
|
881
|
-
if (this.
|
|
882
|
-
|
|
883
|
-
if (/^\d+-/.test(lastline.split('\n').pop())) {
|
|
884
|
-
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
750
|
+
if (this._responsePartial) {
|
|
751
|
+
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
|
887
752
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
753
|
+
else {
|
|
754
|
+
this._responseQueue.push(lines[i]);
|
|
755
|
+
}
|
|
756
|
+
// The line just added is the last line of that queue entry, so it alone decides
|
|
757
|
+
// whether the reply is still partial. Looking for the last line of the accumulated
|
|
758
|
+
// entry instead would rescan a string that grows with every continuation line,
|
|
759
|
+
// which is quadratic in the size of the reply
|
|
760
|
+
this._responsePartial = isPartialLine(lines[i]);
|
|
761
|
+
// Checked as each line lands, so a peer that never completes a reply cannot keep
|
|
762
|
+
// buffering, and the limit does not depend on how it split its bytes into chunks
|
|
763
|
+
if (this._responsePartial && this._responseQueue[this._responseQueue.length - 1].length > maxResponseSize) {
|
|
764
|
+
return this._onResponseTooLarge();
|
|
895
765
|
}
|
|
896
766
|
}
|
|
897
|
-
|
|
767
|
+
if (this._remainder.length > maxResponseSize) {
|
|
768
|
+
return this._onResponseTooLarge();
|
|
769
|
+
}
|
|
770
|
+
if (this._responsePartial) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
898
773
|
this._processResponse();
|
|
899
774
|
}
|
|
900
|
-
|
|
775
|
+
/**
|
|
776
|
+
* Drops a connection whose peer keeps extending a reply it never completes, releasing
|
|
777
|
+
* whatever was buffered for that reply
|
|
778
|
+
* @internal
|
|
779
|
+
*/
|
|
780
|
+
_onResponseTooLarge() {
|
|
781
|
+
this._remainder = '';
|
|
782
|
+
this._responseQueue = [];
|
|
783
|
+
this._responsePartial = false;
|
|
784
|
+
this._onError(new Error('Server response exceeds maximum allowed size'), 'EPROTOCOL', false, 'CONN');
|
|
785
|
+
}
|
|
901
786
|
/**
|
|
902
787
|
* 'error' listener for the socket
|
|
903
788
|
*
|
|
904
789
|
* @event
|
|
905
|
-
* @param
|
|
906
|
-
* @param
|
|
790
|
+
* @param err Error object
|
|
791
|
+
* @param type Error name
|
|
792
|
+
* @param data Server response that triggered the error, false if there is none
|
|
793
|
+
* @param command SMTP command that was in flight
|
|
794
|
+
* @internal
|
|
907
795
|
*/
|
|
908
796
|
_onError(err, type, data, command) {
|
|
909
797
|
clearTimeout(this._connectionTimeout);
|
|
910
798
|
clearTimeout(this._greetingTimeout);
|
|
911
|
-
|
|
912
799
|
if (this._destroyed) {
|
|
913
800
|
// just ignore, already closed
|
|
914
801
|
// this might happen when a socket is canceled because of reached timeout
|
|
915
802
|
// but the socket timeout error itself receives only after
|
|
916
803
|
return;
|
|
917
804
|
}
|
|
918
|
-
|
|
919
805
|
err = this._formatError(err, type, data, command);
|
|
920
|
-
|
|
921
806
|
const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
|
|
922
807
|
if (transientCodes.includes(err.code)) {
|
|
923
808
|
this.logger.warn(data, err.message);
|
|
924
|
-
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
925
811
|
this.logger.error(data, err.message);
|
|
926
812
|
}
|
|
927
|
-
|
|
813
|
+
// close() forgets the send in flight, it is completed with this same error afterwards so
|
|
814
|
+
// a late message stream error has nothing left to report
|
|
815
|
+
const pendingSend = this._pendingSend;
|
|
928
816
|
this.emit('error', err);
|
|
929
817
|
this.close();
|
|
818
|
+
if (pendingSend) {
|
|
819
|
+
pendingSend.callback(err);
|
|
820
|
+
}
|
|
930
821
|
}
|
|
931
|
-
|
|
822
|
+
/** @internal */
|
|
932
823
|
_formatError(message, type, response, command) {
|
|
933
824
|
let err;
|
|
934
|
-
|
|
935
825
|
if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
|
|
936
826
|
err = message;
|
|
937
|
-
}
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
938
829
|
err = new Error(message);
|
|
939
830
|
}
|
|
940
|
-
|
|
941
831
|
if (type && type !== 'Error') {
|
|
942
832
|
err.code = type;
|
|
943
833
|
}
|
|
944
|
-
|
|
945
834
|
if (response) {
|
|
946
835
|
err.response = response;
|
|
947
836
|
err.message += ': ' + response;
|
|
948
837
|
}
|
|
949
|
-
|
|
950
838
|
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
|
951
839
|
if (responseCode) {
|
|
952
840
|
err.responseCode = responseCode;
|
|
953
841
|
}
|
|
954
|
-
|
|
955
842
|
if (command) {
|
|
956
843
|
err.command = command;
|
|
957
844
|
}
|
|
958
|
-
|
|
959
845
|
return err;
|
|
960
846
|
}
|
|
961
|
-
|
|
962
847
|
/**
|
|
963
848
|
* 'close' listener for the socket
|
|
964
849
|
*
|
|
965
850
|
* @event
|
|
851
|
+
* @internal
|
|
966
852
|
*/
|
|
967
853
|
_onClose() {
|
|
968
854
|
let serverResponse = false;
|
|
969
|
-
|
|
970
855
|
if (this._remainder && this._remainder.trim()) {
|
|
971
856
|
this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
|
|
972
857
|
if (this.options.debug || this.options.transactionLog) {
|
|
973
|
-
this.logger.debug(
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
},
|
|
977
|
-
serverResponse
|
|
978
|
-
);
|
|
858
|
+
this.logger.debug({
|
|
859
|
+
tnx: 'server'
|
|
860
|
+
}, serverResponse);
|
|
979
861
|
}
|
|
980
862
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
);
|
|
988
|
-
|
|
863
|
+
this.logger.info({
|
|
864
|
+
tnx: 'network'
|
|
865
|
+
}, 'Connection closed');
|
|
866
|
+
// the unterminated remainder is only reported as a reply (and so gives the error a responseCode)
|
|
867
|
+
// when it starts like a complete failure reply, not for a fragment such as "55" or a 250
|
|
868
|
+
const failureResponse = typeof serverResponse === 'string' && /^[45]\d{2}[ -]/.test(serverResponse) ? serverResponse : false;
|
|
989
869
|
if (this.upgrading && !this._destroyed) {
|
|
990
|
-
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS',
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
870
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', failureResponse, 'CONN');
|
|
871
|
+
}
|
|
872
|
+
if (!failureResponse && this._responseActions[0] === this._actionGreeting && this._connectCallback && !this._destroyed) {
|
|
873
|
+
// A silent close before the greeting is handed to the connect() callback rather than
|
|
874
|
+
// emitted as 'error', callers that never saw an error for it must not start throwing one
|
|
875
|
+
const connectCallback = this._connectCallback;
|
|
876
|
+
this._connectCallback = false;
|
|
877
|
+
const err = this._formatError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
|
|
878
|
+
this.logger.warn({ tnx: 'network' }, err.message);
|
|
879
|
+
connectCallback(err);
|
|
880
|
+
this.close();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
|
|
884
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
|
|
995
885
|
}
|
|
996
|
-
|
|
997
886
|
this._destroy();
|
|
998
887
|
}
|
|
999
|
-
|
|
1000
888
|
/**
|
|
1001
889
|
* 'end' listener for the socket
|
|
1002
890
|
*
|
|
1003
891
|
* @event
|
|
892
|
+
* @internal
|
|
1004
893
|
*/
|
|
1005
894
|
_onEnd() {
|
|
1006
895
|
if (this._socket && !this._socket.destroyed) {
|
|
1007
|
-
// Peer sent FIN
|
|
896
|
+
// Peer sent FIN, finish our half of the close gracefully rather
|
|
1008
897
|
// than destroying. 'close' fires after the OS finalizes teardown.
|
|
1009
898
|
this._socket.end();
|
|
1010
899
|
}
|
|
1011
900
|
}
|
|
1012
|
-
|
|
1013
901
|
/**
|
|
1014
902
|
* 'timeout' listener for the socket
|
|
1015
903
|
*
|
|
1016
904
|
* @event
|
|
905
|
+
* @internal
|
|
1017
906
|
*/
|
|
1018
907
|
_onTimeout() {
|
|
1019
908
|
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
|
|
1020
909
|
}
|
|
1021
|
-
|
|
1022
910
|
/**
|
|
1023
911
|
* Destroys the client, emits 'end'
|
|
912
|
+
* @internal
|
|
1024
913
|
*/
|
|
1025
914
|
_destroy() {
|
|
1026
915
|
if (this._destroyed) {
|
|
@@ -1029,14 +918,20 @@ class SMTPConnection extends EventEmitter {
|
|
|
1029
918
|
this._destroyed = true;
|
|
1030
919
|
// keep the documented public flag in sync with the private state
|
|
1031
920
|
this.destroyed = true;
|
|
921
|
+
// a connection the server dropped before the greeting would otherwise keep
|
|
922
|
+
// the greeting timer, and with it the process, alive until it fires
|
|
923
|
+
clearTimeout(this._connectionTimeout);
|
|
924
|
+
clearTimeout(this._greetingTimeout);
|
|
925
|
+
this._connectionTimeout = false;
|
|
926
|
+
this._greetingTimeout = false;
|
|
1032
927
|
this.emit('end');
|
|
1033
928
|
}
|
|
1034
|
-
|
|
1035
929
|
/**
|
|
1036
930
|
* Upgrades the connection to TLS
|
|
1037
931
|
*
|
|
1038
|
-
* @param
|
|
932
|
+
* @param callback Callback function to run when the connection
|
|
1039
933
|
* has been secured
|
|
934
|
+
* @internal
|
|
1040
935
|
*/
|
|
1041
936
|
_upgradeConnection(callback) {
|
|
1042
937
|
// RFC 3207 section 6: the client MUST discard any knowledge obtained from
|
|
@@ -1047,28 +942,21 @@ class SMTPConnection extends EventEmitter {
|
|
|
1047
942
|
// part of the secured EHLO capabilities). STARTTLS response injection.
|
|
1048
943
|
this._remainder = '';
|
|
1049
944
|
this._responseQueue = [];
|
|
1050
|
-
|
|
945
|
+
this._responsePartial = false;
|
|
1051
946
|
// do not remove all listeners or it breaks node v0.10 as there's
|
|
1052
947
|
// apparently a 'finish' event set that would be cleared as well
|
|
1053
|
-
|
|
1054
948
|
// we can safely keep 'error', 'end', 'close' etc. events
|
|
1055
|
-
this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
|
|
1056
|
-
this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
|
|
1057
|
-
|
|
1058
949
|
const socketPlain = this._socket;
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
);
|
|
1066
|
-
|
|
950
|
+
socketPlain.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
|
|
951
|
+
socketPlain.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
|
|
952
|
+
const opts = Object.assign({
|
|
953
|
+
socket: socketPlain,
|
|
954
|
+
host: this.host
|
|
955
|
+
}, this.options.tls || {});
|
|
1067
956
|
// ensure servername for SNI
|
|
1068
957
|
if (this.servername && !opts.servername) {
|
|
1069
958
|
opts.servername = this.servername;
|
|
1070
959
|
}
|
|
1071
|
-
|
|
1072
960
|
// Remove all listeners from the plain socket to allow proper garbage
|
|
1073
961
|
// collection. Used on both the TLS-success path and the synchronous
|
|
1074
962
|
// tls.connect() throw path; either way the plain socket is done.
|
|
@@ -1080,7 +968,6 @@ class SMTPConnection extends EventEmitter {
|
|
|
1080
968
|
// options.connection socket; strip it so nothing lingers on the plain socket
|
|
1081
969
|
socketPlain.removeListener('error', this._onConnectionSocketError);
|
|
1082
970
|
};
|
|
1083
|
-
|
|
1084
971
|
this.upgrading = true;
|
|
1085
972
|
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
|
1086
973
|
try {
|
|
@@ -1088,200 +975,170 @@ class SMTPConnection extends EventEmitter {
|
|
|
1088
975
|
this.secure = true;
|
|
1089
976
|
this.upgrading = false;
|
|
1090
977
|
this._socket.on('data', this._onSocketData);
|
|
1091
|
-
|
|
1092
978
|
removePlainSocketListeners();
|
|
1093
|
-
|
|
1094
979
|
return callback(null, true);
|
|
1095
980
|
});
|
|
1096
|
-
}
|
|
981
|
+
}
|
|
982
|
+
catch (err) {
|
|
1097
983
|
removePlainSocketListeners();
|
|
1098
984
|
return callback(err);
|
|
1099
985
|
}
|
|
1100
|
-
|
|
1101
986
|
this._socket.on('error', this._onSocketError);
|
|
1102
987
|
this._socket.once('close', this._onSocketClose);
|
|
1103
988
|
this._socket.once('end', this._onSocketEnd);
|
|
1104
|
-
|
|
1105
989
|
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
|
|
1106
990
|
this._socket.on('timeout', this._onSocketTimeout);
|
|
1107
|
-
|
|
1108
991
|
// resume in case the socket was paused
|
|
1109
992
|
socketPlain.resume();
|
|
1110
993
|
}
|
|
1111
|
-
|
|
1112
994
|
/**
|
|
1113
995
|
* Processes queued responses from the server
|
|
996
|
+
* @internal
|
|
1114
997
|
*/
|
|
1115
998
|
_processResponse() {
|
|
1116
999
|
if (!this._responseQueue.length) {
|
|
1117
1000
|
return false;
|
|
1118
1001
|
}
|
|
1119
|
-
|
|
1120
1002
|
const raw = (this._responseQueue.shift() || '').toString();
|
|
1121
|
-
|
|
1122
1003
|
// Skip unexpected empty lines without consuming a response action or
|
|
1123
1004
|
// overwriting lastServerResponse; reprocess whatever else is queued.
|
|
1124
1005
|
if (!raw.trim()) {
|
|
1125
1006
|
setImmediate(() => this._processResponse());
|
|
1126
1007
|
return;
|
|
1127
1008
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// last line is still a continuation: put the partial response back on the
|
|
1133
|
-
// queue and wait for the rest rather than dropping it
|
|
1009
|
+
if (isPartialResponse(raw)) {
|
|
1010
|
+
// the rest of the reply is still on its way: put it back on the queue and wait
|
|
1011
|
+
// rather than dropping it. It has not been received in full, so it must not be
|
|
1012
|
+
// reported as the last server response either
|
|
1134
1013
|
this._responseQueue.unshift(raw);
|
|
1135
1014
|
return;
|
|
1136
1015
|
}
|
|
1137
|
-
|
|
1016
|
+
const str = (this.lastServerResponse = decodeServerResponse(raw));
|
|
1138
1017
|
if (this.options.debug || this.options.transactionLog) {
|
|
1139
|
-
this.logger.debug(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
},
|
|
1143
|
-
str.replace(/\r?\n$/, '')
|
|
1144
|
-
);
|
|
1018
|
+
this.logger.debug({
|
|
1019
|
+
tnx: 'server'
|
|
1020
|
+
}, str.replace(/\r?\n$/, ''));
|
|
1145
1021
|
}
|
|
1146
|
-
|
|
1147
1022
|
const action = this._responseActions.shift();
|
|
1148
|
-
|
|
1149
1023
|
if (typeof action === 'function') {
|
|
1150
1024
|
action.call(this, str);
|
|
1151
1025
|
setImmediate(() => this._processResponse());
|
|
1152
|
-
}
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1153
1028
|
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
|
|
1154
1029
|
}
|
|
1155
1030
|
}
|
|
1156
|
-
|
|
1157
1031
|
/**
|
|
1158
1032
|
* Send a command to the server, append \r\n
|
|
1159
1033
|
*
|
|
1160
|
-
* @param
|
|
1161
|
-
* @param
|
|
1034
|
+
* @param str String to be sent to the server
|
|
1035
|
+
* @param logStr Optional string to be used for logging instead of the actual string
|
|
1036
|
+
* @internal
|
|
1162
1037
|
*/
|
|
1163
1038
|
_sendCommand(str, logStr) {
|
|
1164
1039
|
if (this._destroyed) {
|
|
1165
1040
|
// Connection already closed, can't send any more data
|
|
1166
1041
|
return;
|
|
1167
1042
|
}
|
|
1168
|
-
|
|
1169
|
-
if (
|
|
1043
|
+
const socket = this._socket;
|
|
1044
|
+
if (socket.destroyed) {
|
|
1170
1045
|
return this.close();
|
|
1171
1046
|
}
|
|
1172
|
-
|
|
1173
1047
|
if (this.options.debug || this.options.transactionLog) {
|
|
1174
|
-
this.logger.debug(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
this._socket.write(Buffer.from(str + '\r\n', 'utf-8'));
|
|
1048
|
+
this.logger.debug({
|
|
1049
|
+
tnx: 'client'
|
|
1050
|
+
}, (logStr || str || '').toString().replace(/\r?\n$/, ''));
|
|
1051
|
+
}
|
|
1052
|
+
socket.write(Buffer.from(str + '\r\n', 'utf-8'));
|
|
1183
1053
|
}
|
|
1184
|
-
|
|
1185
1054
|
/**
|
|
1186
1055
|
* Initiates a new message by submitting envelope data, starting with
|
|
1187
1056
|
* MAIL FROM: command
|
|
1188
1057
|
*
|
|
1189
|
-
* @param
|
|
1058
|
+
* @param envelope Envelope object in the form of
|
|
1190
1059
|
* {from:'...', to:['...']}
|
|
1191
1060
|
* or
|
|
1192
1061
|
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
|
1062
|
+
* @param callback Callback to run once the envelope is processed
|
|
1063
|
+
* @internal
|
|
1193
1064
|
*/
|
|
1194
1065
|
_setEnvelope(envelope, callback) {
|
|
1195
1066
|
const args = [];
|
|
1196
1067
|
let useSmtpUtf8 = false;
|
|
1197
|
-
|
|
1198
|
-
this._envelope =
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
this._envelope.to = []
|
|
1202
|
-
|
|
1068
|
+
this._envelope = (envelope || {});
|
|
1069
|
+
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
|
1070
|
+
.toString()
|
|
1071
|
+
.trim();
|
|
1072
|
+
this._envelope.to = []
|
|
1073
|
+
.concat(this._envelope.to || [])
|
|
1074
|
+
.map(to => ((to && to.address) || to || '').toString().trim());
|
|
1203
1075
|
if (!this._envelope.to.length) {
|
|
1204
1076
|
return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
|
|
1205
1077
|
}
|
|
1206
|
-
|
|
1207
1078
|
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
|
|
1208
1079
|
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
|
|
1209
1080
|
}
|
|
1210
|
-
|
|
1211
1081
|
// check if the sender address uses only ASCII characters,
|
|
1212
1082
|
// otherwise require usage of SMTPUTF8 extension
|
|
1213
1083
|
if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
|
|
1214
1084
|
useSmtpUtf8 = true;
|
|
1215
1085
|
}
|
|
1216
|
-
|
|
1217
1086
|
for (let i = 0, len = this._envelope.to.length; i < len; i++) {
|
|
1218
1087
|
if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
|
|
1219
1088
|
return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
|
|
1220
1089
|
}
|
|
1221
|
-
|
|
1222
1090
|
// check if the recipients addresses use only ASCII characters,
|
|
1223
1091
|
// otherwise require usage of SMTPUTF8 extension
|
|
1224
1092
|
if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
|
|
1225
1093
|
useSmtpUtf8 = true;
|
|
1226
1094
|
}
|
|
1227
1095
|
}
|
|
1228
|
-
|
|
1229
1096
|
// clone the recipients array for latter manipulation
|
|
1230
1097
|
this._envelope.rcptQueue = [].concat(this._envelope.to || []);
|
|
1231
1098
|
this._envelope.rejected = [];
|
|
1232
1099
|
this._envelope.rejectedErrors = [];
|
|
1233
1100
|
this._envelope.accepted = [];
|
|
1234
|
-
|
|
1235
1101
|
if (this._envelope.dsn) {
|
|
1236
1102
|
try {
|
|
1237
1103
|
this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
|
|
1238
|
-
}
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1239
1106
|
return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
|
|
1240
1107
|
}
|
|
1241
1108
|
}
|
|
1242
|
-
|
|
1243
1109
|
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
|
1244
1110
|
// response action, so a rejection here cannot leave an orphaned action in
|
|
1245
1111
|
// _responseActions (which would consume the next reply and desync a reused
|
|
1246
1112
|
// connection).
|
|
1247
1113
|
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1248
1114
|
if (!this.secure) {
|
|
1249
|
-
return callback(
|
|
1250
|
-
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1251
|
-
);
|
|
1115
|
+
return callback(this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
|
|
1252
1116
|
}
|
|
1253
1117
|
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
1254
|
-
return callback(
|
|
1255
|
-
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1256
|
-
);
|
|
1118
|
+
return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
|
|
1257
1119
|
}
|
|
1258
1120
|
}
|
|
1259
|
-
|
|
1260
1121
|
this._responseActions.push(str => {
|
|
1261
1122
|
this._actionMAIL(str, callback);
|
|
1262
1123
|
});
|
|
1263
|
-
|
|
1264
1124
|
// If the server supports SMTPUTF8 and the envelope includes an internationalized
|
|
1265
1125
|
// email address then append SMTPUTF8 keyword to the MAIL FROM command
|
|
1266
1126
|
if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
|
|
1267
1127
|
args.push('SMTPUTF8');
|
|
1268
1128
|
this._usingSmtpUtf8 = true;
|
|
1269
1129
|
}
|
|
1270
|
-
|
|
1271
1130
|
// If the server supports 8BITMIME and the message might contain non-ascii bytes
|
|
1272
1131
|
// then append the 8BITMIME keyword to the MAIL FROM command
|
|
1273
1132
|
if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
|
|
1274
1133
|
args.push('BODY=8BITMIME');
|
|
1275
1134
|
this._using8BitMime = true;
|
|
1276
1135
|
}
|
|
1277
|
-
|
|
1278
1136
|
if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
|
|
1279
1137
|
const sizeValue = Number(this._envelope.size) || 0;
|
|
1280
1138
|
if (sizeValue > 0) {
|
|
1281
1139
|
args.push('SIZE=' + sizeValue);
|
|
1282
1140
|
}
|
|
1283
1141
|
}
|
|
1284
|
-
|
|
1285
1142
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
1286
1143
|
// then append DSN params to the MAIL FROM command
|
|
1287
1144
|
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
|
@@ -1292,17 +1149,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
1292
1149
|
args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
|
|
1293
1150
|
}
|
|
1294
1151
|
}
|
|
1295
|
-
|
|
1296
1152
|
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
|
1297
1153
|
// (TLS connection + server support) was already validated above, before
|
|
1298
1154
|
// the response action was queued.
|
|
1299
1155
|
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1300
1156
|
args.push('REQUIRETLS');
|
|
1301
1157
|
}
|
|
1302
|
-
|
|
1303
1158
|
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
|
|
1304
1159
|
}
|
|
1305
|
-
|
|
1160
|
+
/** @internal */
|
|
1306
1161
|
_setDsnEnvelope(params) {
|
|
1307
1162
|
let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
|
|
1308
1163
|
if (ret) {
|
|
@@ -1317,13 +1172,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1317
1172
|
break;
|
|
1318
1173
|
}
|
|
1319
1174
|
}
|
|
1320
|
-
|
|
1321
1175
|
if (ret && !['FULL', 'HDRS'].includes(ret)) {
|
|
1322
1176
|
throw new Error('ret: ' + JSON.stringify(ret));
|
|
1323
1177
|
}
|
|
1324
|
-
|
|
1325
1178
|
const envid = (params.envid || params.id || '').toString() || null;
|
|
1326
|
-
|
|
1327
1179
|
let notify = params.notify || null;
|
|
1328
1180
|
if (notify) {
|
|
1329
1181
|
if (typeof notify === 'string') {
|
|
@@ -1337,12 +1189,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1337
1189
|
}
|
|
1338
1190
|
notify = notify.join(',');
|
|
1339
1191
|
}
|
|
1340
|
-
|
|
1341
1192
|
let orcpt = (params.recipient || params.orcpt || '').toString() || null;
|
|
1342
1193
|
if (orcpt && orcpt.indexOf(';') < 0) {
|
|
1343
1194
|
orcpt = 'rfc822;' + orcpt;
|
|
1344
1195
|
}
|
|
1345
|
-
|
|
1346
1196
|
return {
|
|
1347
1197
|
ret,
|
|
1348
1198
|
envid,
|
|
@@ -1350,292 +1200,252 @@ class SMTPConnection extends EventEmitter {
|
|
|
1350
1200
|
orcpt
|
|
1351
1201
|
};
|
|
1352
1202
|
}
|
|
1353
|
-
|
|
1203
|
+
/** @internal */
|
|
1354
1204
|
_getDsnRcptToArgs() {
|
|
1205
|
+
const envelope = this._envelope;
|
|
1355
1206
|
const args = [];
|
|
1356
1207
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
1357
1208
|
// then append DSN params to the RCPT TO command
|
|
1358
|
-
if (
|
|
1359
|
-
if (
|
|
1360
|
-
args.push('NOTIFY=' + shared.encodeXText(
|
|
1209
|
+
if (envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
|
1210
|
+
if (envelope.dsn.notify) {
|
|
1211
|
+
args.push('NOTIFY=' + shared.encodeXText(envelope.dsn.notify));
|
|
1361
1212
|
}
|
|
1362
|
-
if (
|
|
1363
|
-
args.push('ORCPT=' + shared.encodeXText(
|
|
1213
|
+
if (envelope.dsn.orcpt) {
|
|
1214
|
+
args.push('ORCPT=' + shared.encodeXText(envelope.dsn.orcpt));
|
|
1364
1215
|
}
|
|
1365
1216
|
}
|
|
1366
1217
|
return args.length ? ' ' + args.join(' ') : '';
|
|
1367
1218
|
}
|
|
1368
|
-
|
|
1219
|
+
/** @internal */
|
|
1369
1220
|
_createSendStream(callback) {
|
|
1221
|
+
const envelope = this._envelope;
|
|
1370
1222
|
const dataStream = new DataStream();
|
|
1371
|
-
|
|
1372
1223
|
if (this.options.lmtp) {
|
|
1373
|
-
|
|
1374
|
-
const final = i ===
|
|
1224
|
+
envelope.accepted.forEach((recipient, i) => {
|
|
1225
|
+
const final = i === envelope.accepted.length - 1;
|
|
1375
1226
|
this._responseActions.push(str => {
|
|
1376
1227
|
this._actionLMTPStream(recipient, final, str, callback);
|
|
1377
1228
|
});
|
|
1378
1229
|
});
|
|
1379
|
-
}
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1380
1232
|
this._responseActions.push(str => {
|
|
1381
1233
|
this._actionSMTPStream(str, callback);
|
|
1382
1234
|
});
|
|
1383
1235
|
}
|
|
1384
|
-
|
|
1385
1236
|
this._currentDataStream = dataStream;
|
|
1386
1237
|
dataStream.pipe(this._socket, {
|
|
1387
1238
|
end: false
|
|
1388
1239
|
});
|
|
1389
|
-
|
|
1390
1240
|
if (this.options.debug) {
|
|
1391
1241
|
const logStream = new PassThrough();
|
|
1392
1242
|
logStream.on('readable', () => {
|
|
1393
1243
|
let chunk;
|
|
1394
1244
|
while ((chunk = logStream.read())) {
|
|
1395
|
-
this.logger.debug(
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
},
|
|
1399
|
-
chunk.toString('binary').replace(/\r?\n$/, '')
|
|
1400
|
-
);
|
|
1245
|
+
this.logger.debug({
|
|
1246
|
+
tnx: 'message'
|
|
1247
|
+
}, chunk.toString('binary').replace(/\r?\n$/, ''));
|
|
1401
1248
|
}
|
|
1402
1249
|
});
|
|
1403
1250
|
dataStream.pipe(logStream);
|
|
1404
1251
|
}
|
|
1405
|
-
|
|
1406
1252
|
dataStream.once('end', () => {
|
|
1407
1253
|
if (this._currentDataStream === dataStream) {
|
|
1408
1254
|
this._currentDataStream = false;
|
|
1409
1255
|
}
|
|
1410
|
-
this.logger.info(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
},
|
|
1416
|
-
'<%s bytes encoded mime message (source size %s bytes)>',
|
|
1417
|
-
dataStream.outByteCount,
|
|
1418
|
-
dataStream.inByteCount
|
|
1419
|
-
);
|
|
1256
|
+
this.logger.info({
|
|
1257
|
+
tnx: 'message',
|
|
1258
|
+
inByteCount: dataStream.inByteCount,
|
|
1259
|
+
outByteCount: dataStream.outByteCount
|
|
1260
|
+
}, '<%s bytes encoded mime message (source size %s bytes)>', dataStream.outByteCount, dataStream.inByteCount);
|
|
1420
1261
|
});
|
|
1421
|
-
|
|
1422
1262
|
return dataStream;
|
|
1423
1263
|
}
|
|
1424
|
-
|
|
1425
1264
|
/** ACTIONS **/
|
|
1426
|
-
|
|
1427
1265
|
/**
|
|
1428
1266
|
* Will be run after the connection is created and the server sends
|
|
1429
1267
|
* a greeting. If the incoming message starts with 220 initiate
|
|
1430
1268
|
* SMTP session by sending EHLO command
|
|
1431
1269
|
*
|
|
1432
|
-
* @param
|
|
1270
|
+
* @param str Message from the server
|
|
1271
|
+
* @internal
|
|
1433
1272
|
*/
|
|
1434
1273
|
_actionGreeting(str) {
|
|
1435
1274
|
clearTimeout(this._greetingTimeout);
|
|
1436
|
-
|
|
1437
1275
|
if (str.substr(0, 3) !== '220') {
|
|
1438
1276
|
this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
|
|
1439
1277
|
return;
|
|
1440
1278
|
}
|
|
1441
|
-
|
|
1442
1279
|
if (this.options.lmtp) {
|
|
1443
1280
|
this._responseActions.push(this._actionLHLO);
|
|
1444
1281
|
this._sendCommand('LHLO ' + this.name);
|
|
1445
|
-
}
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1446
1284
|
this._responseActions.push(this._actionEHLO);
|
|
1447
1285
|
this._sendCommand('EHLO ' + this.name);
|
|
1448
1286
|
}
|
|
1449
1287
|
}
|
|
1450
|
-
|
|
1451
1288
|
/**
|
|
1452
1289
|
* Handles server response for LHLO command. If it yielded in
|
|
1453
1290
|
* error, emit 'error', otherwise treat this as an EHLO response
|
|
1454
1291
|
*
|
|
1455
|
-
* @param
|
|
1292
|
+
* @param str Message from the server
|
|
1293
|
+
* @internal
|
|
1456
1294
|
*/
|
|
1457
1295
|
_actionLHLO(str) {
|
|
1458
1296
|
if (str.charAt(0) !== '2') {
|
|
1459
1297
|
this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
|
|
1460
1298
|
return;
|
|
1461
1299
|
}
|
|
1462
|
-
|
|
1463
1300
|
this._actionEHLO(str);
|
|
1464
1301
|
}
|
|
1465
|
-
|
|
1466
1302
|
/**
|
|
1467
1303
|
* Handles server response for EHLO command. If it yielded in
|
|
1468
1304
|
* error, try HELO instead, otherwise initiate TLS negotiation
|
|
1469
1305
|
* if STARTTLS is supported by the server or move into the
|
|
1470
1306
|
* authentication phase.
|
|
1471
1307
|
*
|
|
1472
|
-
* @param
|
|
1308
|
+
* @param str Message from the server
|
|
1309
|
+
* @internal
|
|
1473
1310
|
*/
|
|
1474
1311
|
_actionEHLO(str) {
|
|
1475
1312
|
let match;
|
|
1476
|
-
|
|
1477
1313
|
if (str.substr(0, 3) === '421') {
|
|
1478
1314
|
this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
|
|
1479
1315
|
return;
|
|
1480
1316
|
}
|
|
1481
|
-
|
|
1482
1317
|
if (str.charAt(0) !== '2') {
|
|
1483
1318
|
if (this.options.requireTLS) {
|
|
1484
|
-
this._onError(
|
|
1485
|
-
new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str),
|
|
1486
|
-
'ECONNECTION',
|
|
1487
|
-
str,
|
|
1488
|
-
'EHLO'
|
|
1489
|
-
);
|
|
1319
|
+
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
|
|
1490
1320
|
return;
|
|
1491
1321
|
}
|
|
1492
|
-
|
|
1493
1322
|
// Try HELO instead
|
|
1494
1323
|
this._responseActions.push(this._actionHELO);
|
|
1495
1324
|
this._sendCommand('HELO ' + this.name);
|
|
1496
1325
|
return;
|
|
1497
1326
|
}
|
|
1498
|
-
|
|
1499
1327
|
this._ehloLines = str
|
|
1500
1328
|
.split(/\r?\n/)
|
|
1501
1329
|
.map(line => line.replace(/^\d+[ -]/, '').trim())
|
|
1502
1330
|
.filter(line => line)
|
|
1503
1331
|
.slice(1);
|
|
1504
|
-
|
|
1505
1332
|
// Detect if the server supports STARTTLS
|
|
1506
1333
|
if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
|
|
1507
1334
|
this._sendCommand('STARTTLS');
|
|
1508
1335
|
this._responseActions.push(this._actionSTARTTLS);
|
|
1509
1336
|
return;
|
|
1510
1337
|
}
|
|
1511
|
-
|
|
1512
1338
|
// Detect if the server supports SMTPUTF8
|
|
1513
1339
|
if (/[ -]SMTPUTF8\b/im.test(str)) {
|
|
1514
1340
|
this._supportedExtensions.push('SMTPUTF8');
|
|
1515
1341
|
}
|
|
1516
|
-
|
|
1517
1342
|
// Detect if the server supports DSN
|
|
1518
1343
|
if (/[ -]DSN\b/im.test(str)) {
|
|
1519
1344
|
this._supportedExtensions.push('DSN');
|
|
1520
1345
|
}
|
|
1521
|
-
|
|
1522
1346
|
// Detect if the server supports 8BITMIME
|
|
1523
1347
|
if (/[ -]8BITMIME\b/im.test(str)) {
|
|
1524
1348
|
this._supportedExtensions.push('8BITMIME');
|
|
1525
1349
|
}
|
|
1526
|
-
|
|
1527
1350
|
// Detect if the server supports REQUIRETLS (RFC 8689)
|
|
1528
1351
|
if (/[ -]REQUIRETLS\b/im.test(str)) {
|
|
1529
1352
|
this._supportedExtensions.push('REQUIRETLS');
|
|
1530
1353
|
}
|
|
1531
|
-
|
|
1532
1354
|
// Detect if the server supports PIPELINING
|
|
1533
1355
|
if (/[ -]PIPELINING\b/im.test(str)) {
|
|
1534
1356
|
this._supportedExtensions.push('PIPELINING');
|
|
1535
1357
|
}
|
|
1536
|
-
|
|
1537
1358
|
// Detect if the server supports AUTH
|
|
1538
1359
|
if (/[ -]AUTH\b/i.test(str)) {
|
|
1539
1360
|
this.allowsAuth = true;
|
|
1540
1361
|
}
|
|
1541
|
-
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
this._supportedAuth.push('CRAM-MD5');
|
|
1362
|
+
// Detect the advertised SASL mechanisms. The list is split into whole tokens rather
|
|
1363
|
+
// than searched for each name with a pattern: the patterns this replaced let two
|
|
1364
|
+
// whitespace runs overlap and backtracked quadratically over an AUTH line padded with
|
|
1365
|
+
// spaces, so a hostile server could stall the event loop from its EHLO reply
|
|
1366
|
+
// (GHSA-4ffr-jq9g-5ffx).
|
|
1367
|
+
const authMechanisms = new Set();
|
|
1368
|
+
for (const line of this._ehloLines) {
|
|
1369
|
+
const authMatch = /^AUTH[\s=](.*)/i.exec(line);
|
|
1370
|
+
if (authMatch) {
|
|
1371
|
+
for (const mechanism of authMatch[1].split(/[\s=]+/)) {
|
|
1372
|
+
authMechanisms.add(mechanism.toUpperCase());
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1555
1375
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1376
|
+
// listed in order of preference, the first one the credentials allow is used
|
|
1377
|
+
for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
|
|
1378
|
+
if (authMechanisms.has(mechanism)) {
|
|
1379
|
+
this._supportedAuth.push(mechanism);
|
|
1380
|
+
}
|
|
1560
1381
|
}
|
|
1561
|
-
|
|
1562
1382
|
// Detect if the server supports SIZE extensions (and the max allowed size)
|
|
1563
1383
|
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
|
1564
1384
|
this._supportedExtensions.push('SIZE');
|
|
1565
1385
|
this._maxAllowedSize = Number(match[1]) || 0;
|
|
1566
1386
|
}
|
|
1567
|
-
|
|
1568
1387
|
this.emit('connect');
|
|
1569
1388
|
}
|
|
1570
|
-
|
|
1571
1389
|
/**
|
|
1572
1390
|
* Handles server response for HELO command. If it yielded in
|
|
1573
1391
|
* error, emit 'error', otherwise move into the authentication phase.
|
|
1574
1392
|
*
|
|
1575
|
-
* @param
|
|
1393
|
+
* @param str Message from the server
|
|
1394
|
+
* @internal
|
|
1576
1395
|
*/
|
|
1577
1396
|
_actionHELO(str) {
|
|
1578
1397
|
if (str.charAt(0) !== '2') {
|
|
1579
1398
|
this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
|
|
1580
1399
|
return;
|
|
1581
1400
|
}
|
|
1582
|
-
|
|
1583
1401
|
// assume that authentication is enabled (most probably is not though)
|
|
1584
1402
|
this.allowsAuth = true;
|
|
1585
|
-
|
|
1586
1403
|
this.emit('connect');
|
|
1587
1404
|
}
|
|
1588
|
-
|
|
1589
1405
|
/**
|
|
1590
1406
|
* Handles server response for STARTTLS command. If there's an error
|
|
1591
1407
|
* try HELO instead, otherwise initiate TLS upgrade. If the upgrade
|
|
1592
1408
|
* succeedes restart the EHLO
|
|
1593
1409
|
*
|
|
1594
|
-
* @param
|
|
1410
|
+
* @param str Message from the server
|
|
1411
|
+
* @internal
|
|
1595
1412
|
*/
|
|
1596
1413
|
_actionSTARTTLS(str) {
|
|
1597
1414
|
if (str.charAt(0) !== '2') {
|
|
1598
1415
|
if (this.options.opportunisticTLS) {
|
|
1599
|
-
this.logger.info(
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
);
|
|
1605
|
-
return this.emit('connect');
|
|
1416
|
+
this.logger.info({
|
|
1417
|
+
tnx: 'smtp'
|
|
1418
|
+
}, 'Failed STARTTLS upgrade, continuing unencrypted');
|
|
1419
|
+
this.emit('connect');
|
|
1420
|
+
return;
|
|
1606
1421
|
}
|
|
1607
1422
|
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
|
|
1608
1423
|
return;
|
|
1609
1424
|
}
|
|
1610
|
-
|
|
1611
1425
|
this._upgradeConnection((err, secured) => {
|
|
1612
1426
|
if (err) {
|
|
1613
1427
|
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
|
|
1614
1428
|
return;
|
|
1615
1429
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
tnx: 'smtp'
|
|
1620
|
-
},
|
|
1621
|
-
'Connection upgraded with STARTTLS'
|
|
1622
|
-
);
|
|
1623
|
-
|
|
1430
|
+
this.logger.info({
|
|
1431
|
+
tnx: 'smtp'
|
|
1432
|
+
}, 'Connection upgraded with STARTTLS');
|
|
1624
1433
|
if (secured) {
|
|
1625
1434
|
// restart session
|
|
1626
1435
|
if (this.options.lmtp) {
|
|
1627
1436
|
this._responseActions.push(this._actionLHLO);
|
|
1628
1437
|
this._sendCommand('LHLO ' + this.name);
|
|
1629
|
-
}
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1630
1440
|
this._responseActions.push(this._actionEHLO);
|
|
1631
1441
|
this._sendCommand('EHLO ' + this.name);
|
|
1632
1442
|
}
|
|
1633
|
-
}
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1634
1445
|
this.emit('connect');
|
|
1635
1446
|
}
|
|
1636
1447
|
});
|
|
1637
1448
|
}
|
|
1638
|
-
|
|
1639
1449
|
/**
|
|
1640
1450
|
* Handle the response for AUTH LOGIN command. We are expecting
|
|
1641
1451
|
* '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
|
|
@@ -1643,7 +1453,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1643
1453
|
* exact match but settle with 334 response in general as some
|
|
1644
1454
|
* hosts invalidly use a longer message than VXNlcm5hbWU6
|
|
1645
1455
|
*
|
|
1646
|
-
* @param
|
|
1456
|
+
* @param str Message from the server
|
|
1457
|
+
* @param callback Callback to run once the authentication sequence completes
|
|
1458
|
+
* @internal
|
|
1647
1459
|
*/
|
|
1648
1460
|
_actionAUTH_LOGIN_USER(str, callback) {
|
|
1649
1461
|
if (!/^334[ -]/.test(str)) {
|
|
@@ -1651,14 +1463,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
1651
1463
|
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
|
|
1652
1464
|
return;
|
|
1653
1465
|
}
|
|
1654
|
-
|
|
1655
1466
|
this._responseActions.push(str => {
|
|
1656
1467
|
this._actionAUTH_LOGIN_PASS(str, callback);
|
|
1657
1468
|
});
|
|
1658
|
-
|
|
1659
1469
|
this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
|
|
1660
1470
|
}
|
|
1661
|
-
|
|
1662
1471
|
/**
|
|
1663
1472
|
* Handle the response for AUTH CRAM-MD5 command. We are expecting
|
|
1664
1473
|
* '334 <challenge string>'. Data to be sent as response needs to be
|
|
@@ -1666,102 +1475,89 @@ class SMTPConnection extends EventEmitter {
|
|
|
1666
1475
|
* a HMAC key, prefixed by the username and a space, and finally all
|
|
1667
1476
|
* base64 encoded again.
|
|
1668
1477
|
*
|
|
1669
|
-
* @param
|
|
1478
|
+
* @param str Message from the server
|
|
1479
|
+
* @param callback Callback to run once the authentication sequence completes
|
|
1480
|
+
* @internal
|
|
1670
1481
|
*/
|
|
1671
1482
|
_actionAUTH_CRAM_MD5(str, callback) {
|
|
1672
1483
|
const challengeMatch = str.match(/^334\s+(.+)$/);
|
|
1673
|
-
|
|
1674
1484
|
if (!challengeMatch) {
|
|
1675
|
-
return callback(
|
|
1676
|
-
this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
|
|
1677
|
-
);
|
|
1485
|
+
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
|
|
1678
1486
|
}
|
|
1679
|
-
|
|
1680
1487
|
// Decode from base64
|
|
1681
1488
|
const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
|
|
1682
1489
|
const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
|
|
1683
|
-
|
|
1684
1490
|
hmacMD5.update(base64decoded);
|
|
1685
|
-
|
|
1686
1491
|
const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
|
|
1687
|
-
|
|
1688
1492
|
this._responseActions.push(str => {
|
|
1689
1493
|
this._actionAUTH_CRAM_MD5_PASS(str, callback);
|
|
1690
1494
|
});
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
// hidden hash for logs
|
|
1695
|
-
Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64')
|
|
1696
|
-
);
|
|
1495
|
+
this._sendCommand(Buffer.from(prepended).toString('base64'),
|
|
1496
|
+
// hidden hash for logs
|
|
1497
|
+
Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64'));
|
|
1697
1498
|
}
|
|
1698
|
-
|
|
1699
1499
|
/**
|
|
1700
1500
|
* Handles the response to CRAM-MD5 authentication, if there's no error,
|
|
1701
1501
|
* the user can be considered logged in. Start waiting for a message to send
|
|
1702
1502
|
*
|
|
1703
|
-
* @param
|
|
1503
|
+
* @param str Message from the server
|
|
1504
|
+
* @param callback Callback to run once the authentication sequence completes
|
|
1505
|
+
* @internal
|
|
1704
1506
|
*/
|
|
1705
1507
|
_actionAUTH_CRAM_MD5_PASS(str, callback) {
|
|
1706
1508
|
if (!str.match(/^235\s+/)) {
|
|
1707
1509
|
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
|
|
1708
1510
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
method: this._authMethod
|
|
1716
|
-
},
|
|
1717
|
-
'User %s authenticated',
|
|
1718
|
-
JSON.stringify(this._auth.user)
|
|
1719
|
-
);
|
|
1511
|
+
this.logger.info({
|
|
1512
|
+
tnx: 'smtp',
|
|
1513
|
+
username: this._auth.user,
|
|
1514
|
+
action: 'authenticated',
|
|
1515
|
+
method: this._authMethod
|
|
1516
|
+
}, 'User %s authenticated', JSON.stringify(this._auth.user));
|
|
1720
1517
|
this.authenticated = true;
|
|
1721
1518
|
callback(null, true);
|
|
1722
1519
|
}
|
|
1723
|
-
|
|
1724
1520
|
/**
|
|
1725
1521
|
* Handle the response for AUTH LOGIN command. We are expecting
|
|
1726
1522
|
* '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
|
|
1727
1523
|
* response needs to be base64 encoded password.
|
|
1728
1524
|
*
|
|
1729
|
-
* @param
|
|
1525
|
+
* @param str Message from the server
|
|
1526
|
+
* @param callback Callback to run once the authentication sequence completes
|
|
1527
|
+
* @internal
|
|
1730
1528
|
*/
|
|
1731
1529
|
_actionAUTH_LOGIN_PASS(str, callback) {
|
|
1732
1530
|
if (!/^334[ -]/.test(str)) {
|
|
1733
1531
|
// expecting '334 UGFzc3dvcmQ6'
|
|
1734
1532
|
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
|
|
1735
1533
|
}
|
|
1736
|
-
|
|
1737
1534
|
this._responseActions.push(str => {
|
|
1738
1535
|
this._actionAUTHComplete(str, callback);
|
|
1739
1536
|
});
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
// Hidden pass for logs
|
|
1744
|
-
Buffer.from('/* secret */', 'utf-8').toString('base64')
|
|
1745
|
-
);
|
|
1537
|
+
this._sendCommand(Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
|
|
1538
|
+
// Hidden pass for logs
|
|
1539
|
+
Buffer.from('/* secret */', 'utf-8').toString('base64'));
|
|
1746
1540
|
}
|
|
1747
|
-
|
|
1748
1541
|
/**
|
|
1749
1542
|
* Handles the response for authentication, if there's no error,
|
|
1750
1543
|
* the user can be considered logged in. Start waiting for a message to send
|
|
1751
1544
|
*
|
|
1752
|
-
* @param
|
|
1545
|
+
* @param str Message from the server
|
|
1546
|
+
* @param isRetry True if this is a retry after a failed login, or the callback itself
|
|
1547
|
+
* @param [callback] Callback to run once the authentication sequence completes
|
|
1548
|
+
* @internal
|
|
1753
1549
|
*/
|
|
1754
1550
|
_actionAUTHComplete(str, isRetry, callback) {
|
|
1755
1551
|
if (!callback && typeof isRetry === 'function') {
|
|
1756
1552
|
callback = isRetry;
|
|
1757
1553
|
isRetry = false;
|
|
1758
1554
|
}
|
|
1759
|
-
|
|
1760
1555
|
if (str.substr(0, 3) === '334') {
|
|
1761
1556
|
this._responseActions.push(str => {
|
|
1762
1557
|
if (isRetry || this._authMethod !== 'XOAUTH2') {
|
|
1763
1558
|
this._actionAUTHComplete(str, true, callback);
|
|
1764
|
-
}
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1765
1561
|
// fetch a new OAuth2 access token
|
|
1766
1562
|
setImmediate(() => this._handleXOauth2Token(true, callback));
|
|
1767
1563
|
}
|
|
@@ -1769,103 +1565,98 @@ class SMTPConnection extends EventEmitter {
|
|
|
1769
1565
|
this._sendCommand('');
|
|
1770
1566
|
return;
|
|
1771
1567
|
}
|
|
1772
|
-
|
|
1773
1568
|
if (str.charAt(0) !== '2') {
|
|
1774
|
-
this.logger.info(
|
|
1775
|
-
{
|
|
1776
|
-
tnx: 'smtp',
|
|
1777
|
-
username: this._auth.user,
|
|
1778
|
-
action: 'authfail',
|
|
1779
|
-
method: this._authMethod
|
|
1780
|
-
},
|
|
1781
|
-
'User %s failed to authenticate',
|
|
1782
|
-
JSON.stringify(this._auth.user)
|
|
1783
|
-
);
|
|
1784
|
-
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
this.logger.info(
|
|
1788
|
-
{
|
|
1569
|
+
this.logger.info({
|
|
1789
1570
|
tnx: 'smtp',
|
|
1790
1571
|
username: this._auth.user,
|
|
1791
|
-
action: '
|
|
1572
|
+
action: 'authfail',
|
|
1792
1573
|
method: this._authMethod
|
|
1793
|
-
},
|
|
1794
|
-
'
|
|
1795
|
-
|
|
1796
|
-
|
|
1574
|
+
}, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
|
|
1575
|
+
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
|
|
1576
|
+
}
|
|
1577
|
+
this.logger.info({
|
|
1578
|
+
tnx: 'smtp',
|
|
1579
|
+
username: this._auth.user,
|
|
1580
|
+
action: 'authenticated',
|
|
1581
|
+
method: this._authMethod
|
|
1582
|
+
}, 'User %s authenticated', JSON.stringify(this._auth.user));
|
|
1797
1583
|
this.authenticated = true;
|
|
1798
1584
|
callback(null, true);
|
|
1799
1585
|
}
|
|
1800
|
-
|
|
1801
1586
|
/**
|
|
1802
1587
|
* Handle response for a MAIL FROM: command
|
|
1803
1588
|
*
|
|
1804
|
-
* @param
|
|
1589
|
+
* @param str Message from the server
|
|
1590
|
+
* @param callback Callback to run once the envelope is processed
|
|
1591
|
+
* @internal
|
|
1805
1592
|
*/
|
|
1806
1593
|
_actionMAIL(str, callback) {
|
|
1594
|
+
const envelope = this._envelope;
|
|
1807
1595
|
if (Number(str.charAt(0)) !== 2) {
|
|
1808
|
-
const message =
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
: 'Mail command failed';
|
|
1596
|
+
const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
|
|
1597
|
+
? 'Internationalized mailbox name not allowed'
|
|
1598
|
+
: 'Mail command failed';
|
|
1812
1599
|
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
|
|
1813
1600
|
}
|
|
1814
|
-
|
|
1815
|
-
if (!this._envelope.rcptQueue.length) {
|
|
1601
|
+
if (!envelope.rcptQueue.length) {
|
|
1816
1602
|
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
|
|
1817
1603
|
}
|
|
1818
|
-
|
|
1819
1604
|
this._recipientQueue = [];
|
|
1820
1605
|
const usePipelining = this._supportedExtensions.includes('PIPELINING');
|
|
1821
|
-
|
|
1822
1606
|
do {
|
|
1823
|
-
const curRecipient =
|
|
1607
|
+
const curRecipient = envelope.rcptQueue.shift();
|
|
1824
1608
|
this._recipientQueue.push(curRecipient);
|
|
1825
1609
|
this._responseActions.push(str => {
|
|
1826
1610
|
this._actionRCPT(str, callback);
|
|
1827
1611
|
});
|
|
1828
1612
|
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1829
|
-
} while (usePipelining &&
|
|
1613
|
+
} while (usePipelining && envelope.rcptQueue.length);
|
|
1830
1614
|
}
|
|
1831
|
-
|
|
1832
1615
|
/**
|
|
1833
1616
|
* Handle response for a RCPT TO: command
|
|
1834
1617
|
*
|
|
1835
|
-
* @param
|
|
1618
|
+
* @param str Message from the server
|
|
1619
|
+
* @param callback Callback to run once the envelope is processed
|
|
1620
|
+
* @internal
|
|
1836
1621
|
*/
|
|
1837
1622
|
_actionRCPT(str, callback) {
|
|
1623
|
+
const envelope = this._envelope;
|
|
1838
1624
|
let err;
|
|
1839
1625
|
const curRecipient = this._recipientQueue.shift();
|
|
1840
1626
|
if (Number(str.charAt(0)) !== 2) {
|
|
1841
1627
|
// this is a soft error
|
|
1842
|
-
const message =
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
this._envelope.rejected.push(curRecipient);
|
|
1628
|
+
const message = this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
|
|
1629
|
+
? 'Internationalized mailbox name not allowed'
|
|
1630
|
+
: 'Recipient command failed';
|
|
1631
|
+
envelope.rejected.push(curRecipient);
|
|
1847
1632
|
// store error for the failed recipient
|
|
1848
1633
|
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
|
|
1849
1634
|
err.recipient = curRecipient;
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1635
|
+
envelope.rejectedErrors.push(err);
|
|
1636
|
+
}
|
|
1637
|
+
else {
|
|
1638
|
+
envelope.accepted.push(curRecipient);
|
|
1853
1639
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (this._envelope.rejected.length < this._envelope.to.length) {
|
|
1640
|
+
if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
|
|
1641
|
+
if (envelope.rejected.length < envelope.to.length) {
|
|
1857
1642
|
this._responseActions.push(str => {
|
|
1858
1643
|
this._actionDATA(str, callback);
|
|
1859
1644
|
});
|
|
1860
1645
|
this._sendCommand('DATA');
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
// report a temporary rejection when there is one, taking the last reply would mark the
|
|
1649
|
+
// whole message as permanently failed although some recipients were only deferred
|
|
1650
|
+
const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
|
|
1651
|
+
const reply = deferred?.response ?? str;
|
|
1652
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
|
|
1653
|
+
err.rejected = envelope.rejected;
|
|
1654
|
+
err.rejectedErrors = envelope.rejectedErrors;
|
|
1865
1655
|
return callback(err);
|
|
1866
1656
|
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1657
|
+
}
|
|
1658
|
+
else if (envelope.rcptQueue.length) {
|
|
1659
|
+
const nextRecipient = envelope.rcptQueue.shift();
|
|
1869
1660
|
this._recipientQueue.push(nextRecipient);
|
|
1870
1661
|
this._responseActions.push(str => {
|
|
1871
1662
|
this._actionRCPT(str, callback);
|
|
@@ -1873,40 +1664,39 @@ class SMTPConnection extends EventEmitter {
|
|
|
1873
1664
|
this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
|
|
1874
1665
|
}
|
|
1875
1666
|
}
|
|
1876
|
-
|
|
1877
1667
|
/**
|
|
1878
1668
|
* Handle response for a DATA command
|
|
1879
1669
|
*
|
|
1880
|
-
* @param
|
|
1670
|
+
* @param str Message from the server
|
|
1671
|
+
* @param callback Callback to run once the envelope is processed
|
|
1672
|
+
* @internal
|
|
1881
1673
|
*/
|
|
1882
1674
|
_actionDATA(str, callback) {
|
|
1675
|
+
const envelope = this._envelope;
|
|
1883
1676
|
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
|
|
1884
1677
|
// some servers might use 250 instead, so lets check for 2 or 3 as the first digit
|
|
1885
1678
|
if (!/^[23]/.test(str)) {
|
|
1886
1679
|
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
|
|
1887
1680
|
}
|
|
1888
|
-
|
|
1889
1681
|
const response = {
|
|
1890
|
-
accepted:
|
|
1891
|
-
rejected:
|
|
1682
|
+
accepted: envelope.accepted,
|
|
1683
|
+
rejected: envelope.rejected
|
|
1892
1684
|
};
|
|
1893
|
-
|
|
1894
1685
|
if (this._ehloLines && this._ehloLines.length) {
|
|
1895
1686
|
response.ehlo = this._ehloLines;
|
|
1896
1687
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
response.rejectedErrors = this._envelope.rejectedErrors;
|
|
1688
|
+
if (envelope.rejectedErrors.length) {
|
|
1689
|
+
response.rejectedErrors = envelope.rejectedErrors;
|
|
1900
1690
|
}
|
|
1901
|
-
|
|
1902
1691
|
callback(null, response);
|
|
1903
1692
|
}
|
|
1904
|
-
|
|
1905
1693
|
/**
|
|
1906
1694
|
* Handle response for a DATA stream when using SMTP
|
|
1907
1695
|
* We expect a single response that defines if the sending succeeded or failed
|
|
1908
1696
|
*
|
|
1909
|
-
* @param
|
|
1697
|
+
* @param str Message from the server
|
|
1698
|
+
* @param callback Callback to run with the final send result
|
|
1699
|
+
* @internal
|
|
1910
1700
|
*/
|
|
1911
1701
|
_actionSMTPStream(str, callback) {
|
|
1912
1702
|
if (Number(str.charAt(0)) !== 2) {
|
|
@@ -1914,27 +1704,29 @@ class SMTPConnection extends EventEmitter {
|
|
|
1914
1704
|
}
|
|
1915
1705
|
return callback(null, str);
|
|
1916
1706
|
}
|
|
1917
|
-
|
|
1918
1707
|
/**
|
|
1919
1708
|
* Handle response for a DATA stream
|
|
1920
1709
|
* We expect a separate response for every recipient. All recipients can either
|
|
1921
1710
|
* succeed or fail separately
|
|
1922
1711
|
*
|
|
1923
|
-
* @param
|
|
1924
|
-
* @param
|
|
1925
|
-
* @param
|
|
1712
|
+
* @param recipient The recipient this response applies to
|
|
1713
|
+
* @param final Is this the final recipient?
|
|
1714
|
+
* @param str Message from the server
|
|
1715
|
+
* @param callback Callback to run with the final send result
|
|
1716
|
+
* @internal
|
|
1926
1717
|
*/
|
|
1927
1718
|
_actionLMTPStream(recipient, final, str, callback) {
|
|
1719
|
+
const envelope = this._envelope;
|
|
1928
1720
|
let err;
|
|
1929
1721
|
if (Number(str.charAt(0)) !== 2) {
|
|
1930
1722
|
// Message failed
|
|
1931
1723
|
err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
|
|
1932
1724
|
err.recipient = recipient;
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
for (let i = 0, len =
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
1725
|
+
envelope.rejected.push(recipient);
|
|
1726
|
+
envelope.rejectedErrors.push(err);
|
|
1727
|
+
for (let i = 0, len = envelope.accepted.length; i < len; i++) {
|
|
1728
|
+
if (envelope.accepted[i] === recipient) {
|
|
1729
|
+
envelope.accepted.splice(i, 1);
|
|
1938
1730
|
}
|
|
1939
1731
|
}
|
|
1940
1732
|
}
|
|
@@ -1942,76 +1734,64 @@ class SMTPConnection extends EventEmitter {
|
|
|
1942
1734
|
return callback(null, str);
|
|
1943
1735
|
}
|
|
1944
1736
|
}
|
|
1945
|
-
|
|
1737
|
+
/** @internal */
|
|
1946
1738
|
_handleXOauth2Token(isRetry, callback) {
|
|
1947
1739
|
this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
|
|
1948
1740
|
if (err) {
|
|
1949
|
-
this.logger.info(
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
},
|
|
1956
|
-
'User %s failed to authenticate',
|
|
1957
|
-
JSON.stringify(this._auth.user)
|
|
1958
|
-
);
|
|
1741
|
+
this.logger.info({
|
|
1742
|
+
tnx: 'smtp',
|
|
1743
|
+
username: this._auth.user,
|
|
1744
|
+
action: 'authfail',
|
|
1745
|
+
method: this._authMethod
|
|
1746
|
+
}, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
|
|
1959
1747
|
return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
|
|
1960
1748
|
}
|
|
1961
1749
|
this._responseActions.push(str => {
|
|
1962
1750
|
this._actionAUTHComplete(str, isRetry, callback);
|
|
1963
1751
|
});
|
|
1964
|
-
this._sendCommand(
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */')
|
|
1968
|
-
);
|
|
1752
|
+
this._sendCommand('AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
|
|
1753
|
+
// Hidden for logs
|
|
1754
|
+
'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */'));
|
|
1969
1755
|
});
|
|
1970
1756
|
}
|
|
1971
|
-
|
|
1972
1757
|
/**
|
|
1973
1758
|
*
|
|
1974
|
-
* @param
|
|
1975
|
-
* @
|
|
1759
|
+
* @param command
|
|
1760
|
+
* @internal
|
|
1976
1761
|
*/
|
|
1977
1762
|
_isDestroyedMessage(command) {
|
|
1978
1763
|
if (this._destroyed) {
|
|
1979
1764
|
return 'Cannot ' + command + ' - smtp connection is already destroyed.';
|
|
1980
1765
|
}
|
|
1981
|
-
|
|
1982
1766
|
if (this._socket) {
|
|
1983
1767
|
if (this._socket.destroyed) {
|
|
1984
1768
|
return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
|
|
1985
1769
|
}
|
|
1986
|
-
|
|
1987
1770
|
if (!this._socket.writable) {
|
|
1988
1771
|
return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
|
|
1989
1772
|
}
|
|
1990
1773
|
}
|
|
1991
1774
|
}
|
|
1992
|
-
|
|
1775
|
+
/** @internal */
|
|
1993
1776
|
_getHostname() {
|
|
1994
1777
|
// defaul hostname is machine hostname or [IP]
|
|
1995
1778
|
let defaultHostname;
|
|
1996
1779
|
try {
|
|
1997
1780
|
defaultHostname = os.hostname() || '';
|
|
1998
|
-
}
|
|
1781
|
+
}
|
|
1782
|
+
catch (_err) {
|
|
1999
1783
|
// fails on windows 7
|
|
2000
1784
|
defaultHostname = 'localhost';
|
|
2001
1785
|
}
|
|
2002
|
-
|
|
2003
1786
|
// ignore if not FQDN
|
|
2004
1787
|
if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
|
|
2005
1788
|
defaultHostname = '[127.0.0.1]';
|
|
2006
1789
|
}
|
|
2007
|
-
|
|
2008
1790
|
// IP should be enclosed in []
|
|
2009
1791
|
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
|
2010
1792
|
defaultHostname = '[' + defaultHostname + ']';
|
|
2011
1793
|
}
|
|
2012
|
-
|
|
2013
1794
|
return defaultHostname;
|
|
2014
1795
|
}
|
|
2015
1796
|
}
|
|
2016
|
-
|
|
2017
|
-
module.exports = SMTPConnection;
|
|
1797
|
+
export default SMTPConnection;
|