nodemailer 10.3.0 → 10.3.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.3.1](https://github.com/nodemailer/nodemailer/compare/v10.3.0...v10.3.1) (2026-10-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * apply the otp option in MailComposer too ([1d4087f](https://github.com/nodemailer/nodemailer/commit/1d4087f358d124267bddd5eb5e11a4e22fccd3d0))
9
+
3
10
  ## [10.3.0](https://github.com/nodemailer/nodemailer/compare/v10.2.1...v10.3.0) (2026-10-29)
4
11
 
5
12
 
@@ -124,7 +124,7 @@ export interface MailComposerOptions {
124
124
  headers?: MimeNodeHeaders | undefined;
125
125
  /** List-* headers, read by the mailer */
126
126
  list?: MailComposerListHeaders | undefined;
127
- /** One-time passcode for the OTP-Token header, read by the mailer */
127
+ /** One-time passcode for the OTP-Token header. An invalid value makes compile() throw an EOTP error */
128
128
  otp?: MailComposerOtp | undefined;
129
129
  /** SMTP envelope to use instead of the one generated from the headers */
130
130
  envelope?: MimeNodeEnvelopeInput | undefined;
@@ -39,6 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  const index_js_1 = __importDefault(require("../mime-node/index.js"));
41
41
  const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
42
+ const errors = __importStar(require("../errors.js"));
42
43
  const index_js_2 = require("../shared/index.js");
43
44
  /**
44
45
  * Creates the object for composing a MimeNode instance out from the mail options
@@ -133,6 +134,10 @@ class MailComposer {
133
134
  if (this.mail.headers) {
134
135
  this.message.addHeader(this.mail.headers);
135
136
  }
137
+ // after the custom headers, so it replaces an OTP-Token from them, the field may appear only once
138
+ if (this.mail.otp) {
139
+ this.message.setHeader('OTP-Token', formatOtpToken(this.mail.otp));
140
+ }
136
141
  // Add headers to the root node, always overrides custom headers
137
142
  ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
138
143
  const key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
@@ -641,6 +646,54 @@ class MailComposer {
641
646
  return element;
642
647
  }
643
648
  }
649
+ /**
650
+ * Returns the value of the OTP-Token header for the otp option. The header is defined by
651
+ * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
652
+ * for a newer revision of the draft or a published RFC, the header name and its
653
+ * parameters are still open questions in the draft.
654
+ */
655
+ function formatOtpToken(otp) {
656
+ // a structured field String can only hold printable ascii. The code is a secret the recipient
657
+ // compares against the one in the message body, so it is rejected rather than cleaned up
658
+ if (typeof otp.code !== 'string' || !/^[\x20-\x7e]+$/.test(otp.code)) {
659
+ throw otpError('The otp code must be a non-empty string of printable ASCII characters');
660
+ }
661
+ // a structured field String escapes the same two characters as a quoted-string
662
+ return mimeFuncs.quoteString(otp.code) + '; origin=' + mimeFuncs.quoteString(getOtpOrigin(otp.origin));
663
+ }
664
+ /**
665
+ * Returns the canonical ascii serialization of the origin the code is bound to. The draft
666
+ * requires a potentially trustworthy origin, so only https is accepted, plus http on a
667
+ * loopback host for local development
668
+ */
669
+ function getOtpOrigin(origin) {
670
+ let url;
671
+ try {
672
+ url = new URL(String(origin));
673
+ }
674
+ catch {
675
+ // handled below
676
+ }
677
+ // the WHATWG parser lets characters such as a quote or a brace into a host, which the
678
+ // origin grammar the draft requires (RFC 6454) does not allow, so a receiver would discard
679
+ // the header. Only a DNS name, an IPv4 address or a bracketed IPv6 address is accepted.
680
+ // Userinfo is refused as well, an origin never has it and it turns a value built as
681
+ // 'https://example.com' + input into the origin of whatever host follows an "@"
682
+ if (!url || url.username || url.password || !/^([a-z0-9_-]+\.)*[a-z0-9_-]+$|^\[[0-9a-f:.]+\]$/.test(url.hostname)) {
683
+ throw otpError('The otp origin must be an absolute https URL');
684
+ }
685
+ const isLoopback = /^(localhost|.+\.localhost|127(\.\d+){3}|\[::1\])$/.test(url.hostname);
686
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) {
687
+ throw otpError('The otp origin must be an https URL, or an http URL on a loopback host');
688
+ }
689
+ // the URL parser already lowercases the host, converts it to punycode and drops the default port
690
+ return url.origin;
691
+ }
692
+ function otpError(message) {
693
+ const err = new Error(message);
694
+ err.code = errors.EOTP;
695
+ return err;
696
+ }
644
697
  exports.default = MailComposer;
645
698
  module.exports = exports.default;
646
699
  Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -183,7 +183,6 @@ class Mail extends node_events_1.EventEmitter {
183
183
  mail.setMailerHeader();
184
184
  mail.setPriorityHeaders();
185
185
  mail.setListHeaders();
186
- mail.setOtpHeader();
187
186
  recipientCount = mail.message.getEnvelope().to.length;
188
187
  }
189
188
  catch (err) {
@@ -103,11 +103,4 @@ export default class MailMessage<T = SentMessageInfo> {
103
103
  setMailerHeader(): void;
104
104
  setPriorityHeaders(): void;
105
105
  setListHeaders(): void;
106
- /**
107
- * Sets the OTP-Token header from the otp option. The header is defined by
108
- * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
109
- * for a newer revision of the draft or a published RFC, the header name and its
110
- * parameters are still open questions in the draft.
111
- */
112
- setOtpHeader(): void;
113
106
  }
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const shared = __importStar(require("../shared/index.js"));
40
40
  const index_js_1 = __importDefault(require("../mime-node/index.js"));
41
41
  const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
42
- const errors = __importStar(require("../errors.js"));
43
42
  // Only an own key counts as already set. `key in obj` also matches every member of
44
43
  // Object.prototype, which silently drops a transporter default legitimately named
45
44
  // toString or constructor.
@@ -301,26 +300,6 @@ class MailMessage {
301
300
  });
302
301
  });
