nodemailer 10.2.1 → 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 +19 -0
- package/dist/cjs/dkim/sign.js +1 -1
- package/dist/cjs/errors.d.ts +2 -0
- package/dist/cjs/errors.js +3 -1
- package/dist/cjs/mail-composer/index.d.ts +12 -0
- package/dist/cjs/mail-composer/index.js +53 -0
- package/dist/cjs/mailer/index.d.ts +3 -2
- package/dist/cjs/mime-node/index.js +10 -2
- package/dist/cjs/nodemailer.d.ts +1 -1
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/well-known/services.js +7 -0
- package/dist/esm/dkim/sign.js +1 -1
- package/dist/esm/errors.d.ts +2 -0
- package/dist/esm/errors.js +2 -0
- package/dist/esm/mail-composer/index.d.ts +12 -0
- package/dist/esm/mail-composer/index.js +53 -0
- package/dist/esm/mailer/index.d.ts +3 -2
- package/dist/esm/mime-node/index.js +10 -2
- package/dist/esm/nodemailer.d.ts +1 -1
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/well-known/services.js +7 -0
- package/dist/well-known/services.json +8 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
10
|
+
## [10.3.0](https://github.com/nodemailer/nodemailer/compare/v10.2.1...v10.3.0) (2026-10-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add the otp option for the OTP-Token header ([7789e15](https://github.com/nodemailer/nodemailer/commit/7789e1501e4f555e9754a15664aa30f1415902b9))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* encode a Content-ID header as a message id ([#1904](https://github.com/nodemailer/nodemailer/issues/1904)) ([47f2b25](https://github.com/nodemailer/nodemailer/commit/47f2b252fccc8ec6a1a5700e12c1087ae3633e17))
|
|
21
|
+
|
|
3
22
|
## [10.2.1](https://github.com/nodemailer/nodemailer/compare/v10.2.0...v10.2.1) (2026-10-22)
|
|
4
23
|
|
|
5
24
|
|
package/dist/cjs/dkim/sign.js
CHANGED
|
@@ -68,7 +68,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
|
|
68
68
|
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
|
69
69
|
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
|
70
70
|
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
|
71
|
-
'List-Owner:List-Archive';
|
|
71
|
+
'List-Owner:List-Archive:OTP-Token';
|
|
72
72
|
const fieldNames = options.headerFieldNames || defaultFieldNames;
|
|
73
73
|
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
|
74
74
|
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const ERROR_CODES: {
|
|
|
21
21
|
readonly EPROTOCOL: "Invalid SMTP server response";
|
|
22
22
|
readonly EENVELOPE: "Invalid mail envelope (sender or recipients)";
|
|
23
23
|
readonly EMESSAGE: "Message delivery error";
|
|
24
|
+
readonly EOTP: "Invalid one-time passcode data (otp option)";
|
|
24
25
|
readonly ESTREAM: "Stream processing error";
|
|
25
26
|
readonly EAUTH: "Authentication failed";
|
|
26
27
|
readonly ENOAUTH: "Authentication credentials not provided";
|
|
@@ -48,6 +49,7 @@ export declare const EREQUIRETLS = "EREQUIRETLS";
|
|
|
48
49
|
export declare const EPROTOCOL = "EPROTOCOL";
|
|
49
50
|
export declare const EENVELOPE = "EENVELOPE";
|
|
50
51
|
export declare const EMESSAGE = "EMESSAGE";
|
|
52
|
+
export declare const EOTP = "EOTP";
|
|
51
53
|
export declare const ESTREAM = "ESTREAM";
|
|
52
54
|
export declare const EAUTH = "EAUTH";
|
|
53
55
|
export declare const ENOAUTH = "ENOAUTH";
|
package/dist/cjs/errors.js
CHANGED
|
@@ -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.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;
|
|
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.EOTP = 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
|
*/
|
|
@@ -27,6 +27,7 @@ exports.ERROR_CODES = {
|
|
|
27
27
|
EPROTOCOL: 'Invalid SMTP server response',
|
|
28
28
|
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
|
|
29
29
|
EMESSAGE: 'Message delivery error',
|
|
30
|
+
EOTP: 'Invalid one-time passcode data (otp option)',
|
|
30
31
|
ESTREAM: 'Stream processing error',
|
|
31
32
|
// Authentication errors
|
|
32
33
|
EAUTH: 'Authentication failed',
|
|
@@ -55,6 +56,7 @@ exports.EREQUIRETLS = 'EREQUIRETLS';
|
|
|
55
56
|
exports.EPROTOCOL = 'EPROTOCOL';
|
|
56
57
|
exports.EENVELOPE = 'EENVELOPE';
|
|
57
58
|
exports.EMESSAGE = 'EMESSAGE';
|
|
59
|
+
exports.EOTP = 'EOTP';
|
|
58
60
|
exports.ESTREAM = 'ESTREAM';
|
|
59
61
|
exports.EAUTH = 'EAUTH';
|
|
60
62
|
exports.ENOAUTH = 'ENOAUTH';
|
|
@@ -72,6 +72,16 @@ export type MailComposerListHeaderEntry = string | {
|
|
|
72
72
|
export interface MailComposerListHeaders {
|
|
73
73
|
[key: string]: MailComposerListHeaderEntry | (MailComposerListHeaderEntry | MailComposerListHeaderEntry[])[];
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The otp value: a one-time passcode and the origin it is bound to, sent as the OTP-Token
|
|
77
|
+
* header (draft-goto-otp-token)
|
|
78
|
+
*/
|
|
79
|
+
export interface MailComposerOtp {
|
|
80
|
+
/** The passcode, printable ascii */
|
|
81
|
+
code: string;
|
|
82
|
+
/** Origin the code is bound to, https or a loopback http origin. A full url is reduced to its origin */
|
|
83
|
+
origin: string;
|
|
84
|
+
}
|
|
75
85
|
/**
|
|
76
86
|
* Encoding for the non-ascii header values: quoted-printable ('Q', the default) or base64
|
|
77
87
|
* ('B'). Only the first letter is read, so the short forms work as well
|
|
@@ -114,6 +124,8 @@ export interface MailComposerOptions {
|
|
|
114
124
|
headers?: MimeNodeHeaders | undefined;
|
|
115
125
|
/** List-* headers, read by the mailer */
|
|
116
126
|
list?: MailComposerListHeaders | undefined;
|
|
127
|
+
/** One-time passcode for the OTP-Token header. An invalid value makes compile() throw an EOTP error */
|
|
128
|
+
otp?: MailComposerOtp | undefined;
|
|
117
129
|
/** SMTP envelope to use instead of the one generated from the headers */
|
|
118
130
|
envelope?: MimeNodeEnvelopeInput | undefined;
|
|
119
131
|
/** Content-Transfer-Encoding to force for the text/* nodes that do not set their own */
|
|
@@ -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 });
|
|
@@ -4,7 +4,7 @@ import DKIM, { type DKIMOptions } from '../dkim/index.js';
|
|
|
4
4
|
import MailMessage, { type MailDefaults, type SendMailOptions } from './mail-message.js';
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import type { ConnectionOptions } from 'node:tls';
|
|
7
|
-
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerTextEncoding } from '../mail-composer/index.js';
|
|
7
|
+
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerOtp, MailComposerTextEncoding } from '../mail-composer/index.js';
|
|
8
8
|
import type { Callback, NodemailerError, ResultCallback } from '../errors.js';
|
|
9
9
|
import type { ParsedUrl } from '../shared/url.js';
|
|
10
10
|
import type { MimeNodeAddress, MimeNodeEnvelope, MimeNodeEnvelopeInput, MimeNodeHeaders, MimeNodeOptions } from '../mime-node/index.js';
|
|
@@ -227,11 +227,12 @@ declare namespace Mail {
|
|
|
227
227
|
type Headers = MimeNodeHeaders;
|
|
228
228
|
type ListHeader = MailComposerListHeaderEntry;
|
|
229
229
|
type ListHeaders = MailComposerListHeaders;
|
|
230
|
+
type Otp = MailComposerOtp;
|
|
230
231
|
type Envelope = MimeNodeEnvelopeInput;
|
|
231
232
|
type Connection = SocketOptions;
|
|
232
233
|
type TextEncoding = MailComposerTextEncoding;
|
|
233
234
|
type PluginFunction<T = SentMessageInfo> = MailPluginFunction<T>;
|
|
234
235
|
}
|
|
235
236
|
/** The same aliases as module level exports, for `import * as Mail` and `import Mail = require()` */
|
|
236
|
-
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
|
237
|
+
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MailComposerOtp as Otp, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
|
237
238
|
export default Mail;
|
|
@@ -1302,7 +1302,7 @@ class MimeNode {
|
|
|
1302
1302
|
.replace(/[\s\x00-\x20\x7f:]/g, '')
|
|
1303
1303
|
.toLowerCase()
|
|
1304
1304
|
// use uppercase words, except MIME
|
|
1305
|
-
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1305
|
+
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^OTP(?=-Token$)|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1306
1306
|
// special case
|
|
1307
1307
|
.replace(/^Content-Features$/i, 'Content-features');
|
|
1308
1308
|
return key;
|
|
@@ -1374,7 +1374,7 @@ class MimeNode {
|
|
|
1374
1374
|
// values enclosed in <>
|
|
1375
1375
|
case 'Message-ID':
|
|
1376
1376
|
case 'In-Reply-To':
|
|
1377
|
-
case 'Content-
|
|
1377
|
+
case 'Content-ID':
|
|
1378
1378
|
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1379
1379
|
// be read as literal text. drop the characters that can not appear in a header
|
|
1380
1380
|
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
@@ -1389,6 +1389,14 @@ class MimeNode {
|
|
|
1389
1389
|
value = value + '>';
|
|
1390
1390
|
}
|
|
1391
1391
|
return value;
|
|
1392
|
+
// a structured field (RFC 9651) that can not be mime encoded, an encoded word would
|
|
1393
|
+
// turn its quoted strings into text that no structured field parser reads. Only
|
|
1394
|
+
// the characters that can not appear in a header at all are removed
|
|
1395
|
+
case 'OTP-Token':
|
|
1396
|
+
return (value || '')
|
|
1397
|
+
.toString()
|
|
1398
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1399
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
1392
1400
|
// space separated list of values enclosed in <>
|
|
1393
1401
|
case 'References': {
|
|
1394
1402
|
const ids = [];
|
package/dist/cjs/nodemailer.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export type { Mail, MailDefaults, SentMessageInfo, Transport, TransportOptions }
|
|
|
111
111
|
export type { SendMailOptions, Transporter } from './mailer/index.js';
|
|
112
112
|
export type { NodemailerError, ErrorCode } from './errors.js';
|
|
113
113
|
export type { MailMessage, MailMessageData, PluginFunction, VerifyCallback } from './mailer/index.js';
|
|
114
|
-
export type { MailComposerOptions, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerIcalEvent as IcalAttachment, MailComposerListHeaders as ListHeaders } from './mail-composer/index.js';
|
|
114
|
+
export type { MailComposerOptions, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerIcalEvent as IcalAttachment, MailComposerListHeaders as ListHeaders, MailComposerOtp as Otp } from './mail-composer/index.js';
|
|
115
115
|
export type { MimeNodeAddress as Address, MimeNodeHeaders as Headers } from './mime-node/index.js';
|
|
116
116
|
export type { SMTPConnectionOptions, SMTPConnectionAuth, SMTPEnvelope, SMTPEnvelopeDsn } from './smtp-connection/index.js';
|
|
117
117
|
export type { DKIMOptions } from './dkim/index.js';
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -208,6 +208,13 @@ exports.services = {
|
|
|
208
208
|
"host": "smtp.europe.secureserver.net",
|
|
209
209
|
"port": 25
|
|
210
210
|
},
|
|
211
|
+
"Groundmail": {
|
|
212
|
+
"description": "Groundmail",
|
|
213
|
+
"host": "smtp.groundmail.dev",
|
|
214
|
+
"port": 587,
|
|
215
|
+
"secure": false,
|
|
216
|
+
"requireTLS": true
|
|
217
|
+
},
|
|
211
218
|
"hot.ee": {
|
|
212
219
|
"description": "Hot.ee (Estonian email provider)",
|
|
213
220
|
"host": "mail.hot.ee"
|
package/dist/esm/dkim/sign.js
CHANGED
|
@@ -30,7 +30,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
|
|
30
30
|
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
|
31
31
|
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
|
32
32
|
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
|
33
|
-
'List-Owner:List-Archive';
|
|
33
|
+
'List-Owner:List-Archive:OTP-Token';
|
|
34
34
|
const fieldNames = options.headerFieldNames || defaultFieldNames;
|
|
35
35
|
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
|
36
36
|
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const ERROR_CODES: {
|
|
|
21
21
|
readonly EPROTOCOL: "Invalid SMTP server response";
|
|
22
22
|
readonly EENVELOPE: "Invalid mail envelope (sender or recipients)";
|
|
23
23
|
readonly EMESSAGE: "Message delivery error";
|
|
24
|
+
readonly EOTP: "Invalid one-time passcode data (otp option)";
|
|
24
25
|
readonly ESTREAM: "Stream processing error";
|
|
25
26
|
readonly EAUTH: "Authentication failed";
|
|
26
27
|
readonly ENOAUTH: "Authentication credentials not provided";
|
|
@@ -48,6 +49,7 @@ export declare const EREQUIRETLS = "EREQUIRETLS";
|
|
|
48
49
|
export declare const EPROTOCOL = "EPROTOCOL";
|
|
49
50
|
export declare const EENVELOPE = "EENVELOPE";
|
|
50
51
|
export declare const EMESSAGE = "EMESSAGE";
|
|
52
|
+
export declare const EOTP = "EOTP";
|
|
51
53
|
export declare const ESTREAM = "ESTREAM";
|
|
52
54
|
export declare const EAUTH = "EAUTH";
|
|
53
55
|
export declare const ENOAUTH = "ENOAUTH";
|
package/dist/esm/errors.js
CHANGED
|
@@ -24,6 +24,7 @@ export const ERROR_CODES = {
|
|
|
24
24
|
EPROTOCOL: 'Invalid SMTP server response',
|
|
25
25
|
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
|
|
26
26
|
EMESSAGE: 'Message delivery error',
|
|
27
|
+
EOTP: 'Invalid one-time passcode data (otp option)',
|
|
27
28
|
ESTREAM: 'Stream processing error',
|
|
28
29
|
// Authentication errors
|
|
29
30
|
EAUTH: 'Authentication failed',
|
|
@@ -52,6 +53,7 @@ export const EREQUIRETLS = 'EREQUIRETLS';
|
|
|
52
53
|
export const EPROTOCOL = 'EPROTOCOL';
|
|
53
54
|
export const EENVELOPE = 'EENVELOPE';
|
|
54
55
|
export const EMESSAGE = 'EMESSAGE';
|
|
56
|
+
export const EOTP = 'EOTP';
|
|
55
57
|
export const ESTREAM = 'ESTREAM';
|
|
56
58
|
export const EAUTH = 'EAUTH';
|
|
57
59
|
export const ENOAUTH = 'ENOAUTH';
|
|
@@ -72,6 +72,16 @@ export type MailComposerListHeaderEntry = string | {
|
|
|
72
72
|
export interface MailComposerListHeaders {
|
|
73
73
|
[key: string]: MailComposerListHeaderEntry | (MailComposerListHeaderEntry | MailComposerListHeaderEntry[])[];
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The otp value: a one-time passcode and the origin it is bound to, sent as the OTP-Token
|
|
77
|
+
* header (draft-goto-otp-token)
|
|
78
|
+
*/
|
|
79
|
+
export interface MailComposerOtp {
|
|
80
|
+
/** The passcode, printable ascii */
|
|
81
|
+
code: string;
|
|
82
|
+
/** Origin the code is bound to, https or a loopback http origin. A full url is reduced to its origin */
|
|
83
|
+
origin: string;
|
|
84
|
+
}
|
|
75
85
|
/**
|
|
76
86
|
* Encoding for the non-ascii header values: quoted-printable ('Q', the default) or base64
|
|
77
87
|
* ('B'). Only the first letter is read, so the short forms work as well
|
|
@@ -114,6 +124,8 @@ export interface MailComposerOptions {
|
|
|
114
124
|
headers?: MimeNodeHeaders | undefined;
|
|
115
125
|
/** List-* headers, read by the mailer */
|
|
116
126
|
list?: MailComposerListHeaders | undefined;
|
|
127
|
+
/** One-time passcode for the OTP-Token header. An invalid value makes compile() throw an EOTP error */
|
|
128
|
+
otp?: MailComposerOtp | undefined;
|
|
117
129
|
/** SMTP envelope to use instead of the one generated from the headers */
|
|
118
130
|
envelope?: MimeNodeEnvelopeInput | undefined;
|
|
119
131
|
/** Content-Transfer-Encoding to force for the text/* nodes that do not set their own */
|
|
@@ -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;
|
|
@@ -4,7 +4,7 @@ import DKIM, { type DKIMOptions } from '../dkim/index.js';
|
|
|
4
4
|
import MailMessage, { type MailDefaults, type SendMailOptions } from './mail-message.js';
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import type { ConnectionOptions } from 'node:tls';
|
|
7
|
-
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerTextEncoding } from '../mail-composer/index.js';
|
|
7
|
+
import type { MailComposerAlternative, MailComposerAttachment, MailComposerIcalEvent, MailComposerListHeaderEntry, MailComposerListHeaders, MailComposerOtp, MailComposerTextEncoding } from '../mail-composer/index.js';
|
|
8
8
|
import type { Callback, NodemailerError, ResultCallback } from '../errors.js';
|
|
9
9
|
import type { ParsedUrl } from '../shared/url.js';
|
|
10
10
|
import type { MimeNodeAddress, MimeNodeEnvelope, MimeNodeEnvelopeInput, MimeNodeHeaders, MimeNodeOptions } from '../mime-node/index.js';
|
|
@@ -227,11 +227,12 @@ declare namespace Mail {
|
|
|
227
227
|
type Headers = MimeNodeHeaders;
|
|
228
228
|
type ListHeader = MailComposerListHeaderEntry;
|
|
229
229
|
type ListHeaders = MailComposerListHeaders;
|
|
230
|
+
type Otp = MailComposerOtp;
|
|
230
231
|
type Envelope = MimeNodeEnvelopeInput;
|
|
231
232
|
type Connection = SocketOptions;
|
|
232
233
|
type TextEncoding = MailComposerTextEncoding;
|
|
233
234
|
type PluginFunction<T = SentMessageInfo> = MailPluginFunction<T>;
|
|
234
235
|
}
|
|
235
236
|
/** The same aliases as module level exports, for `import * as Mail` and `import Mail = require()` */
|
|
236
|
-
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
|
237
|
+
export type { SendMailOptions as Options, MimeNodeAddress as Address, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerAlternative as AmpAttachment, MailComposerIcalEvent as IcalAttachment, MimeNodeHeaders as Headers, MailComposerListHeaderEntry as ListHeader, MailComposerListHeaders as ListHeaders, MailComposerOtp as Otp, MimeNodeEnvelopeInput as Envelope, SocketOptions as Connection, MailComposerTextEncoding as TextEncoding };
|
|
237
238
|
export default Mail;
|
|
@@ -1264,7 +1264,7 @@ class MimeNode {
|
|
|
1264
1264
|
.replace(/[\s\x00-\x20\x7f:]/g, '')
|
|
1265
1265
|
.toLowerCase()
|
|
1266
1266
|
// use uppercase words, except MIME
|
|
1267
|
-
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1267
|
+
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^OTP(?=-Token$)|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1268
1268
|
// special case
|
|
1269
1269
|
.replace(/^Content-Features$/i, 'Content-features');
|
|
1270
1270
|
return key;
|
|
@@ -1336,7 +1336,7 @@ class MimeNode {
|
|
|
1336
1336
|
// values enclosed in <>
|
|
1337
1337
|
case 'Message-ID':
|
|
1338
1338
|
case 'In-Reply-To':
|
|
1339
|
-
case 'Content-
|
|
1339
|
+
case 'Content-ID':
|
|
1340
1340
|
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1341
1341
|
// be read as literal text. drop the characters that can not appear in a header
|
|
1342
1342
|
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
@@ -1351,6 +1351,14 @@ class MimeNode {
|
|
|
1351
1351
|
value = value + '>';
|
|
1352
1352
|
}
|
|
1353
1353
|
return value;
|
|
1354
|
+
// a structured field (RFC 9651) that can not be mime encoded, an encoded word would
|
|
1355
|
+
// turn its quoted strings into text that no structured field parser reads. Only
|
|
1356
|
+
// the characters that can not appear in a header at all are removed
|
|
1357
|
+
case 'OTP-Token':
|
|
1358
|
+
return (value || '')
|
|
1359
|
+
.toString()
|
|
1360
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1361
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
1354
1362
|
// space separated list of values enclosed in <>
|
|
1355
1363
|
case 'References': {
|
|
1356
1364
|
const ids = [];
|
package/dist/esm/nodemailer.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export type { Mail, MailDefaults, SentMessageInfo, Transport, TransportOptions }
|
|
|
111
111
|
export type { SendMailOptions, Transporter } from './mailer/index.js';
|
|
112
112
|
export type { NodemailerError, ErrorCode } from './errors.js';
|
|
113
113
|
export type { MailMessage, MailMessageData, PluginFunction, VerifyCallback } from './mailer/index.js';
|
|
114
|
-
export type { MailComposerOptions, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerIcalEvent as IcalAttachment, MailComposerListHeaders as ListHeaders } from './mail-composer/index.js';
|
|
114
|
+
export type { MailComposerOptions, MailComposerAttachment as Attachment, MailComposerAlternative as AttachmentLike, MailComposerIcalEvent as IcalAttachment, MailComposerListHeaders as ListHeaders, MailComposerOtp as Otp } from './mail-composer/index.js';
|
|
115
115
|
export type { MimeNodeAddress as Address, MimeNodeHeaders as Headers } from './mime-node/index.js';
|
|
116
116
|
export type { SMTPConnectionOptions, SMTPConnectionAuth, SMTPEnvelope, SMTPEnvelopeDsn } from './smtp-connection/index.js';
|
|
117
117
|
export type { DKIMOptions } from './dkim/index.js';
|
package/dist/esm/package-info.js
CHANGED
|
@@ -205,6 +205,13 @@ export const services = {
|
|
|
205
205
|
"host": "smtp.europe.secureserver.net",
|
|
206
206
|
"port": 25
|
|
207
207
|
},
|
|
208
|
+
"Groundmail": {
|
|
209
|
+
"description": "Groundmail",
|
|
210
|
+
"host": "smtp.groundmail.dev",
|
|
211
|
+
"port": 587,
|
|
212
|
+
"secure": false,
|
|
213
|
+
"requireTLS": true
|
|
214
|
+
},
|
|
208
215
|
"hot.ee": {
|
|
209
216
|
"description": "Hot.ee (Estonian email provider)",
|
|
210
217
|
"host": "mail.hot.ee"
|
|
@@ -180,6 +180,14 @@
|
|
|
180
180
|
"port": 25
|
|
181
181
|
},
|
|
182
182
|
|
|
183
|
+
"Groundmail": {
|
|
184
|
+
"description": "Groundmail",
|
|
185
|
+
"host": "smtp.groundmail.dev",
|
|
186
|
+
"port": 587,
|
|
187
|
+
"secure": false,
|
|
188
|
+
"requireTLS": true
|
|
189
|
+
},
|
|
190
|
+
|
|
183
191
|
"hot.ee": {
|
|
184
192
|
"description": "Hot.ee (Estonian email provider)",
|
|
185
193
|
"host": "mail.hot.ee"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "10.
|
|
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.
|
|
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",
|
|
@@ -163,14 +163,14 @@
|
|
|
163
163
|
"libbase64": "1.4.1",
|
|
164
164
|
"libmime": "5.6.3",
|
|
165
165
|
"libqp": "3.0.1",
|
|
166
|
-
"mailauth": "8.0.
|
|
166
|
+
"mailauth": "8.0.4",
|
|
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.
|
|
170
|
+
"smtp-server": "3.21.2",
|
|
171
171
|
"tsx": "4.23.15",
|
|
172
172
|
"typescript": "6.0.3",
|
|
173
|
-
"typescript-eslint": "8.72.
|
|
173
|
+
"typescript-eslint": "8.72.1"
|
|
174
174
|
},
|
|
175
175
|
"engines": {
|
|
176
176
|
"node": ">=20.0.0"
|