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
|
@@ -365,6 +365,26 @@ export function buildHeaderParam(key, data, maxLength) {
|
|
|
365
365
|
* goes in. Whitespace used to come off it only because the value was trimmed first.
|
|
366
366
|
*/
|
|
367
367
|
const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
|
|
368
|
+
/**
|
|
369
|
+
* Offset of the ')' that closes the RFC 822 comment opening at `start`, or -1 when the
|
|
370
|
+
* comment never closes. Comments nest, and a backslash escapes the character after it
|
|
371
|
+
*/
|
|
372
|
+
function _findCommentEnd(str, start) {
|
|
373
|
+
let depth = 0;
|
|
374
|
+
for (let i = start, len = str.length; i < len; i++) {
|
|
375
|
+
const c = str.charAt(i);
|
|
376
|
+
if (c === '\\') {
|
|
377
|
+
i++;
|
|
378
|
+
}
|
|
379
|
+
else if (c === '(') {
|
|
380
|
+
depth++;
|
|
381
|
+
}
|
|
382
|
+
else if (c === ')' && --depth === 0) {
|
|
383
|
+
return i;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return -1;
|
|
387
|
+
}
|
|
368
388
|
/**
|
|
369
389
|
* Whether a string can be a header parameter name.
|
|
370
390
|
*
|
|
@@ -391,13 +411,16 @@ function _isParamName(name) {
|
|
|
391
411
|
* }
|
|
392
412
|
*
|
|
393
413
|
* @param str Header value
|
|
414
|
+
* @param options Options, see ParseHeaderValueOptions
|
|
394
415
|
* @return Header value as a parsed structure
|
|
395
416
|
*/
|
|
396
|
-
export function parseHeaderValue(str) {
|
|
417
|
+
export function parseHeaderValue(str, options) {
|
|
397
418
|
const response = {
|
|
398
419
|
value: '',
|
|
399
420
|
params: {}
|
|
400
421
|
};
|
|
422
|
+
// Cleared again by an unclosed '(', see the loop
|
|
423
|
+
let stripComments = !!options?.stripComments;
|
|
401
424
|
// A duplicated parameter resolves to its first occurrence, the way a duplicated header
|
|
402
425
|
// does. Letting the last one win disagrees with the receivers that take the first, and
|
|
403
426
|
// the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
|
|
@@ -459,6 +482,25 @@ export function parseHeaderValue(str) {
|
|
|
459
482
|
};
|
|
460
483
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
461
484
|
chr = str.charAt(i);
|
|
485
|
+
// With stripComments, an RFC 822 comment stands anywhere outside a quoted string but
|
|
486
|
+
// right behind a token it would continue, see ParseHeaderValueOptions. In the value
|
|
487
|
+
// of a parameter, no pending whitespace means the last significant character was the
|
|
488
|
+
// '=' or a character of the token
|
|
489
|
+
if (stripComments && chr === '(' && !quote && !quoteClosed && !(stage === 'value' && key !== false && !pendingSpace)) {
|
|
490
|
+
let end = _findCommentEnd(str, i);
|
|
491
|
+
if (end < 0 && str.indexOf(';', i) < 0) {
|
|
492
|
+
// a comment that never closes runs to the end of the value, unless a
|
|
493
|
+
// parameter follows it, which is content then and survives
|
|
494
|
+
end = len - 1;
|
|
495
|
+
}
|
|
496
|
+
if (end >= 0) {
|
|
497
|
+
i = end;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// looking for a closing ')' again would scan a run of unclosed '(' to its end
|
|
501
|
+
// once for each of them
|
|
502
|
+
stripComments = false;
|
|
503
|
+
}
|
|
462
504
|
if (stage === 'key') {
|
|
463
505
|
if (chr === '=') {
|
|
464
506
|
key = takeValue().trim();
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -618,23 +616,24 @@ class MimeNode {
|
|
|
618
616
|
return;
|
|
619
617
|
}
|
|
620
618
|
switch (header.key) {
|
|
619
|
+
// The comments of a MIME header are not part of its value and are dropped, so a
|
|
620
|
+
// caller supplied 'boundary=b (comment)' is the boundary b it names, and the
|
|
621
|
+
// header goes out without the comment
|
|
621
622
|
case 'Content-Disposition':
|
|
622
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
623
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
623
624
|
if (this.filename) {
|
|
624
625
|
structured.params.filename = this.filename;
|
|
625
626
|
}
|
|
626
627
|
value = mimeFuncs.buildHeaderValue(structured);
|
|
627
628
|
break;
|
|
628
629
|
case 'Content-Type':
|
|
629
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
630
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
630
631
|
// the type token decides multipart and charset below, so clean it before
|
|
631
632
|
// those run and not just on the way out, otherwise a control char makes
|
|
632
633
|
// the checks miss and the header ends up claiming a type it is not set up for
|
|
633
634
|
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
|
634
635
|
this._handleContentType(structured);
|
|
635
|
-
if (structured.value.match(/^text\/plain\b/) &&
|
|
636
|
-
typeof this.content === 'string' &&
|
|
637
|
-
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
636
|
+
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
|
638
637
|
structured.params.charset = 'utf-8';
|
|
639
638
|
}
|
|
640
639
|
if (this.filename) {
|
|
@@ -675,7 +674,7 @@ class MimeNode {
|
|
|
675
674
|
}
|
|
676
675
|
break;
|
|
677
676
|
}
|
|
678
|
-
value = this.
|
|
677
|
+
value = this.encodeHeaderValue(key, value);
|
|
679
678
|
// skip empty lines
|
|
680
679
|
if (!(value || '').toString().trim()) {
|
|
681
680
|
return;
|
|
@@ -1293,8 +1292,7 @@ class MimeNode {
|
|
|
1293
1292
|
// where it came from. MimeNode is exported and subclassable, so the generator is
|
|
1294
1293
|
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
|
1295
1294
|
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
|
1296
|
-
this.boundary = structured.params.boundary =
|
|
1297
|
-
declared || _stripBoundaryControls(this._generateBoundary());
|
|
1295
|
+
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
|
1298
1296
|
}
|
|
1299
1297
|
else {
|
|
1300
1298
|
this.boundary = false;
|
|
@@ -1312,13 +1310,19 @@ class MimeNode {
|
|
|
1312
1310
|
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
|
1313
1311
|
}
|
|
1314
1312
|
/**
|
|
1315
|
-
* Encodes a header value
|
|
1313
|
+
* Encodes a header value the way this node encodes it in the generated message:
|
|
1314
|
+
* address headers are punycoded and mime encoded, message id headers get their
|
|
1315
|
+
* angle brackets, a Date object is formatted and any other value is mime encoded
|
|
1316
|
+
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
|
1316
1317
|
*
|
|
1317
|
-
*
|
|
1318
|
+
* The result is a single unfolded line without the key. Fold it before writing it
|
|
1319
|
+
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
|
1320
|
+
*
|
|
1321
|
+
* @param key Header key, case insensitive
|
|
1318
1322
|
* @param value Header value
|
|
1319
|
-
* @
|
|
1323
|
+
* @returns Encoded header value
|
|
1320
1324
|
*/
|
|
1321
|
-
|
|
1325
|
+
encodeHeaderValue(key, value) {
|
|
1322
1326
|
key = this._normalizeHeaderKey(key);
|
|
1323
1327
|
switch (key) {
|
|
1324
1328
|
// Structured headers
|
|
@@ -1383,6 +1387,27 @@ class MimeNode {
|
|
|
1383
1387
|
return this._encodeHeaderText(value);
|
|
1384
1388
|
}
|
|
1385
1389
|
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Encodes a header value without a node at hand. Same as the instance method of a
|
|
1392
|
+
* node created with the given options.
|
|
1393
|
+
*
|
|
1394
|
+
* @param key Header key, case insensitive
|
|
1395
|
+
* @param value Header value
|
|
1396
|
+
* @param [options] Encoding options
|
|
1397
|
+
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
|
1398
|
+
* @returns Encoded header value
|
|
1399
|
+
*/
|
|
1400
|
+
static encodeHeaderValue(key, value, options) {
|
|
1401
|
+
return new MimeNode(false, { textEncoding: options?.textEncoding }).encodeHeaderValue(key, value);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Alias of encodeHeaderValue, kept for callers that used the method before it was public
|
|
1405
|
+
*
|
|
1406
|
+
* @internal
|
|
1407
|
+
*/
|
|
1408
|
+
_encodeHeaderValue(key, value) {
|
|
1409
|
+
return this.encodeHeaderValue(key, value);
|
|
1410
|
+
}
|
|
1386
1411
|
/**
|
|
1387
1412
|
* Rebuilds address object using punycode and other adjustments
|
|
1388
1413
|
*
|
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 => {
|