303
302
  }
304
- /**
305
- * Sets the OTP-Token header from the otp option. The header is defined by
306
- * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
307
- * for a newer revision of the draft or a published RFC, the header name and its
308
- * parameters are still open questions in the draft.
309
- */
310
- setOtpHeader() {
311
- const otp = this.data.otp;
312
- if (!this.message || !otp) {
313
- return;
314
- }
315
- // a structured field String can only hold printable ascii. The code is a secret the recipient
316
- // compares against the one in the message body, so it is rejected rather than cleaned up
317
- if (typeof otp.code !== 'string' || !/^[\x20-\x7e]+$/.test(otp.code)) {
318
- throw otpError('The otp code must be a non-empty string of printable ASCII characters');
319
- }
320
- // a structured field String escapes the same two characters as a quoted-string. setHeader
321
- // replaces any OTP-Token from the custom headers, the field may appear only once
322
- this.message.setHeader('OTP-Token', mimeFuncs.quoteString(otp.code) + '; origin=' + mimeFuncs.quoteString(getOtpOrigin(otp.origin)));
323
- }
324
303
  /** @internal */
325
304
  _getListHeaders(listData) {
326
305
  // make sure an url looks like <protocol:url>
@@ -381,38 +360,5 @@ class MailMessage {
381
360
  }
382
361
  }
383
362
  exports.default = MailMessage;
