nodemailer 10.1.7 → 10.2.0
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 +16 -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/mime-types.js +3 -12
- package/dist/cjs/mime-node/index.d.ts +34 -1
- package/dist/cjs/mime-node/index.js +35 -13
- 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/mime-types.js +3 -12
- package/dist/esm/mime-node/index.d.ts +34 -1
- package/dist/esm/mime-node/index.js +35 -13
- 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 +4 -2
|
@@ -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
|
|
@@ -1097,10 +1097,7 @@ const extensions = new Map([
|
|
|
1097
1097
|
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
|
1098
1098
|
['bed', 'application/vnd.realvnc.bed'],
|
|
1099
1099
|
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
|
1100
|
-
[
|
|
1101
|
-
'bin',
|
|
1102
|
-
['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
|
|
1103
|
-
],
|
|
1100
|
+
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
|
|
1104
1101
|
['bm', 'image/bmp'],
|
|
1105
1102
|
['bmi', 'application/vnd.bmi'],
|
|
1106
1103
|
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
|
@@ -1145,10 +1142,7 @@ const extensions = new Map([
|
|
|
1145
1142
|
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
|
1146
1143
|
['cil', 'application/vnd.ms-artgalry'],
|
|
1147
1144
|
['cla', 'application/vnd.claymore'],
|
|
1148
|
-
[
|
|
1149
|
-
'class',
|
|
1150
|
-
['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
|
|
1151
|
-
],
|
|
1145
|
+
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
|
|
1152
1146
|
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
|
1153
1147
|
['clkp', 'application/vnd.crick.clicker.palette'],
|
|
1154
1148
|
['clkt', 'application/vnd.crick.clicker.template'],
|
|
@@ -1753,10 +1747,7 @@ const extensions = new Map([
|
|
|
1753
1747
|
['sbml', 'application/sbml+xml'],
|
|
1754
1748
|
['sc', 'application/vnd.ibm.secure-container'],
|
|
1755
1749
|
['scd', 'application/x-msschedule'],
|
|
1756
|
-
[
|
|
1757
|
-
'scm',
|
|
1758
|
-
['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
|
|
1759
|
-
],
|
|
1750
|
+
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
|
|
1760
1751
|
['scq', 'application/scvp-cv-request'],
|
|
1761
1752
|
['scs', 'application/scvp-cv-response'],
|
|
1762
1753
|
['sct', 'text/scriptlet'],
|
|
@@ -58,6 +58,13 @@ export interface MimeNodePreparedHeaderValue {
|
|
|
58
58
|
* objects (for the address headers), a prepared value object, or a list of these
|
|
59
59
|
*/
|
|
60
60
|
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Options for the static MimeNode.encodeHeaderValue
|
|
63
|
+
*/
|
|
64
|
+
export interface MimeNodeHeaderEncodingOptions {
|
|
65
|
+
/** Either 'Q' (the default) or 'B' */
|
|
66
|
+
textEncoding?: string | undefined;
|
|
67
|
+
}
|
|
61
68
|
/**
|
|
62
69
|
* A single header, the shape the headers are stored in and one of the list forms
|
|
63
70
|
* setHeader and addHeader accept
|
|
@@ -319,6 +326,31 @@ declare class MimeNode {
|
|
|
319
326
|
* @param raw Raw MIME contents
|
|
320
327
|
*/
|
|
321
328
|
setRaw(raw: MimeNodeContent): this;
|
|
329
|
+
/**
|
|
330
|
+
* Encodes a header value the way this node encodes it in the generated message:
|
|
331
|
+
* address headers are punycoded and mime encoded, message id headers get their
|
|
332
|
+
* angle brackets, a Date object is formatted and any other value is mime encoded
|
|
333
|
+
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
|
334
|
+
*
|
|
335
|
+
* The result is a single unfolded line without the key. Fold it before writing it
|
|
336
|
+
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
|
337
|
+
*
|
|
338
|
+
* @param key Header key, case insensitive
|
|
339
|
+
* @param value Header value
|
|
340
|
+
* @returns Encoded header value
|
|
341
|
+
*/
|
|
342
|
+
encodeHeaderValue(key: string, value: MimeNodeHeaderValue): string;
|
|
343
|
+
/**
|
|
344
|
+
* Encodes a header value without a node at hand. Same as the instance method of a
|
|
345
|
+
* node created with the given options.
|
|
346
|
+
*
|
|
347
|
+
* @param key Header key, case insensitive
|
|
348
|
+
* @param value Header value
|
|
349
|
+
* @param [options] Encoding options
|
|
350
|
+
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
|
351
|
+
* @returns Encoded header value
|
|
352
|
+
*/
|
|
353
|
+
static encodeHeaderValue(key: string, value: MimeNodeHeaderValue, options?: MimeNodeHeaderEncodingOptions): string;
|
|
322
354
|
}
|
|
323
355
|
/**
|
|
324
356
|
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
|
|
@@ -327,7 +359,8 @@ declare namespace MimeNode {
|
|
|
327
359
|
type Options = MimeNodeOptions;
|
|
328
360
|
type Addresses = MimeNodeAddresses;
|
|
329
361
|
type Envelope = MimeNodeEnvelope;
|
|
362
|
+
type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
|
|
330
363
|
}
|
|
331
364
|
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
|
332
|
-
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope };
|
|
365
|
+
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope, MimeNodeHeaderEncodingOptions as HeaderEncodingOptions };
|
|
333
366
|
export default MimeNode;
|