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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/addressparser/index.js +2 -5
  3. package/dist/cjs/dkim/index.js +1 -3
  4. package/dist/cjs/errors.d.ts +19 -0
  5. package/dist/cjs/errors.js +14 -1
  6. package/dist/cjs/fetch/cookies.js +2 -4
  7. package/dist/cjs/fetch/index.js +2 -8
  8. package/dist/cjs/mail-composer/index.js +42 -9
  9. package/dist/cjs/mailer/mail-message.js +5 -6
  10. package/dist/cjs/mime-funcs/index.d.ts +18 -1
  11. package/dist/cjs/mime-funcs/index.js +43 -1
  12. package/dist/cjs/mime-funcs/mime-types.js +3 -12
  13. package/dist/cjs/mime-node/index.d.ts +34 -1
  14. package/dist/cjs/mime-node/index.js +40 -15
  15. package/dist/cjs/package-info.d.ts +1 -1
  16. package/dist/cjs/package-info.js +1 -1
  17. package/dist/cjs/qp/index.js +3 -12
  18. package/dist/cjs/sendmail-transport/index.js +1 -3
  19. package/dist/cjs/shared/index.js +2 -6
  20. package/dist/cjs/smtp-connection/http-proxy-client.js +1 -1
  21. package/dist/cjs/smtp-connection/index.d.ts +6 -0
  22. package/dist/cjs/smtp-connection/index.js +15 -12
  23. package/dist/cjs/smtp-connection/server-message-id.d.ts +14 -0
  24. package/dist/cjs/smtp-connection/server-message-id.js +53 -0
  25. package/dist/cjs/smtp-pool/index.d.ts +11 -0
  26. package/dist/cjs/smtp-pool/index.js +25 -1
  27. package/dist/cjs/smtp-pool/pool-resource.js +2 -5
  28. package/dist/esm/addressparser/index.js +2 -5
  29. package/dist/esm/dkim/index.js +1 -3
  30. package/dist/esm/errors.d.ts +19 -0
  31. package/dist/esm/errors.js +12 -0
  32. package/dist/esm/fetch/cookies.js +2 -4
  33. package/dist/esm/fetch/index.js +2 -8
  34. package/dist/esm/mail-composer/index.js +42 -9
  35. package/dist/esm/mailer/mail-message.js +5 -6
  36. package/dist/esm/mime-funcs/index.d.ts +18 -1
  37. package/dist/esm/mime-funcs/index.js +43 -1
  38. package/dist/esm/mime-funcs/mime-types.js +3 -12
  39. package/dist/esm/mime-node/index.d.ts +34 -1
  40. package/dist/esm/mime-node/index.js +40 -15
  41. package/dist/esm/package-info.d.ts +1 -1
  42. package/dist/esm/package-info.js +1 -1
  43. package/dist/esm/qp/index.js +3 -12
  44. package/dist/esm/sendmail-transport/index.js +1 -3
  45. package/dist/esm/shared/index.js +2 -6
  46. package/dist/esm/smtp-connection/http-proxy-client.js +1 -1
  47. package/dist/esm/smtp-connection/index.d.ts +6 -0
  48. package/dist/esm/smtp-connection/index.js +16 -13
  49. package/dist/esm/smtp-connection/server-message-id.d.ts +14 -0
  50. package/dist/esm/smtp-connection/server-message-id.js +50 -0
  51. package/dist/esm/smtp-pool/index.d.ts +11 -0
  52. package/dist/esm/smtp-pool/index.js +25 -1
  53. package/dist/esm/smtp-pool/pool-resource.js +2 -5
  54. package/package.json +11 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.2.1](https://github.com/nodemailer/nodemailer/compare/v10.2.0...v10.2.1) (2026-10-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * drop the comments of a Content-Type or Content-Disposition value with the stripComments option ([b3052b7](https://github.com/nodemailer/nodemailer/commit/b3052b7fe2e49b51dd66d6c7814a072e885f1ac8))
9
+
10
+ ## [10.2.0](https://github.com/nodemailer/nodemailer/compare/v10.1.7...v10.2.0) (2026-10-21)
11
+
12
+
13
+ ### Features
14
+
15
+ * add info.serverMessageId from the final 250 reply ([108dddd](https://github.com/nodemailer/nodemailer/commit/108dddd88d9d71f708630b7aba7fa2a7d2f14076))
16
+ * add pendingCount() to the SMTP pool ([6cffb70](https://github.com/nodemailer/nodemailer/commit/6cffb7037269a6012d992c263a8927fcaf0563f2))
17
+ * make header value encoding public with MimeNode encodeHeaderValue() ([3b048c7](https://github.com/nodemailer/nodemailer/commit/3b048c7a0fb60f946985b09b3143b0680194e47b))
18
+ * mark SMTP errors as permanent or transient with err.permanent ([6a20e2a](https://github.com/nodemailer/nodemailer/commit/6a20e2ae109eeb5662235d72d1e5a2d38515997a))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * label an ASCII message/rfc822 attachment 7bit instead of 8bit ([ad66fa1](https://github.com/nodemailer/nodemailer/commit/ad66fa1a048135867a629bcb7b6dc308ee06a945))
24
+ * report the recipients an LMTP server refuses after DATA in rejectedErrors ([dd64eb3](https://github.com/nodemailer/nodemailer/commit/dd64eb3171e3ab5629828084d3944a7859790968))
25
+
3
26
  ## [10.1.7](https://github.com/nodemailer/nodemailer/compare/v10.1.6...v10.1.7) (2026-10-18)
4
27
 
5
28
 
@@ -693,9 +693,7 @@ function _handleAddress(tokens, from, to) {
693
693
  // A run that is a quoted local part with its '@' and domain is an addr-spec of
694
694
  // its own, with the quoting put back on, and takes its turn like any other
695
695
  const local = data.quotedLocalAt[i];
696
- const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
697
- ? _quoteLocalPart(data.text[i])
698
- : '';
696
+ const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
699
697
  if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
700
698
  data.address = [quotedLocal || candidates[i]];
701
699
  data.text.splice(i, 1);
@@ -1020,8 +1018,7 @@ class Tokenizer {
1020
1018
  return;
1021
1019
  }
1022
1020
  if (this.node < 0) {
1023
- this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
1024
- (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
1021
+ this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
1025
1022
  }
1026
1023
  if (chr === '\n') {
1027
1024
  // Convert newlines to spaces. Carriage return is ignored as \r and \n usually
@@ -25,9 +25,7 @@ class DKIMSigner {
25
25
  this.chunks = [];
26
26
  this.chunklen = 0;
27
27
  this.readPos = 0;
28
- this.cachePath = this.cacheDir
29
- ? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex'))
30
- : false;
28
+ this.cachePath = this.cacheDir ? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex')) : false;
31
29
  this.cache = false;
32
30
  this.headers = false;
33
31
  this.bodyHash = false;
@@ -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 */
@@ -10,7 +10,7 @@
10
10
  * err.code = errors.ECONNECTION;
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.ERR_ACCESS_DENIED = exports.isTransientError = exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
13
+ exports.ERR_ACCESS_DENIED = exports.isPermanentResponseCode = exports.isTransientError = exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
14
14
  /**
15
15
  * Error code descriptions for documentation and debugging
16
16
  */
@@ -73,6 +73,19 @@ exports.EFETCH = 'EFETCH';
73
73
  */
74
74
  const isTransientError = (err) => err.responseCode === 421 || err.code === exports.ECONNECTION || err.code === exports.ESOCKET || err.code === exports.ETIMEDOUT;
75
75
  exports.isTransientError = isTransientError;
76
+ /**
77
+ * Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
78
+ * message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
79
+ * and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
80
+ * says the session was in the wrong state for the command rather than that the message was
81
+ * refused, so the same message can go through on a new connection. 2yz and 3yz replies are
82
+ * only an error when the client expected something else, and are not permanent either.
83
+ *
84
+ * @param responseCode SMTP reply code
85
+ * @returns true for a permanent rejection
86
+ */
87
+ const isPermanentResponseCode = (responseCode) => responseCode >= 500 && responseCode < 600 && responseCode !== 503;
88
+ exports.isPermanentResponseCode = isPermanentResponseCode;
76
89
  /**
77
90
  * Code Node.js sets on an error when its permission model (`--permission`) denies an
78
91
  * operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
@@ -198,16 +198,14 @@ class Cookies {
198
198
  // check if hostname matches
199
199
  // .foo.com also matches subdomains, foo.com does not
200
200
  if (urlparts.hostname !== cookie.domain &&
201
- (cookie.domain.charAt(0) !== '.' ||
202
- ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
201
+ (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
203
202
  return false;
204
203
  }
205
204
  // check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
206
205
  // identical paths match, otherwise the cookie path must be a directory prefix
207
206
  const pathname = urlparts.pathname || '/';
208
207
  const cookiePath = cookie.path;
209
- const pathMatches = pathname === cookiePath ||
210
- (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
208
+ const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
211
209
  if (!pathMatches) {
212
210
  return false;
213
211
  }
@@ -262,11 +262,7 @@ function nmfetch(url, options) {
262
262
  }
263
263
  });
264
264
  }
265
- if (parsed.protocol === 'https:' &&
266
- parsed.hostname &&
267
- parsed.hostname !== reqOptions.host &&
268
- !node_net_1.default.isIP(parsed.hostname) &&
269
- !reqOptions.servername) {
265
+ if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !node_net_1.default.isIP(parsed.hostname) && !reqOptions.servername) {
270
266
  reqOptions.servername = parsed.hostname;
271
267
  }
272
268
  try {
@@ -369,9 +365,7 @@ function nmfetch(url, options) {
369
365
  // downgrade would also put them on the wire in cleartext). Strip sensitive
370
366
  // request headers so an attacker who controls the redirect target cannot
371
367
  // harvest them.
372
- const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
373
- redirectParsed.hostname !== parsed.hostname ||
374
- fetchPort(redirectParsed) !== fetchPort(parsed);
368
+ const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
375
369
  if (options.headers && crossOrigin) {
376
370
  const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
377
371
  Object.keys(options.headers).forEach(key => {
@@ -54,6 +54,40 @@ function isContentObject(value) {
54
54
  const content = value;
55
55
  return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
56
56
  }
57
+ /**
58
+ * Tells whether attachment content is a string or a Buffer that 7bit can describe: ASCII
59
+ * only, no NUL and no line over the 998 octets of RFC 5322. Anything else, a stream or a
60
+ * file path included, can not be checked here
61
+ */
62
+ function fitsSevenBit(content) {
63
+ let at;
64
+ let length;
65
+ if (typeof content === 'string') {
66
+ at = i => content.charCodeAt(i);
67
+ length = content.length;
68
+ }
69
+ else if (Buffer.isBuffer(content)) {
70
+ at = i => content[i];
71
+ length = content.length;
72
+ }
73
+ else {
74
+ return false;
75
+ }
76
+ let lineLength = 0;
77
+ for (let i = 0; i < length; i++) {
78
+ const c = at(i);
79
+ if (c === 0 || c > 0x7f) {
80
+ return false;
81
+ }
82
+ if (c === 0x0a || c === 0x0d) {
83
+ lineLength = 0;
84
+ }
85
+ else if (++lineLength > 998) {
86
+ return false;
87
+ }
88
+ }
89
+ return true;
90
+ }
57
91
  class MailComposer {
58
92
  constructor(mail) {
59
93
  this.mail = mail || {};
@@ -136,8 +170,11 @@ class MailComposer {
136
170
  contentTransferEncoding = attachment.contentTransferEncoding;
137
171
  }
138
172
  else if (isMessageNode) {
139
- // the content might include non-ASCII bytes but at this point we do not know it yet
140
- contentTransferEncoding = '8bit';
173
+ // a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
174
+ // the content as it is. A stream or a file might hold non-ASCII bytes that can not be
175
+ // checked here, so only content at hand that fits 7bit is labelled as such
176
+ // content with an `encoding` is decoded later, so what it decodes to is not known here either
177
+ contentTransferEncoding = !attachment.encoding && fitsSevenBit(attachment.content) ? '7bit' : '8bit';
141
178
  }
142
179
  else {
143
180
  contentTransferEncoding = 'base64'; // the default
@@ -152,9 +189,7 @@ class MailComposer {
152
189
  }
153
190
  else if (!isMessageNode && attachment.filename !== false) {
154
191
  // a backslash separates as well, so a Windows path does not put the sender's directories in the headers
155
- data.filename =
156
- (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
157
- 'attachment-' + (i + 1);
192
+ data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
158
193
  if (data.filename.indexOf('.') < 0) {
159
194
  data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
160
195
  }
@@ -315,8 +350,7 @@ class MailComposer {
315
350
  if (this.mail.icalEvent) {
316
351
  eventObject = Object.assign({}, this._getIcalEvent());
317
352
  eventObject.filename = false;
318
- eventObject.contentType =
319
- 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
353
+ eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
320
354
  if (!eventObject.headers) {
321
355
  eventObject.headers = {};
322
356
  }
@@ -344,8 +378,7 @@ class MailComposer {
344
378
  alternative = this._processDataUrl(alternative);
345
379
  }
346
380
  const data = {
347
- contentType: alternative.contentType ||
348
- mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
381
+ contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
349
382
  contentTransferEncoding: alternative.contentTransferEncoding
350
383
  };
351
384
  if (alternative.filename) {
@@ -109,8 +109,7 @@ class MailMessage {
109
109
  if (!attachment.filename) {
110
110
  // a backslash separates as well, so a Windows path does not put the sender's directories in the headers
111
111
  attachment.filename =
112
- (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
113
- 'attachment-' + (i + 1);
112
+ (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
114
113
  if (attachment.filename.indexOf('.') < 0) {
115
114
  attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
116
115
  }
@@ -245,7 +244,7 @@ class MailMessage {
245
244
  value = (value && value.value) || value;
246
245
  if (value) {
247
246
  if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
248
- value = this.message._encodeHeaderValue(key, value);
247
+ value = this.message.encodeHeaderValue(key, value);
249
248
  }
250
249
  data.normalizedHeaders[key] = value;
251
250
  }
@@ -257,10 +256,10 @@ class MailMessage {
257
256
  });
258
257
  }
259
258
  if (data.references) {
260
- data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
259
+ data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
261
260
  }
262
261
  if (data.inReplyTo) {
263
- data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
262
+ data.normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', data.inReplyTo);
264
263
  }
265
264
  return callback(null, data);
266
265
  });
@@ -329,7 +328,7 @@ class MailMessage {
329
328
  comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
330
329
  // List-ID expects a bare domain-like identifier, so strip the
331
330
  // scheme prefix that _formatListUrl adds or passes through
332
- return ((value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<'));
331
+ return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<');
333
332
  }
334
333
  // List-*: <http://domain> (comment)
335
334
  // the ctext specials go out as quoted-pairs, otherwise a ")" closes the
@@ -107,6 +107,22 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
107
107
  * @return A list of encoded keys and headers
108
108
  */
109
109
  export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
110
+ /**
111
+ * Options of parseHeaderValue()
112
+ */
113
+ export interface ParseHeaderValueOptions {
114
+ /**
115
+ * Drop the RFC 822 comments outside quoted strings, which the MIME headers allow and
116
+ * which are not part of the value: 'boundary=b (comment)' names the boundary 'b'. A
117
+ * parenthesis right behind the '=' of a parameter or behind a token character of its
118
+ * value continues the token, as it does for Python and postal-mime, so 'Invoice(1).pdf'
119
+ * keeps its name and 'boundary=(c)b' names the boundary '(c)b'. A comment that never
120
+ * closes runs to the end of the value, unless a parameter follows it, in which case it is
121
+ * content and the parameter survives. Comments are kept by default for the sake of the
122
+ * callers of this exported function, not because keeping them is ever the better reading
123
+ */
124
+ stripComments?: boolean | undefined;
125
+ }
110
126
  /**
111
127
  * Parses a header value with key=value arguments into a structured
112
128
  * object.
@@ -120,9 +136,10 @@ export declare function buildHeaderParam(key: string, data: string | Buffer, max
120
136
  * }
121
137
  *
122
138
  * @param str Header value
139
+ * @param options Options, see ParseHeaderValueOptions
123
140
  * @return Header value as a parsed structure
124
141
  */
125
- export declare function parseHeaderValue(str: string): ParsedHeaderValue;
142
+ export declare function parseHeaderValue(str: string, options?: ParseHeaderValueOptions): ParsedHeaderValue;
126
143
  /**
127
144
  * Returns file extension for a content type string. If no suitable extensions
128
145
  * are found, 'bin' is used as the default extension
@@ -414,6 +414,26 @@ function buildHeaderParam(key, data, maxLength) {
414
414
  * goes in. Whitespace used to come off it only because the value was trimmed first.
415
415
  */
416
416
  const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
417
+ /**
418
+ * Offset of the ')' that closes the RFC 822 comment opening at `start`, or -1 when the
419
+ * comment never closes. Comments nest, and a backslash escapes the character after it
420
+ */
421
+ function _findCommentEnd(str, start) {
422
+ let depth = 0;
423
+ for (let i = start, len = str.length; i < len; i++) {
424
+ const c = str.charAt(i);
425
+ if (c === '\\') {
426
+ i++;
427
+ }
428
+ else if (c === '(') {
429
+ depth++;
430
+ }
431
+ else if (c === ')' && --depth === 0) {
432
+ return i;
433
+ }
434
+ }
435
+ return -1;
436
+ }
417
437
  /**
418
438
  * Whether a string can be a header parameter name.
419
439
  *
@@ -440,13 +460,16 @@ function _isParamName(name) {
440
460
  * }
441
461
  *
442
462
  * @param str Header value
463
+ * @param options Options, see ParseHeaderValueOptions
443
464
  * @return Header value as a parsed structure
444
465
  */
445
- function parseHeaderValue(str) {
466
+ function parseHeaderValue(str, options) {
446
467
  const response = {
447
468
  value: '',
448
469
  params: {}
449
470
  };
471
+ // Cleared again by an unclosed '(', see the loop
472
+ let stripComments = !!options?.stripComments;
450
473
  // A duplicated parameter resolves to its first occurrence, the way a duplicated header
451
474
  // does. Letting the last one win disagrees with the receivers that take the first, and
452
475
  // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
@@ -508,6 +531,25 @@ function parseHeaderValue(str) {
508
531
  };
509
532
  for (let i = 0, len = str.length; i < len; i++) {
510
533
  chr = str.charAt(i);
534
+ // With stripComments, an RFC 822 comment stands anywhere outside a quoted string but
535
+ // right behind a token it would continue, see ParseHeaderValueOptions. In the value
536
+ // of a parameter, no pending whitespace means the last significant character was the
537
+ // '=' or a character of the token
538
+ if (stripComments && chr === '(' && !quote && !quoteClosed && !(stage === 'value' && key !== false && !pendingSpace)) {
539
+ let end = _findCommentEnd(str, i);
540
+ if (end < 0 && str.indexOf(';', i) < 0) {
541
+ // a comment that never closes runs to the end of the value, unless a
542
+ // parameter follows it, which is content then and survives
543
+ end = len - 1;
544
+ }
545
+ if (end >= 0) {
546
+ i = end;
547
+ continue;
548
+ }
549
+ // looking for a closing ')' again would scan a run of unclosed '(' to its end
550
+ // once for each of them
551
+ stripComments = false;
552
+ }
511
553
  if (stage === 'key') {
512
554
  if (chr === '=') {
513
555
  key = takeValue().trim();
@@ -1104,10 +1104,7 @@ const extensions = new Map([
1104
1104
  ['bdm', 'application/vnd.syncml.dm+wbxml'],
1105
1105
  ['bed', 'application/vnd.realvnc.bed'],
1106
1106
  ['bh2', 'application/vnd.fujitsu.oasysprs'],
1107
- [
1108
- 'bin',
1109
- ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
1110
- ],
1107
+ ['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
1111
1108
  ['bm', 'image/bmp'],
1112
1109
  ['bmi', 'application/vnd.bmi'],
1113
1110
  ['bmp', ['image/bmp', 'image/x-windows-bmp']],
@@ -1152,10 +1149,7 @@ const extensions = new Map([
1152
1149
  ['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
1153
1150
  ['cil', 'application/vnd.ms-artgalry'],
1154
1151
  ['cla', 'application/vnd.claymore'],
1155
- [
1156
- 'class',
1157
- ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
1158
- ],
1152
+ ['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
1159
1153
  ['clkk', 'application/vnd.crick.clicker.keyboard'],
1160
1154
  ['clkp', 'application/vnd.crick.clicker.palette'],
1161
1155
  ['clkt', 'application/vnd.crick.clicker.template'],
@@ -1760,10 +1754,7 @@ const extensions = new Map([
1760
1754
  ['sbml', 'application/sbml+xml'],
1761
1755
  ['sc', 'application/vnd.ibm.secure-container'],
1762
1756
  ['scd', 'application/x-msschedule'],
1763
- [
1764
- 'scm',
1765
- ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
1766
- ],
1757
+ ['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
1767
1758
  ['scq', 'application/scvp-cv-request'],
1768
1759
  ['scs', 'application/scvp-cv-response'],
1769
1760
  ['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;
@@ -155,9 +155,7 @@ function exceedsDomainLimits(domain) {
155
155
  */
156
156
  function normalizeDomain(domain, toUnicode) {
157
157
  // domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
158
- if (typeof node_url_1.default.domainToASCII === 'function' &&
159
- typeof node_url_1.default.domainToUnicode === 'function' &&
160
- !URL_PARSER_UNSAFE.test(domain)) {
158
+ if (typeof node_url_1.default.domainToASCII === 'function' && typeof node_url_1.default.domainToUnicode === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
161
159
  // The U-label form is decoded from the A-label form rather than from the input:
162
160
  // Deno's domainToASCII returns an empty string for a label it cannot decode, while
163
161
  // its domainToUnicode returns the label with U+FFFD in place of the bad part
@@ -656,23 +654,24 @@ class MimeNode {
656
654
  return;
657
655
  }
658
656
  switch (header.key) {
657
+ // The comments of a MIME header are not part of its value and are dropped, so a
658
+ // caller supplied 'boundary=b (comment)' is the boundary b it names, and the
659
+ // header goes out without the comment
659
660
  case 'Content-Disposition':
660
- structured = mimeFuncs.parseHeaderValue(value);
661
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
661
662
  if (this.filename) {
662
663
  structured.params.filename = this.filename;
663
664
  }
664
665
  value = mimeFuncs.buildHeaderValue(structured);
665
666
  break;
666
667
  case 'Content-Type':
667
- structured = mimeFuncs.parseHeaderValue(value);
668
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
668
669
  // the type token decides multipart and charset below, so clean it before
669
670
  // those run and not just on the way out, otherwise a control char makes
670
671
  // the checks miss and the header ends up claiming a type it is not set up for
671
672
  structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
672
673
  this._handleContentType(structured);
673
- if (structured.value.match(/^text\/plain\b/) &&
674
- typeof this.content === 'string' &&
675
- /[\u0080-\uFFFF]/.test(this.content)) {
674
+ if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
676
675
  structured.params.charset = 'utf-8';
677
676
  }
678
677
  if (this.filename) {
@@ -713,7 +712,7 @@ class MimeNode {
713
712
  }
714
713
  break;
715
714
  }
716
- value = this._encodeHeaderValue(key, value);
715
+ value = this.encodeHeaderValue(key, value);
717
716
  // skip empty lines
718
717
  if (!(value || '').toString().trim()) {
719
718
  return;
@@ -1331,8 +1330,7 @@ class MimeNode {
1331
1330
  // where it came from. MimeNode is exported and subclassable, so the generator is
1332
1331
  // not necessarily the one below. Stripping twice costs nothing, it is idempotent.
1333
1332
  const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
1334
- this.boundary = structured.params.boundary =
1335
- declared || _stripBoundaryControls(this._generateBoundary());
1333
+ this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
1336
1334
  }
1337
1335
  else {
1338
1336
  this.boundary = false;
@@ -1350,13 +1348,19 @@ class MimeNode {
1350
1348
  return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
1351
1349
  }
1352
1350
  /**
1353
- * Encodes a header value for use in the generated rfc2822 email.
1351
+ * Encodes a header value the way this node encodes it in the generated message:
1352
+ * address headers are punycoded and mime encoded, message id headers get their
1353
+ * angle brackets, a Date object is formatted and any other value is mime encoded
1354
+ * when it holds non-ascii or control characters (using the node's `textEncoding`).
1354
1355
  *
1355
- * @param key Header key
1356
+ * The result is a single unfolded line without the key. Fold it before writing it
1357
+ * into a message, `key + ': ' + value` folded at 76 characters is what the node does.
1358
+ *
1359
+ * @param key Header key, case insensitive
1356
1360
  * @param value Header value
1357
- * @internal
1361
+ * @returns Encoded header value
1358
1362
  */
1359
- _encodeHeaderValue(key, value) {
1363
+ encodeHeaderValue(key, value) {
1360
1364
  key = this._normalizeHeaderKey(key);
1361
1365
  switch (key) {
1362
1366
  // Structured headers
@@ -1421,6 +1425,27 @@ class MimeNode {
1421
1425
  return this._encodeHeaderText(value);
1422
1426
  }
1423
1427
  }
1428
+ /**
1429
+ * Encodes a header value without a node at hand. Same as the instance method of a
1430
+ * node created with the given options.
1431
+ *
1432
+ * @param key Header key, case insensitive
1433
+ * @param value Header value
1434
+ * @param [options] Encoding options
1435
+ * @param [options.textEncoding] either 'Q' (the default) or 'B'
1436
+ * @returns Encoded header value
1437
+ */
1438
+ static encodeHeaderValue(key, value, options) {
1439
+ return new MimeNode(false, { textEncoding: options?.textEncoding }).encodeHeaderValue(key, value);
1440
+ }
1441
+ /**
1442
+ * Alias of encodeHeaderValue, kept for callers that used the method before it was public
1443
+ *
1444
+ * @internal
1445
+ */
1446
+ _encodeHeaderValue(key, value) {
1447
+ return this.encodeHeaderValue(key, value);
1448
+ }
1424
1449
  /**
1425
1450
  * Rebuilds address object using punycode and other adjustments
1426
1451
  *
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.1.7";
2
+ export declare const version = "10.2.1";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = 'nodemailer';
6
- exports.version = '10.1.7';
6
+ exports.version = '10.2.1';
7
7
  exports.homepage = 'https://nodemailer.com/';