nodemailer 10.1.7 → 10.2.1
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/CHANGELOG.md +23 -0
- package/dist/cjs/addressparser/index.js +2 -5
- package/dist/cjs/dkim/index.js +1 -3
- package/dist/cjs/errors.d.ts +19 -0
- package/dist/cjs/errors.js +14 -1
- package/dist/cjs/fetch/cookies.js +2 -4
- package/dist/cjs/fetch/index.js +2 -8
- package/dist/cjs/mail-composer/index.js +42 -9
- package/dist/cjs/mailer/mail-message.js +5 -6
- package/dist/cjs/mime-funcs/index.d.ts +18 -1
- package/dist/cjs/mime-funcs/index.js +43 -1
- package/dist/cjs/mime-funcs/mime-types.js +3 -12
- package/dist/cjs/mime-node/index.d.ts +34 -1
- package/dist/cjs/mime-node/index.js +40 -15
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/qp/index.js +3 -12
- package/dist/cjs/sendmail-transport/index.js +1 -3
- package/dist/cjs/shared/index.js +2 -6
- package/dist/cjs/smtp-connection/http-proxy-client.js +1 -1
- package/dist/cjs/smtp-connection/index.d.ts +6 -0
- package/dist/cjs/smtp-connection/index.js +15 -12
- package/dist/cjs/smtp-connection/server-message-id.d.ts +14 -0
- package/dist/cjs/smtp-connection/server-message-id.js +53 -0
- package/dist/cjs/smtp-pool/index.d.ts +11 -0
- package/dist/cjs/smtp-pool/index.js +25 -1
- package/dist/cjs/smtp-pool/pool-resource.js +2 -5
- package/dist/esm/addressparser/index.js +2 -5
- package/dist/esm/dkim/index.js +1 -3
- package/dist/esm/errors.d.ts +19 -0
- package/dist/esm/errors.js +12 -0
- package/dist/esm/fetch/cookies.js +2 -4
- package/dist/esm/fetch/index.js +2 -8
- package/dist/esm/mail-composer/index.js +42 -9
- package/dist/esm/mailer/mail-message.js +5 -6
- package/dist/esm/mime-funcs/index.d.ts +18 -1
- package/dist/esm/mime-funcs/index.js +43 -1
- package/dist/esm/mime-funcs/mime-types.js +3 -12
- package/dist/esm/mime-node/index.d.ts +34 -1
- package/dist/esm/mime-node/index.js +40 -15
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/qp/index.js +3 -12
- package/dist/esm/sendmail-transport/index.js +1 -3
- package/dist/esm/shared/index.js +2 -6
- package/dist/esm/smtp-connection/http-proxy-client.js +1 -1
- package/dist/esm/smtp-connection/index.d.ts +6 -0
- package/dist/esm/smtp-connection/index.js +16 -13
- package/dist/esm/smtp-connection/server-message-id.d.ts +14 -0
- package/dist/esm/smtp-connection/server-message-id.js +50 -0
- package/dist/esm/smtp-pool/index.d.ts +11 -0
- package/dist/esm/smtp-pool/index.js +25 -1
- package/dist/esm/smtp-pool/pool-resource.js +2 -5
- package/package.json +11 -9
package/dist/cjs/qp/index.js
CHANGED
|
@@ -50,15 +50,9 @@ function encodeBytes(buffer, next, binary, previous) {
|
|
|
50
50
|
for (let i = 0; i < len; i++) {
|
|
51
51
|
const ord = buffer[i];
|
|
52
52
|
const following = i + 1 < len ? buffer[i + 1] : next;
|
|
53
|
-
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
|
54
|
-
? ord === 0x0d
|
|
55
|
-
? following === 0x0a
|
|
56
|
-
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
|
57
|
-
: true;
|
|
53
|
+
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (ord === 0x0d ? following === 0x0a : (i > 0 ? buffer[i - 1] : previous) === 0x0d) : true;
|
|
58
54
|
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
|
59
|
-
if (QP_LITERAL[ord] &&
|
|
60
|
-
lineBreakByte &&
|
|
61
|
-
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
55
|
+
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
62
56
|
output[pos++] = ord;
|
|
63
57
|
continue;
|
|
64
58
|
}
|
|
@@ -119,10 +113,7 @@ function wrap(str, lineLength) {
|
|
|
119
113
|
line = line.substr(0, line.length - match[0].length);
|
|
120
114
|
}
|
|
121
115
|
// ensure that utf-8 sequences are not split
|
|
122
|
-
while (line.length > 3 &&
|
|
123
|
-
line.length < len - pos &&
|
|
124
|
-
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
|
125
|
-
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
116
|
+
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
126
117
|
code = parseInt(match[0].substr(1, 2), 16);
|
|
127
118
|
if (code < 128) {
|
|
128
119
|
break;
|
|
@@ -109,9 +109,7 @@ class SendmailTransport {
|
|
|
109
109
|
return done(err);
|
|
110
110
|
}
|
|
111
111
|
// force -i to keep single dots
|
|
112
|
-
const args = this.args
|
|
113
|
-
? ['-i'].concat(this.args).concat(envelope.to)
|
|
114
|
-
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
112
|
+
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
115
113
|
const callback = (err) => {
|
|
116
114
|
if (returned) {
|
|
117
115
|
// ignore any additional responses, already done
|
package/dist/cjs/shared/index.js
CHANGED
|
@@ -263,9 +263,7 @@ const resolveHostname = (options, callback) => {
|
|
|
263
263
|
return respond(err);
|
|
264
264
|
}
|
|
265
265
|
// Get all supported addresses from dns.lookup
|
|
266
|
-
const supportedAddresses = addresses
|
|
267
|
-
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
|
268
|
-
: [];
|
|
266
|
+
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
|
269
267
|
if (addresses && addresses.length && !supportedAddresses.length) {
|
|
270
268
|
// there are addresses but none can be used
|
|
271
269
|
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
|
@@ -601,9 +599,7 @@ const assign = function (...args) {
|
|
|
601
599
|
if ((0, objects_js_1.isProtoKey)(key)) {
|
|
602
600
|
return;
|
|
603
601
|
}
|
|
604
|
-
if (['tls', 'auth'].includes(key) &&
|
|
605
|
-
source[key] &&
|
|
606
|
-
typeof source[key] === 'object') {
|
|
602
|
+
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
|
607
603
|
// tls and auth are special keys that need to be enumerated separately
|
|
608
604
|
// other objects are passed as is. Enumerating is a copy of user supplied
|
|
609
605
|
// keys just like the loop above, so it gets the same treatment
|
|
@@ -47,7 +47,7 @@ const errors = __importStar(require("../errors.js"));
|
|
|
47
47
|
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
|
48
48
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
49
49
|
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
|
50
|
-
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
50
|
+
const unbracket = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
|
51
51
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
|
52
52
|
if (typeof tlsOptions === 'function') {
|
|
53
53
|
callback = tlsOptions;
|
|
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
|
|
245
245
|
messageSize: number;
|
|
246
246
|
/** Final server response for the message */
|
|
247
247
|
response: string;
|
|
248
|
+
/**
|
|
249
|
+
* Message-ID the server gave the message, with angle brackets, when its final reply names
|
|
250
|
+
* one in a known format (Microsoft 365 and Outlook.com, Amazon SES). Such a server replaces
|
|
251
|
+
* the Message-ID header, so the delivered copy carries this id instead of messageId
|
|
252
|
+
*/
|
|
253
|
+
serverMessageId?: string | undefined;
|
|
248
254
|
}
|
|
249
255
|
/**
|
|
250
256
|
* Callback for send(), receives the result once the server accepted the message. The error
|
|
@@ -45,6 +45,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
45
45
|
const data_stream_js_1 = __importDefault(require("./data-stream.js"));
|
|
46
46
|
const node_stream_1 = require("node:stream");
|
|
47
47
|
const shared = __importStar(require("../shared/index.js"));
|
|
48
|
+
const server_message_id_js_1 = require("./server-message-id.js");
|
|
48
49
|
const errors_js_1 = require("../errors.js");
|
|
49
50
|
// default timeout values in ms
|
|
50
51
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
|
@@ -399,9 +400,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
399
400
|
if (this._destroyed || this._closing) {
|
|
400
401
|
return;
|
|
401
402
|
}
|
|
402
|
-
const connectFn = secure
|
|
403
|
-
? node_tls_1.default.connect
|
|
404
|
-
: node_net_1.default.connect;
|
|
403
|
+
const connectFn = secure ? node_tls_1.default.connect : node_net_1.default.connect;
|
|
405
404
|
try {
|
|
406
405
|
this._socket = connectFn(opts, () => this._onConnect());
|
|
407
406
|
this._setupConnectionHandlers();
|
|
@@ -739,6 +738,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
739
738
|
result.messageTime = Date.now() - envelopeTime;
|
|
740
739
|
result.messageSize = stream.outByteCount;
|
|
741
740
|
result.response = str;
|
|
741
|
+
const serverMessageId = (0, server_message_id_js_1.parseServerMessageId)(result.response, this.options.host);
|
|
742
|
+
if (serverMessageId) {
|
|
743
|
+
result.serverMessageId = serverMessageId;
|
|
744
|
+
}
|
|
745
|
+
// an LMTP server answers the message once per recipient, so recipients can be
|
|
746
|
+
// refused after the envelope info was built without any rejected errors on it
|
|
747
|
+
const rejectedErrors = this._envelope?.rejectedErrors;
|
|
748
|
+
if (!result.rejectedErrors && rejectedErrors && rejectedErrors.length) {
|
|
749
|
+
result.rejectedErrors = rejectedErrors;
|
|
750
|
+
}
|
|
742
751
|
return callback(null, result);
|
|
743
752
|
});
|
|
744
753
|
if (typeof message.pipe === 'function') {
|
|
@@ -966,6 +975,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
966
975
|
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
|
967
976
|
if (responseCode) {
|
|
968
977
|
err.responseCode = responseCode;
|
|
978
|
+
err.permanent = (0, errors_js_1.isPermanentResponseCode)(responseCode);
|
|
969
979
|
}
|
|
970
980
|
if (command) {
|
|
971
981
|
err.command = command;
|
|
@@ -1008,12 +1018,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1008
1018
|
this.close();
|
|
1009
1019
|
return;
|
|
1010
1020
|
}
|
|
1011
|
-
if (!failureResponse &&
|
|
1012
|
-
this.stage === 'connected' &&
|
|
1013
|
-
!this._responseActions.length &&
|
|
1014
|
-
!this._pendingSend &&
|
|
1015
|
-
!this._destroyed &&
|
|
1016
|
-
!this._closing) {
|
|
1021
|
+
if (!failureResponse && this.stage === 'connected' && !this._responseActions.length && !this._pendingSend && !this._destroyed && !this._closing) {
|
|
1017
1022
|
// nothing was waiting for the server, so this is the server ending an idle session
|
|
1018
1023
|
// (usually after its idle timeout), not a failure
|
|
1019
1024
|
this.logger.info({
|
|
@@ -1234,9 +1239,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1234
1239
|
const args = [];
|
|
1235
1240
|
let useSmtpUtf8 = false;
|
|
1236
1241
|
this._envelope = (envelope || {});
|
|
1237
|
-
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
|
1238
|
-
.toString()
|
|
1239
|
-
.trim();
|
|
1242
|
+
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
|
1240
1243
|
this._envelope.to = []
|
|
1241
1244
|
.concat(this._envelope.to || [])
|
|
1242
1245
|
.map(to => ((to && to.address) || to || '').toString().trim());
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
|
3
|
+
* servers replace the Message-ID header of a submitted message with one of their own, and
|
|
4
|
+
* the copy the recipients and the sent folder get carries that one instead of the id the
|
|
5
|
+
* message was built with. Only replies in a format known to carry such an id are read.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Reads the server assigned Message-ID out of the final reply to a message
|
|
9
|
+
*
|
|
10
|
+
* @param response Final server reply for the message
|
|
11
|
+
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
|
12
|
+
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseServerMessageId(response: string | undefined, host?: string): string | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
|
4
|
+
* servers replace the Message-ID header of a submitted message with one of their own, and
|
|
5
|
+
* the copy the recipients and the sent folder get carries that one instead of the id the
|
|
6
|
+
* message was built with. Only replies in a format known to carry such an id are read.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseServerMessageId = parseServerMessageId;
|
|
10
|
+
// Microsoft 365 and Outlook.com: "250 2.0.0 OK <id@host.prod.outlook.com> [Hostname=...]"
|
|
11
|
+
const OUTLOOK_REPLY = /^250 2\.0\.0 OK (<[^<>\s]+\.prod\.outlook\.com>)/i;
|
|
12
|
+
// Amazon SES: "250 Ok 0100018b5c3c0a4f-...-000000", the local part of the Message-ID it sets
|
|
13
|
+
const SES_REPLY = /^250 Ok ([0-9a-f]+(?:-[0-9a-f]+)+)$/i;
|
|
14
|
+
// an AWS region name such as us-east-1 or ap-southeast-2
|
|
15
|
+
const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
|
16
|
+
/**
|
|
17
|
+
* Finds the AWS region an SES SMTP endpoint is in from its hostname, such as
|
|
18
|
+
* email-smtp.eu-west-1.amazonaws.com or a VPC endpoint name under it
|
|
19
|
+
*
|
|
20
|
+
* @param host SMTP hostname
|
|
21
|
+
* @returns Region name, or undefined for a host outside amazonaws.com and awsapps.com
|
|
22
|
+
*/
|
|
23
|
+
function sesRegion(host) {
|
|
24
|
+
const labels = host.toLowerCase().replace(/\.$/, '').split('.');
|
|
25
|
+
const domain = labels.slice(-2).join('.');
|
|
26
|
+
if (domain !== 'amazonaws.com' && domain !== 'awsapps.com') {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return labels.slice(0, -2).find(label => AWS_REGION.test(label));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reads the server assigned Message-ID out of the final reply to a message
|
|
33
|
+
*
|
|
34
|
+
* @param response Final server reply for the message
|
|
35
|
+
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
|
36
|
+
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
|
37
|
+
*/
|
|
38
|
+
function parseServerMessageId(response, host) {
|
|
39
|
+
const reply = (response || '').toString().trim();
|
|
40
|
+
const outlook = reply.match(OUTLOOK_REPLY);
|
|
41
|
+
if (outlook) {
|
|
42
|
+
return outlook[1];
|
|
43
|
+
}
|
|
44
|
+
const ses = reply.match(SES_REPLY);
|
|
45
|
+
if (ses && host) {
|
|
46
|
+
const region = sesRegion(host);
|
|
47
|
+
if (region) {
|
|
48
|
+
// the SES Message-ID domain is email.amazonses.com in us-east-1 and the region elsewhere
|
|
49
|
+
return '<' + ses[1].toLowerCase() + '@' + (region === 'us-east-1' ? 'email' : region) + '.amazonses.com>';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -110,6 +110,17 @@ declare class SMTPPool extends EventEmitter {
|
|
|
110
110
|
* Returns true if there are free slots in the queue
|
|
111
111
|
*/
|
|
112
112
|
isIdle(): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
|
115
|
+
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
|
116
|
+
* pool can take another message, it is already true while a message is still being sent,
|
|
117
|
+
* so use this to check that nothing would be lost by closing the pool. A message that is
|
|
118
|
+
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
|
119
|
+
* it is no longer counted by the time its callback runs.
|
|
120
|
+
*
|
|
121
|
+
* @returns Number of queued and in-flight messages
|
|
122
|
+
*/
|
|
123
|
+
pendingCount(): number;
|
|
113
124
|
/**
|
|
114
125
|
* Verifies SMTP configuration
|
|
115
126
|
*
|
|
@@ -100,6 +100,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
100
100
|
this._queue = [];
|
|
101
101
|
this._connections = [];
|
|
102
102
|
this._connectionCounter = 0;
|
|
103
|
+
this._pending = 0;
|
|
103
104
|
this.idling = true;
|
|
104
105
|
setImmediate(() => {
|
|
105
106
|
if (this.idling) {
|
|
@@ -132,10 +133,20 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
132
133
|
setImmediate(() => callback(err));
|
|
133
134
|
return false;
|
|
134
135
|
}
|
|
136
|
+
// counted until the callback runs, whichever path runs it: delivered, failed, requeued
|
|
137
|
+
// until it gives up, or drained by close()
|
|
138
|
+
let counted = true;
|
|
139
|
+
this._pending++;
|
|
135
140
|
this._queue.push({
|
|
136
141
|
mail,
|
|
137
142
|
requeueAttempts: 0,
|
|
138
|
-
callback
|
|
143
|
+
callback: (err, info) => {
|
|
144
|
+
if (counted) {
|
|
145
|
+
counted = false;
|
|
146
|
+
this._pending--;
|
|
147
|
+
}
|
|
148
|
+
callback(err, info);
|
|
149
|
+
}
|
|
139
150
|
});
|
|
140
151
|
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
|
141
152
|
this.idling = false;
|
|
@@ -479,6 +490,19 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
479
490
|
isIdle() {
|
|
480
491
|
return this.idling;
|
|
481
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
|
495
|
+
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
|
496
|
+
* pool can take another message, it is already true while a message is still being sent,
|
|
497
|
+
* so use this to check that nothing would be lost by closing the pool. A message that is
|
|
498
|
+
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
|
499
|
+
* it is no longer counted by the time its callback runs.
|
|
500
|
+
*
|
|
501
|
+
* @returns Number of queued and in-flight messages
|
|
502
|
+
*/
|
|
503
|
+
pendingCount() {
|
|
504
|
+
return this._pending;
|
|
505
|
+
}
|
|
482
506
|
verify(callback) {
|
|
483
507
|
let promise;
|
|
484
508
|
if (!callback) {
|
|
@@ -57,8 +57,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
57
57
|
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
|
58
58
|
case 'OAUTH2': {
|
|
59
59
|
const oauth2 = new index_js_3.default(this.options.auth, this.logger);
|
|
60
|
-
oauth2.provisionCallback =
|
|
61
|
-
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
60
|
+
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
62
61
|
this.auth = {
|
|
63
62
|
type: 'OAUTH2',
|
|
64
63
|
user: this.options.auth.user,
|
|
@@ -246,9 +245,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
246
245
|
this.connection.close();
|
|
247
246
|
return;
|
|
248
247
|
}
|
|
249
|
-
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
|
250
|
-
err.responseCode !== 421 &&
|
|
251
|
-
!this.connection._destroyed) {
|
|
248
|
+
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
|
252
249
|
// The server refused this message, the connection itself is fine. Reset the
|
|
253
250
|
// session and keep using it instead of opening a new one
|
|
254
251
|
this.connection.reset(resetErr => {
|
|
@@ -691,9 +691,7 @@ function _handleAddress(tokens, from, to) {
|
|
|
691
691
|
// A run that is a quoted local part with its '@' and domain is an addr-spec of
|
|
692
692
|
// its own, with the quoting put back on, and takes its turn like any other
|
|
693
693
|
const local = data.quotedLocalAt[i];
|
|
694
|
-
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
|
|
695
|
-
? _quoteLocalPart(data.text[i])
|
|
696
|
-
: '';
|
|
694
|
+
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
|
|
697
695
|
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
|
698
696
|
data.address = [quotedLocal || candidates[i]];
|
|
699
697
|
data.text.splice(i, 1);
|
|
@@ -1018,8 +1016,7 @@ class Tokenizer {
|
|
|
1018
1016
|
return;
|
|
1019
1017
|
}
|
|
1020
1018
|
if (this.node < 0) {
|
|
1021
|
-
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
|
1022
|
-
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
1019
|
+
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
1023
1020
|
}
|
|
1024
1021
|
if (chr === '\n') {
|
|
1025
1022
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
package/dist/esm/dkim/index.js
CHANGED
|
@@ -20,9 +20,7 @@ class DKIMSigner {
|
|
|
20
20
|
this.chunks = [];
|
|
21
21
|
this.chunklen = 0;
|
|
22
22
|
this.readPos = 0;
|
|
23
|
-
this.cachePath = this.cacheDir
|
|
24
|
-
? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
|
|
25
|
-
: false;
|
|
23
|
+
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
|
|
26
24
|
this.cache = false;
|
|
27
25
|
this.headers = false;
|
|
28
26
|
this.bodyHash = false;
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
|
|
|
68
68
|
code?: string | undefined;
|
|
69
69
|
responseCode?: number | undefined;
|
|
70
70
|
}) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
|
73
|
+
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
|
74
|
+
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
|
75
|
+
* says the session was in the wrong state for the command rather than that the message was
|
|
76
|
+
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
|
77
|
+
* only an error when the client expected something else, and are not permanent either.
|
|
78
|
+
*
|
|
79
|
+
* @param responseCode SMTP reply code
|
|
80
|
+
* @returns true for a permanent rejection
|
|
81
|
+
*/
|
|
82
|
+
export declare const isPermanentResponseCode: (responseCode: number) => boolean;
|
|
71
83
|
/**
|
|
72
84
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
|
73
85
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
|
@@ -89,6 +101,13 @@ export interface NodemailerError extends NodeJS.ErrnoException {
|
|
|
89
101
|
response?: string | undefined;
|
|
90
102
|
/** Numeric SMTP response code */
|
|
91
103
|
responseCode?: number | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Set together with responseCode, from the reply class: true when the server rejected the
|
|
106
|
+
* message for good (5xx except 503), false when the same message may be accepted on a later
|
|
107
|
+
* attempt (4xx, 503, or an unexpected 2xx or 3xx). Not set on an error that carries no SMTP
|
|
108
|
+
* reply, such as a connection failure or missing credentials, see isPermanentResponseCode
|
|
109
|
+
*/
|
|
110
|
+
permanent?: boolean | undefined;
|
|
92
111
|
/** URL of the resource that could not be fetched */
|
|
93
112
|
sourceUrl?: string | undefined;
|
|
94
113
|
/** Recipient address the error applies to */
|
package/dist/esm/errors.js
CHANGED
|
@@ -69,6 +69,18 @@ export const EFETCH = 'EFETCH';
|
|
|
69
69
|
* Tells whether the error ended the connection rather than rejected what was sent over it
|
|
70
70
|
*/
|
|
71
71
|
export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
|
|
72
|
+
/**
|
|
73
|
+
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
|
74
|
+
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
|
75
|
+
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
|
76
|
+
* says the session was in the wrong state for the command rather than that the message was
|
|
77
|
+
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
|
78
|
+
* only an error when the client expected something else, and are not permanent either.
|
|
79
|
+
*
|
|
80
|
+
* @param responseCode SMTP reply code
|
|
81
|
+
* @returns true for a permanent rejection
|
|
82
|
+
*/
|
|
83
|
+
export const isPermanentResponseCode = (responseCode) => responseCode >= 500 && responseCode < 600 && responseCode !== 503;
|
|
72
84
|
/**
|
|
73
85
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
|
74
86
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
|
@@ -160,16 +160,14 @@ class Cookies {
|
|
|
160
160
|
// check if hostname matches
|
|
161
161
|
// .foo.com also matches subdomains, foo.com does not
|
|
162
162
|
if (urlparts.hostname !== cookie.domain &&
|
|
163
|
-
(cookie.domain.charAt(0) !== '.' ||
|
|
164
|
-
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
|
163
|
+
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
|
165
164
|
return false;
|
|
166
165
|
}
|
|
167
166
|
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
|
168
167
|
// identical paths match, otherwise the cookie path must be a directory prefix
|
|
169
168
|
const pathname = urlparts.pathname || '/';
|
|
170
169
|
const cookiePath = cookie.path;
|
|
171
|
-
const pathMatches = pathname === cookiePath ||
|
|
172
|
-
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
|
170
|
+
const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
|
173
171
|
if (!pathMatches) {
|
|
174
172
|
return false;
|
|
175
173
|
}
|
package/dist/esm/fetch/index.js
CHANGED
|
@@ -224,11 +224,7 @@ function nmfetch(url, options) {
|
|
|
224
224
|
}
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
|
-
if (parsed.protocol === 'https:' &&
|
|
228
|
-
parsed.hostname &&
|
|
229
|
-
parsed.hostname !== reqOptions.host &&
|
|
230
|
-
!net.isIP(parsed.hostname) &&
|
|
231
|
-
!reqOptions.servername) {
|
|
227
|
+
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
|
232
228
|
reqOptions.servername = parsed.hostname;
|
|
233
229
|
}
|
|
234
230
|
try {
|
|
@@ -331,9 +327,7 @@ function nmfetch(url, options) {
|
|
|
331
327
|
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
|
332
328
|
// request headers so an attacker who controls the redirect target cannot
|
|
333
329
|
// harvest them.
|
|
334
|
-
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
|
335
|
-
redirectParsed.hostname !== parsed.hostname ||
|
|
336
|
-
fetchPort(redirectParsed) !== fetchPort(parsed);
|
|
330
|
+
const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
|
|
337
331
|
if (options.headers && crossOrigin) {
|
|
338
332
|
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
|
339
333
|
Object.keys(options.headers).forEach(key => {
|
|
@@ -16,6 +16,40 @@ function isContentObject(value) {
|
|
|
16
16
|
const content = value;
|
|
17
17
|
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Tells whether attachment content is a string or a Buffer that 7bit can describe: ASCII
|
|
21
|
+
* only, no NUL and no line over the 998 octets of RFC 5322. Anything else, a stream or a
|
|
22
|
+
* file path included, can not be checked here
|
|
23
|
+
*/
|
|
24
|
+
function fitsSevenBit(content) {
|
|
25
|
+
let at;
|
|
26
|
+
let length;
|
|
27
|
+
if (typeof content === 'string') {
|
|
28
|
+
at = i => content.charCodeAt(i);
|
|
29
|
+
length = content.length;
|
|
30
|
+
}
|
|
31
|
+
else if (Buffer.isBuffer(content)) {
|
|
32
|
+
at = i => content[i];
|
|
33
|
+
length = content.length;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
let lineLength = 0;
|
|
39
|
+
for (let i = 0; i < length; i++) {
|
|
40
|
+
const c = at(i);
|
|
41
|
+
if (c === 0 || c > 0x7f) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (c === 0x0a || c === 0x0d) {
|
|
45
|
+
lineLength = 0;
|
|
46
|
+
}
|
|
47
|
+
else if (++lineLength > 998) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
19
53
|
class MailComposer {
|
|
20
54
|
constructor(mail) {
|
|
21
55
|
this.mail = mail || {};
|
|
@@ -98,8 +132,11 @@ class MailComposer {
|
|
|
98
132
|
contentTransferEncoding = attachment.contentTransferEncoding;
|
|
99
133
|
}
|
|
100
134
|
else if (isMessageNode) {
|
|
101
|
-
//
|
|
102
|
-
|
|
135
|
+
// a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
|
|
136
|
+
// the content as it is. A stream or a file might hold non-ASCII bytes that can not be
|
|
137
|
+
// checked here, so only content at hand that fits 7bit is labelled as such
|
|
138
|
+
// content with an `encoding` is decoded later, so what it decodes to is not known here either
|
|
139
|
+
contentTransferEncoding = !attachment.encoding && fitsSevenBit(attachment.content) ? '7bit' : '8bit';
|
|
103
140
|
}
|
|
104
141
|
else {
|
|
105
142
|
contentTransferEncoding = 'base64'; // the default
|
|
@@ -114,9 +151,7 @@ class MailComposer {
|
|
|
114
151
|
}
|
|
115
152
|
else if (!isMessageNode && attachment.filename !== false) {
|
|
116
153
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
|
117
|
-
data.filename =
|
|
118
|
-
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
|
119
|
-
'attachment-' + (i + 1);
|
|
154
|
+
data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
|
120
155
|
if (data.filename.indexOf('.') < 0) {
|
|
121
156
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
|
122
157
|
}
|
|
@@ -277,8 +312,7 @@ class MailComposer {
|
|
|
277
312
|
if (this.mail.icalEvent) {
|
|
278
313
|
eventObject = Object.assign({}, this._getIcalEvent());
|
|
279
314
|
eventObject.filename = false;
|
|
280
|
-
eventObject.contentType =
|
|
281
|
-
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
|
315
|
+
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
|
282
316
|
if (!eventObject.headers) {
|
|
283
317
|
eventObject.headers = {};
|
|
284
318
|
}
|
|
@@ -306,8 +340,7 @@ class MailComposer {
|
|
|
306
340
|
alternative = this._processDataUrl(alternative);
|
|
307
341
|
}
|
|
308
342
|
const data = {
|
|
309
|
-
contentType: alternative.contentType ||
|
|
310
|
-
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
|
343
|
+
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
|
311
344
|
contentTransferEncoding: alternative.contentTransferEncoding
|
|
312
345
|
};
|
|
313
346
|
if (alternative.filename) {
|
|
@@ -71,8 +71,7 @@ export default class MailMessage {
|
|
|
71
71
|
if (!attachment.filename) {
|
|
72
72
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
|
73
73
|
attachment.filename =
|
|
74
|
-
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
|
75
|
-
'attachment-' + (i + 1);
|
|
74
|
+
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
|
76
75
|
if (attachment.filename.indexOf('.') < 0) {
|
|
77
76
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
|
78
77
|
}
|
|
@@ -207,7 +206,7 @@ export default class MailMessage {
|
|
|
207
206
|
value = (value && value.value) || value;
|
|
208
207
|
if (value) {
|
|
209
208
|
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
|
210
|
-
value = this.message.
|
|
209
|
+
value = this.message.encodeHeaderValue(key, value);
|
|
211
210
|
}
|
|
212
211
|
data.normalizedHeaders[key] = value;
|
|
213
212
|
}
|
|
@@ -219,10 +218,10 @@ export default class MailMessage {
|
|
|
219
218
|
});
|
|
220
219
|
}
|
|
221
220
|
if (data.references) {
|
|
222
|
-
data.normalizedHeaders.references = this.message.
|
|
221
|
+
data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
|
|
223
222
|
}
|
|
224
223
|
if (data.inReplyTo) {
|
|
225
|
-
data.normalizedHeaders['in-reply-to'] = this.message.
|
|
224
|
+
data.normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', data.inReplyTo);
|
|
226
225
|
}
|
|
227
226
|
return callback(null, data);
|
|
228
227
|
});
|
|
@@ -291,7 +290,7 @@ export default class MailMessage {
|
|
|
291
290
|
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
|
292
291
|
// List-ID expects a bare domain-like identifier, so strip the
|
|
293
292
|
// scheme prefix that _formatListUrl adds or passes through
|
|
294
|
-
return (
|
|
293
|
+
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<');
|
|
295
294
|
}
|
|
296
295
|
// List-*: <http://domain> (comment)
|
|
297
296
|
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
|
|
@@ -107,6 +107,22 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
|
|
|
107
107
|
* @return A list of encoded keys and headers
|
|
108
108
|
*/
|
|
109
109
|
export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
|
|
110
|
+
/**
|
|
111
|
+
* Options of parseHeaderValue()
|
|
112
|
+
*/
|
|
113
|
+
export interface ParseHeaderValueOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Drop the RFC 822 comments outside quoted strings, which the MIME headers allow and
|
|
116
|
+
* which are not part of the value: 'boundary=b (comment)' names the boundary 'b'. A
|
|
117
|
+
* parenthesis right behind the '=' of a parameter or behind a token character of its
|
|
118
|
+
* value continues the token, as it does for Python and postal-mime, so 'Invoice(1).pdf'
|
|
119
|
+
* keeps its name and 'boundary=(c)b' names the boundary '(c)b'. A comment that never
|
|
120
|
+
* closes runs to the end of the value, unless a parameter follows it, in which case it is
|
|
121
|
+
* content and the parameter survives. Comments are kept by default for the sake of the
|
|
122
|
+
* callers of this exported function, not because keeping them is ever the better reading
|
|
123
|
+
*/
|
|
124
|
+
stripComments?: boolean | undefined;
|
|
125
|
+
}
|
|
110
126
|
/**
|
|
111
127
|
* Parses a header value with key=value arguments into a structured
|
|
112
128
|
* object.
|
|
@@ -120,9 +136,10 @@ export declare function buildHeaderParam(key: string, data: string | Buffer, max
|
|
|
120
136
|
* }
|
|
121
137
|
*
|
|
122
138
|
* @param str Header value
|
|
139
|
+
* @param options Options, see ParseHeaderValueOptions
|
|
123
140
|
* @return Header value as a parsed structure
|
|
124
141
|
*/
|
|
125
|
-
export declare function parseHeaderValue(str: string): ParsedHeaderValue;
|
|
142
|
+
export declare function parseHeaderValue(str: string, options?: ParseHeaderValueOptions): ParsedHeaderValue;
|
|
126
143
|
/**
|
|
127
144
|
* Returns file extension for a content type string. If no suitable extensions
|
|
128
145
|
* are found, 'bin' is used as the default extension
|