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
|
@@ -117,9 +117,7 @@ function exceedsDomainLimits(domain) {
|
|
|
117
117
|
*/
|
|
118
118
|
function normalizeDomain(domain, toUnicode) {
|
|
119
119
|
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
|
120
|
-
if (typeof urlModule.domainToASCII === 'function' &&
|
|
121
|
-
typeof urlModule.domainToUnicode === 'function' &&
|
|
122
|
-
!URL_PARSER_UNSAFE.test(domain)) {
|
|
120
|
+
if (typeof urlModule.domainToASCII === 'function' && typeof urlModule.domainToUnicode === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
|
123
121
|
// The U-label form is decoded from the A-label form rather than from the input:
|
|
124
122
|
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
|
125
123
|
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
|
@@ -632,9 +630,7 @@ class MimeNode {
|
|
|
632
630
|
// the checks miss and the header ends up claiming a type it is not set up for
|
|
633
631
|
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
|
634
632
|
this._handleContentType(structured);
|
|
635
|
-
if (structured.value.match(/^text\/plain\b/) &&
|
|
636
|
-
typeof this.content === 'string' &&
|
|
637
|
-
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
633
|
+
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
|
638
634
|
structured.params.charset = 'utf-8';
|
|
639
635
|
}
|
|
640
636
|
if (this.filename) {
|
|
@@ -675,7 +671,7 @@ class MimeNode {
|
|
|
675
671
|
}
|
|
676
672
|
break;
|
|
677
673
|
}
|
|
678
|
-
value = this.
|
|
674
|
+
value = this.encodeHeaderValue(key, value);
|
|
679
675
|
// skip empty lines
|
|
680
676
|
if (!(value || '').toString().trim()) {
|
|
681
677
|
return;
|
|
@@ -1293,8 +1289,7 @@ class MimeNode {
|
|
|
1293
1289
|
// where it came from. MimeNode is exported and subclassable, so the generator is
|
|
1294
1290
|
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
|
1295
1291
|
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
|
1296
|
-
this.boundary = structured.params.boundary =
|
|
1297
|
-
declared || _stripBoundaryControls(this._generateBoundary());
|
|
1292
|
+
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
|
1298
1293
|
}
|
|
1299
1294
|
else {
|
|
1300
1295
|
this.boundary = false;
|
|
@@ -1312,13 +1307,19 @@ class MimeNode {
|
|
|
1312
1307
|
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
|
1313
1308
|
}
|
|
1314
1309
|
/**
|
|
1315
|
-
* Encodes a header value
|
|
1310
|
+
* Encodes a header value the way this node encodes it in the generated message:
|
|
1311
|
+
* address headers are punycoded and mime encoded, message id headers get their
|
|
1312
|
+
* angle brackets, a Date object is formatted and any other value is mime encoded
|
|
1313
|
+
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
|
1316
1314
|
*
|
|
1317
|
-
*
|
|
1315
|
+
* The result is a single unfolded line without the key. Fold it before writing it
|
|
1316
|
+
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
|
1317
|
+
*
|
|
1318
|
+
* @param key Header key, case insensitive
|
|
1318
1319
|
* @param value Header value
|
|
1319
|
-
* @
|
|
1320
|
+
* @returns Encoded header value
|
|
1320
1321
|
*/
|
|
1321
|
-
|
|
1322
|
+
encodeHeaderValue(key, value) {
|
|
1322
1323
|
key = this._normalizeHeaderKey(key);
|
|
1323
1324
|
switch (key) {
|
|
1324
1325
|
// Structured headers
|
|
@@ -1383,6 +1384,27 @@ class MimeNode {
|
|
|
1383
1384
|
return this._encodeHeaderText(value);
|
|
1384
1385
|
}
|
|
1385
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Encodes a header value without a node at hand. Same as the instance method of a
|
|
1389
|
+
* node created with the given options.
|
|
1390
|
+
*
|
|
1391
|
+
* @param key Header key, case insensitive
|
|
1392
|
+
* @param value Header value
|
|
1393
|
+
* @param [options] Encoding options
|
|
1394
|
+
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
|
1395
|
+
* @returns Encoded header value
|
|
1396
|
+
*/
|
|
1397
|
+
static encodeHeaderValue(key, value, options) {
|
|
1398
|
+
return new MimeNode(false, { textEncoding: options?.textEncoding }).encodeHeaderValue(key, value);
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Alias of encodeHeaderValue, kept for callers that used the method before it was public
|
|
1402
|
+
*
|
|
1403
|
+
* @internal
|
|
1404
|
+
*/
|
|
1405
|
+
_encodeHeaderValue(key, value) {
|
|
1406
|
+
return this.encodeHeaderValue(key, value);
|
|
1407
|
+
}
|
|
1386
1408
|
/**
|
|
1387
1409
|
* Rebuilds address object using punycode and other adjustments
|
|
1388
1410
|
*
|
package/dist/esm/package-info.js
CHANGED
package/dist/esm/qp/index.js
CHANGED
|
@@ -45,15 +45,9 @@ function encodeBytes(buffer, next, binary, previous) {
|
|
|
45
45
|
for (let i = 0; i < len; i++) {
|
|
46
46
|
const ord = buffer[i];
|
|
47
47
|
const following = i + 1 < len ? buffer[i + 1] : next;
|
|
48
|
-
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
|
49
|
-
? ord === 0x0d
|
|
50
|
-
? following === 0x0a
|
|
51
|
-
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
|
52
|
-
: true;
|
|
48
|
+
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (ord === 0x0d ? following === 0x0a : (i > 0 ? buffer[i - 1] : previous) === 0x0d) : true;
|
|
53
49
|
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
|
54
|
-
if (QP_LITERAL[ord] &&
|
|
55
|
-
lineBreakByte &&
|
|
56
|
-
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
50
|
+
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
57
51
|
output[pos++] = ord;
|
|
58
52
|
continue;
|
|
59
53
|
}
|
|
@@ -114,10 +108,7 @@ export function wrap(str, lineLength) {
|
|
|
114
108
|
line = line.substr(0, line.length - match[0].length);
|
|
115
109
|
}
|
|
116
110
|
// ensure that utf-8 sequences are not split
|
|
117
|
-
while (line.length > 3 &&
|
|
118
|
-
line.length < len - pos &&
|
|
119
|
-
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
|
120
|
-
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
111
|
+
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
121
112
|
code = parseInt(match[0].substr(1, 2), 16);
|
|
122
113
|
if (code < 128) {
|
|
123
114
|
break;
|
|
@@ -71,9 +71,7 @@ class SendmailTransport {
|
|
|
71
71
|
return done(err);
|
|
72
72
|
}
|
|
73
73
|
// force -i to keep single dots
|
|
74
|
-
const args = this.args
|
|
75
|
-
? ['-i'].concat(this.args).concat(envelope.to)
|
|
76
|
-
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
74
|
+
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
77
75
|
const callback = (err) => {
|
|
78
76
|
if (returned) {
|
|
79
77
|
// ignore any additional responses, already done
|
package/dist/esm/shared/index.js
CHANGED
|
@@ -217,9 +217,7 @@ export const resolveHostname = (options, callback) => {
|
|
|
217
217
|
return respond(err);
|
|
218
218
|
}
|
|
219
219
|
// Get all supported addresses from dns.lookup
|
|
220
|
-
const supportedAddresses = addresses
|
|
221
|
-
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
|
222
|
-
: [];
|
|
220
|
+
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
|
223
221
|
if (addresses && addresses.length && !supportedAddresses.length) {
|
|
224
222
|
// there are addresses but none can be used
|
|
225
223
|
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
|
@@ -549,9 +547,7 @@ export const assign = function (...args) {
|
|
|
549
547
|
if (isProtoKey(key)) {
|
|
550
548
|
return;
|
|
551
549
|
}
|
|
552
|
-
if (['tls', 'auth'].includes(key) &&
|
|
553
|
-
source[key] &&
|
|
554
|
-
typeof source[key] === 'object') {
|
|
550
|
+
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
|
555
551
|
// tls and auth are special keys that need to be enumerated separately
|
|
556
552
|
// other objects are passed as is. Enumerating is a copy of user supplied
|
|
557
553
|
// keys just like the loop above, so it gets the same treatment
|
|
@@ -9,7 +9,7 @@ import * as errors from '../errors.js';
|
|
|
9
9
|
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
|
10
10
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
11
11
|
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
|
12
|
-
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
12
|
+
const unbracket = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
|
13
13
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
|
14
14
|
if (typeof tlsOptions === 'function') {
|
|
15
15
|
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
|
|
@@ -7,7 +7,8 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import DataStream from './data-stream.js';
|
|
8
8
|
import { PassThrough } from 'node:stream';
|
|
9
9
|
import * as shared from '../shared/index.js';
|
|
10
|
-
import {
|
|
10
|
+
import { parseServerMessageId } from './server-message-id.js';
|
|
11
|
+
import { ERR_ACCESS_DENIED, isPermanentResponseCode, isTransientError } from '../errors.js';
|
|
11
12
|
// default timeout values in ms
|
|
12
13
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
|
13
14
|
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
|
@@ -361,9 +362,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
361
362
|
if (this._destroyed || this._closing) {
|
|
362
363
|
return;
|
|
363
364
|
}
|
|
364
|
-
const connectFn = secure
|
|
365
|
-
? tls.connect
|
|
366
|
-
: net.connect;
|
|
365
|
+
const connectFn = secure ? tls.connect : net.connect;
|
|
367
366
|
try {
|
|
368
367
|
this._socket = connectFn(opts, () => this._onConnect());
|
|
369
368
|
this._setupConnectionHandlers();
|
|
@@ -701,6 +700,16 @@ class SMTPConnection extends EventEmitter {
|
|
|
701
700
|
result.messageTime = Date.now() - envelopeTime;
|
|
702
701
|
result.messageSize = stream.outByteCount;
|
|
703
702
|
result.response = str;
|
|
703
|
+
const serverMessageId = parseServerMessageId(result.response, this.options.host);
|
|
704
|
+
if (serverMessageId) {
|
|
705
|
+
result.serverMessageId = serverMessageId;
|
|
706
|
+
}
|
|
707
|
+
// an LMTP server answers the message once per recipient, so recipients can be
|
|
708
|
+
// refused after the envelope info was built without any rejected errors on it
|
|
709
|
+
const rejectedErrors = this._envelope?.rejectedErrors;
|
|
710
|
+
if (!result.rejectedErrors && rejectedErrors && rejectedErrors.length) {
|
|
711
|
+
result.rejectedErrors = rejectedErrors;
|
|
712
|
+
}
|
|
704
713
|
return callback(null, result);
|
|
705
714
|
});
|
|
706
715
|
if (typeof message.pipe === 'function') {
|
|
@@ -928,6 +937,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
928
937
|
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
|
929
938
|
if (responseCode) {
|
|
930
939
|
err.responseCode = responseCode;
|
|
940
|
+
err.permanent = isPermanentResponseCode(responseCode);
|
|
931
941
|
}
|
|
932
942
|
if (command) {
|
|
933
943
|
err.command = command;
|
|
@@ -970,12 +980,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
970
980
|
this.close();
|
|
971
981
|
return;
|
|
972
982
|
}
|
|
973
|
-
if (!failureResponse &&
|
|
974
|
-
this.stage === 'connected' &&
|
|
975
|
-
!this._responseActions.length &&
|
|
976
|
-
!this._pendingSend &&
|
|
977
|
-
!this._destroyed &&
|
|
978
|
-
!this._closing) {
|
|
983
|
+
if (!failureResponse && this.stage === 'connected' && !this._responseActions.length && !this._pendingSend && !this._destroyed && !this._closing) {
|
|
979
984
|
// nothing was waiting for the server, so this is the server ending an idle session
|
|
980
985
|
// (usually after its idle timeout), not a failure
|
|
981
986
|
this.logger.info({
|
|
@@ -1196,9 +1201,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1196
1201
|
const args = [];
|
|
1197
1202
|
let useSmtpUtf8 = false;
|
|
1198
1203
|
this._envelope = (envelope || {});
|
|
1199
|
-
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
|
1200
|
-
.toString()
|
|
1201
|
-
.trim();
|
|
1204
|
+
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
|
1202
1205
|
this._envelope.to = []
|
|
1203
1206
|
.concat(this._envelope.to || [])
|
|
1204
1207
|
.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,50 @@
|
|
|
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
|
+
// Microsoft 365 and Outlook.com: "250 2.0.0 OK <id@host.prod.outlook.com> [Hostname=...]"
|
|
8
|
+
const OUTLOOK_REPLY = /^250 2\.0\.0 OK (<[^<>\s]+\.prod\.outlook\.com>)/i;
|
|
9
|
+
// Amazon SES: "250 Ok 0100018b5c3c0a4f-...-000000", the local part of the Message-ID it sets
|
|
10
|
+
const SES_REPLY = /^250 Ok ([0-9a-f]+(?:-[0-9a-f]+)+)$/i;
|
|
11
|
+
// an AWS region name such as us-east-1 or ap-southeast-2
|
|
12
|
+
const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
|
13
|
+
/**
|
|
14
|
+
* Finds the AWS region an SES SMTP endpoint is in from its hostname, such as
|
|
15
|
+
* email-smtp.eu-west-1.amazonaws.com or a VPC endpoint name under it
|
|
16
|
+
*
|
|
17
|
+
* @param host SMTP hostname
|
|
18
|
+
* @returns Region name, or undefined for a host outside amazonaws.com and awsapps.com
|
|
19
|
+
*/
|
|
20
|
+
function sesRegion(host) {
|
|
21
|
+
const labels = host.toLowerCase().replace(/\.$/, '').split('.');
|
|
22
|
+
const domain = labels.slice(-2).join('.');
|
|
23
|
+
if (domain !== 'amazonaws.com' && domain !== 'awsapps.com') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return labels.slice(0, -2).find(label => AWS_REGION.test(label));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reads the server assigned Message-ID out of the final reply to a message
|
|
30
|
+
*
|
|
31
|
+
* @param response Final server reply for the message
|
|
32
|
+
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
|
33
|
+
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
|
34
|
+
*/
|
|
35
|
+
export function parseServerMessageId(response, host) {
|
|
36
|
+
const reply = (response || '').toString().trim();
|
|
37
|
+
const outlook = reply.match(OUTLOOK_REPLY);
|
|
38
|
+
if (outlook) {
|
|
39
|
+
return outlook[1];
|
|
40
|
+
}
|
|
41
|
+
const ses = reply.match(SES_REPLY);
|
|
42
|
+
if (ses && host) {
|
|
43
|
+
const region = sesRegion(host);
|
|
44
|
+
if (region) {
|
|
45
|
+
// the SES Message-ID domain is email.amazonses.com in us-east-1 and the region elsewhere
|
|
46
|
+
return '<' + ses[1].toLowerCase() + '@' + (region === 'us-east-1' ? 'email' : region) + '.amazonses.com>';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
@@ -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
|
*
|
|
@@ -62,6 +62,7 @@ class SMTPPool extends EventEmitter {
|
|
|
62
62
|
this._queue = [];
|
|
63
63
|
this._connections = [];
|
|
64
64
|
this._connectionCounter = 0;
|
|
65
|
+
this._pending = 0;
|
|
65
66
|
this.idling = true;
|
|
66
67
|
setImmediate(() => {
|
|
67
68
|
if (this.idling) {
|
|
@@ -94,10 +95,20 @@ class SMTPPool extends EventEmitter {
|
|
|
94
95
|
setImmediate(() => callback(err));
|
|
95
96
|
return false;
|
|
96
97
|
}
|
|
98
|
+
// counted until the callback runs, whichever path runs it: delivered, failed, requeued
|
|
99
|
+
// until it gives up, or drained by close()
|
|
100
|
+
let counted = true;
|
|
101
|
+
this._pending++;
|
|
97
102
|
this._queue.push({
|
|
98
103
|
mail,
|
|
99
104
|
requeueAttempts: 0,
|
|
100
|
-
callback
|
|
105
|
+
callback: (err, info) => {
|
|
106
|
+
if (counted) {
|
|
107
|
+
counted = false;
|
|
108
|
+
this._pending--;
|
|
109
|
+
}
|
|
110
|
+
callback(err, info);
|
|
111
|
+
}
|
|
101
112
|
});
|
|
102
113
|
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
|
103
114
|
this.idling = false;
|
|
@@ -441,6 +452,19 @@ class SMTPPool extends EventEmitter {
|
|
|
441
452
|
isIdle() {
|
|
442
453
|
return this.idling;
|
|
443
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
|
457
|
+
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
|
458
|
+
* pool can take another message, it is already true while a message is still being sent,
|
|
459
|
+
* so use this to check that nothing would be lost by closing the pool. A message that is
|
|
460
|
+
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
|
461
|
+
* it is no longer counted by the time its callback runs.
|
|
462
|
+
*
|
|
463
|
+
* @returns Number of queued and in-flight messages
|
|
464
|
+
*/
|
|
465
|
+
pendingCount() {
|
|
466
|
+
return this._pending;
|
|
467
|
+
}
|
|
444
468
|
verify(callback) {
|
|
445
469
|
let promise;
|
|
446
470
|
if (!callback) {
|
|
@@ -19,8 +19,7 @@ export default class PoolResource extends EventEmitter {
|
|
|
19
19
|
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
|
20
20
|
case 'OAUTH2': {
|
|
21
21
|
const oauth2 = new XOAuth2(this.options.auth, this.logger);
|
|
22
|
-
oauth2.provisionCallback =
|
|
23
|
-
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
22
|
+
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
24
23
|
this.auth = {
|
|
25
24
|
type: 'OAUTH2',
|
|
26
25
|
user: this.options.auth.user,
|
|
@@ -208,9 +207,7 @@ export default class PoolResource extends EventEmitter {
|
|
|
208
207
|
this.connection.close();
|
|
209
208
|
return;
|
|
210
209
|
}
|
|
211
|
-
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
|
212
|
-
err.responseCode !== 421 &&
|
|
213
|
-
!this.connection._destroyed) {
|
|
210
|
+
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
|
214
211
|
// The server refused this message, the connection itself is fine. Reset the
|
|
215
212
|
// session and keep using it instead of opening a new one
|
|
216
213
|
this.connection.reset(resetErr => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/nodemailer.js",
|
|
@@ -128,6 +128,8 @@
|
|
|
128
128
|
"test": "node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
|
129
129
|
"pretest:coverage": "npm run build",
|
|
130
130
|
"test:coverage": "c8 node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
|
131
|
+
"pretest:cjs": "npm run build",
|
|
132
|
+
"test:cjs": "NODE_OPTIONS=--no-experimental-require-module node test/package/cjs-smoke.cjs",
|
|
131
133
|
"pretest:bun": "npm run build",
|
|
132
134
|
"test:bun": "bun test --timeout 60000 $(find ./test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
|
133
135
|
"format": "prettier --write \"**/*.{js,cjs,ts,json,md}\"",
|
|
@@ -155,7 +157,7 @@
|
|
|
155
157
|
"@types/node": "20.19.43",
|
|
156
158
|
"bunyan": "1.8.15",
|
|
157
159
|
"c8": "12.0.0",
|
|
158
|
-
"eslint": "10.
|
|
160
|
+
"eslint": "10.13.0",
|
|
159
161
|
"eslint-config-prettier": "10.1.8",
|
|
160
162
|
"globals": "17.13.0",
|
|
161
163
|
"libbase64": "1.3.2",
|