384
- /**
385
- * Returns the canonical ascii serialization of the origin the code is bound to. The draft
386
- * requires a potentially trustworthy origin, so only https is accepted, plus http on a
387
- * loopback host for local development
388
- */
389
- function getOtpOrigin(origin) {
390
- let url;
391
- try {
392
- url = new URL(String(origin));
393
- }
394
- catch {
395
- // handled below
396
- }
397
- // the WHATWG parser lets characters such as a quote or a brace into a host, which the
398
- // origin grammar the draft requires (RFC 6454) does not allow, so a receiver would discard
399
- // the header. Only a DNS name, an IPv4 address or a bracketed IPv6 address is accepted.
400
- // Userinfo is refused as well, an origin never has it and it turns a value built as
401
- // 'https://example.com' + input into the origin of whatever host follows an "@"
402
- if (!url || url.username || url.password || !/^([a-z0-9_-]+\.)*[a-z0-9_-]+$|^\[[0-9a-f:.]+\]$/.test(url.hostname)) {
403
- throw otpError('The otp origin must be an absolute https URL');
404
- }
405
- const isLoopback = /^(localhost|.+\.localhost|127(\.\d+){3}|\[::1\])$/.test(url.hostname);
406
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) {
407
- throw otpError('The otp origin must be an https URL, or an http URL on a loopback host');
408
- }
409
- // the URL parser already lowercases the host, converts it to punycode and drops the default port
410
- return url.origin;
411
- }
412
- function otpError(message) {
413
- const err = new Error(message);
414
- err.code = errors.EOTP;
415
- return err;
416
- }
417
363
  module.exports = exports.default;
418
364
  Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.3.0";
2
+ export declare const version = "10.3.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.3.0';
6
+ exports.version = '10.3.1';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -124,7 +124,7 @@ export interface MailComposerOptions {
124
124
  headers?: MimeNodeHeaders | undefined;
125
125
  /** List-* headers, read by the mailer */
126
126
  list?: MailComposerListHeaders | undefined;
127
- /** One-time passcode for the OTP-Token header, read by the mailer */
127
+ /** One-time passcode for the OTP-Token header. An invalid value makes compile() throw an EOTP error */
128
128
  otp?: MailComposerOtp | undefined;
129
129
  /** SMTP envelope to use instead of the one generated from the headers */
130
130
  envelope?: MimeNodeEnvelopeInput | undefined;
@@ -1,6 +1,7 @@
1
1
  /* eslint no-undefined: 0 */
2
2
  import MimeNode from '../mime-node/index.js';
3
3
  import * as mimeFuncs from '../mime-funcs/index.js';
4
+ import * as errors from '../errors.js';
4
5
  import { parseDataURI, copyOwnKeys } from '../shared/index.js';
5
6
  /**
6
7
  * Creates the object for composing a MimeNode instance out from the mail options
@@ -95,6 +96,10 @@ class MailComposer {
95
96
  if (this.mail.headers) {
96
97
  this.message.addHeader(this.mail.headers);
97
98
  }
99
+ // after the custom headers, so it replaces an OTP-Token from them, the field may appear only once
100
+ if (this.mail.otp) {
101
+ this.message.setHeader('OTP-Token', formatOtpToken(this.mail.otp));
102
+ }
98
103
  // Add headers to the root node, always overrides custom headers
99
104
  ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
100
105
  const key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
@@ -603,4 +608,52 @@ class MailComposer {
603
608
  return element;
604
609
  }
605
610
  }
611
+ /**
612
+ * Returns the value of the OTP-Token header for the otp option. The header is defined by
613
+ * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
614
+ * for a newer revision of the draft or a published RFC, the header name and its
615
+ * parameters are still open questions in the draft.
616
+ */
617
+ function formatOtpToken(otp) {
618
+ // a structured field String can only hold printable ascii. The code is a secret the recipient
619
+ // compares against the one in the message body, so it is rejected rather than cleaned up
620
+ if (typeof otp.code !== 'string' || !/^[\x20-\x7e]+$/.test(otp.code)) {
621
+ throw otpError('The otp code must be a non-empty string of printable ASCII characters');
622
+ }
623
+ // a structured field String escapes the same two characters as a quoted-string
624
+ return mimeFuncs.quoteString(otp.code) + '; origin=' + mimeFuncs.quoteString(getOtpOrigin(otp.origin));
625
+ }
626
+ /**
627
+ * Returns the canonical ascii serialization of the origin the code is bound to. The draft
628
+ * requires a potentially trustworthy origin, so only https is accepted, plus http on a
629
+ * loopback host for local development
630
+ */
631
+ function getOtpOrigin(origin) {
632
+ let url;
633
+ try {
634
+ url = new URL(String(origin));
635
+ }
636
+ catch {
637
+ // handled below
638
+ }
639
+ // the WHATWG parser lets characters such as a quote or a brace into a host, which the
640
+ // origin grammar the draft requires (RFC 6454) does not allow, so a receiver would discard
641
+ // the header. Only a DNS name, an IPv4 address or a bracketed IPv6 address is accepted.
642
+ // Userinfo is refused as well, an origin never has it and it turns a value built as
643
+ // 'https://example.com' + input into the origin of whatever host follows an "@"
644
+ if (!url || url.username || url.password || !/^([a-z0-9_-]+\.)*[a-z0-9_-]+$|^\[[0-9a-f:.]+\]$/.test(url.hostname)) {
645
+ throw otpError('The otp origin must be an absolute https URL');
646
+ }
647
+ const isLoopback = /^(localhost|.+\.localhost|127(\.\d+){3}|\[::1\])$/.test(url.hostname);
648
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) {
649
+ throw otpError('The otp origin must be an https URL, or an http URL on a loopback host');
650
+ }
651
+ // the URL parser already lowercases the host, converts it to punycode and drops the default port
652
+ return url.origin;
653
+ }
654
+ function otpError(message) {
655
+ const err = new Error(message);
656
+ err.code = errors.EOTP;
657
+ return err;
658
+ }
606
659
  export default MailComposer;
@@ -145,7 +145,6 @@ class Mail extends EventEmitter {
145
145
  mail.setMailerHeader();
146
146
  mail.setPriorityHeaders();
147
147
  mail.setListHeaders();
148
- mail.setOtpHeader();
149
148
  recipientCount = mail.message.getEnvelope().to.length;
150
149
  }
151
150
  catch (err) {
@@ -103,11 +103,4 @@ export default class MailMessage<T = SentMessageInfo> {
103
103
  setMailerHeader(): void;
104
104
  setPriorityHeaders(): void;
105
105
  setListHeaders(): void;
106
- /**
107
- * Sets the OTP-Token header from the otp option. The header is defined by
108
- * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
109
- * for a newer revision of the draft or a published RFC, the header name and its
110
- * parameters are still open questions in the draft.
111
- */
112
- setOtpHeader(): void;
113
106
  }
@@ -1,7 +1,6 @@
1
1
  import * as shared from '../shared/index.js';
2
2
  import MimeNode from '../mime-node/index.js';
3
3
  import * as mimeFuncs from '../mime-funcs/index.js';
4
- import * as errors from '../errors.js';
5
4
  // Only an own key counts as already set. `key in obj` also matches every member of
6
5
  // Object.prototype, which silently drops a transporter default legitimately named
7
6
  // toString or constructor.
@@ -263,26 +262,6 @@ export default class MailMessage {
263
262
  });
264
263
  });
265
264
  }
266
- /**
267
- * Sets the OTP-Token header from the otp option. The header is defined by
268
- * draft-goto-otp-token-01, an Internet-Draft that expires on 25 March 2027. By then check
269
- * for a newer revision of the draft or a published RFC, the header name and its
270
- * parameters are still open questions in the draft.
271
- */
272
- setOtpHeader() {
273
- const otp = this.data.otp;
274
- if (!this.message || !otp) {
275
- return;
276
- }
277
- // a structured field String can only hold printable ascii. The code is a secret the recipient
278
- // compares against the one in the message body, so it is rejected rather than cleaned up
279
- if (typeof otp.code !== 'string' || !/^[\x20-\x7e]+$/.test(otp.code)) {
280
- throw otpError('The otp code must be a non-empty string of printable ASCII characters');
281
- }
282
- // a structured field String escapes the same two characters as a quoted-string. setHeader
283
- // replaces any OTP-Token from the custom headers, the field may appear only once
284
- this.message.setHeader('OTP-Token', mimeFuncs.quoteString(otp.code) + '; origin=' + mimeFuncs.quoteString(getOtpOrigin(otp.origin)));
285
- }
286
265
  /** @internal */
287
266
  _getListHeaders(listData) {
288
267
  // make sure an url looks like <protocol:url>
@@ -342,36 +321,3 @@ export default class MailMessage {
342
321
  return '<http://' + url + '>';
343
322
  }
344
323
  }
345
- /**
346
- * Returns the canonical ascii serialization of the origin the code is bound to. The draft
347
- * requires a potentially trustworthy origin, so only https is accepted, plus http on a
348
- * loopback host for local development
349
- */
350
- function getOtpOrigin(origin) {
351
- let url;
352
- try {
353
- url = new URL(String(origin));
354
- }
355
- catch {
356
- // handled below
357
- }
358
- // the WHATWG parser lets characters such as a quote or a brace into a host, which the
359
- // origin grammar the draft requires (RFC 6454) does not allow, so a receiver would discard
360
- // the header. Only a DNS name, an IPv4 address or a bracketed IPv6 address is accepted.
361
- // Userinfo is refused as well, an origin never has it and it turns a value built as
362
- // 'https://example.com' + input into the origin of whatever host follows an "@"
363
- if (!url || url.username || url.password || !/^([a-z0-9_-]+\.)*[a-z0-9_-]+$|^\[[0-9a-f:.]+\]$/.test(url.hostname)) {
364
- throw otpError('The otp origin must be an absolute https URL');
365
- }
366
- const isLoopback = /^(localhost|.+\.localhost|127(\.\d+){3}|\[::1\])$/.test(url.hostname);
367
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) {
368
- throw otpError('The otp origin must be an https URL, or an http URL on a loopback host');
369
- }
370
- // the URL parser already lowercases the host, converts it to punycode and drops the default port
371
- return url.origin;
372
- }
373
- function otpError(message) {
374
- const err = new Error(message);
375
- err.code = errors.EOTP;
376
- return err;
377
- }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.3.0";
2
+ export declare const version = "10.3.1";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = 'nodemailer';
3
- export const version = '10.3.0';
3
+ export const version = '10.3.1';
4
4
  export const homepage = 'https://nodemailer.com/';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.3.0",
3
+ "version": "10.3.1",
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",
@@ -153,7 +153,7 @@
153
153
  },
154
154
  "homepage": "https://nodemailer.com/",
155
155
  "devDependencies": {
156
- "@aws-sdk/client-sesv2": "3.1156.0",
156
+ "@aws-sdk/client-sesv2": "3.1159.0",
157
157
  "@types/node": "20.19.43",
158
158
  "bunyan": "1.8.15",
159
159
  "c8": "12.0.0",
@@ -167,10 +167,10 @@
167
167
  "prettier": "3.9.10",
168
168
  "proxy": "1.0.2",
169
169
  "proxy-test-server": "1.0.0",
170
- "smtp-server": "3.21.1",
170
+ "smtp-server": "3.21.2",
171
171
  "tsx": "4.23.15",
172
172
  "typescript": "6.0.3",
173
- "typescript-eslint": "8.72.0"
173
+ "typescript-eslint": "8.72.1"
174
174
  },
175
175
  "engines": {
176
176
  "node": ">=20.0.0"