nodemailer 10.2.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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/mailer/index.d.ts +3 -2
- package/dist/cjs/mailer/index.js +1 -0
- package/dist/cjs/mailer/mail-message.d.ts +7 -0
- package/dist/cjs/mailer/mail-message.js +54 -0
- package/dist/cjs/mime-funcs/index.d.ts +18 -1
- package/dist/cjs/mime-funcs/index.js +43 -1
- package/dist/cjs/mime-node/index.js +15 -4
- 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/mailer/index.d.ts +3 -2
- package/dist/esm/mailer/index.js +1 -0
- package/dist/esm/mailer/mail-message.d.ts +7 -0
- package/dist/esm/mailer/mail-message.js +54 -0
- package/dist/esm/mime-funcs/index.d.ts +18 -1
- package/dist/esm/mime-funcs/index.js +43 -1
- package/dist/esm/mime-node/index.js +15 -4
- 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 +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.3.0](https://github.com/nodemailer/nodemailer/compare/v10.2.1...v10.3.0) (2026-10-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add the otp option for the OTP-Token header ([7789e15](https://github.com/nodemailer/nodemailer/commit/7789e1501e4f555e9754a15664aa30f1415902b9))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* 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))
|
|
14
|
+
|
|
15
|
+
## [10.2.1](https://github.com/nodemailer/nodemailer/compare/v10.2.0...v10.2.1) (2026-10-22)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* drop the comments of a Content-Type or Content-Disposition value with the stripComments option ([b3052b7](https://github.com/nodemailer/nodemailer/commit/b3052b7fe2e49b51dd66d6c7814a072e885f1ac8))
|
|
21
|
+
|
|
3
22
|
## [10.2.0](https://github.com/nodemailer/nodemailer/compare/v10.1.7...v10.2.0) (2026-10-21)
|
|
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, read by the mailer */
|
|
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 */
|
|
@@ -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;
|
package/dist/cjs/mailer/index.js
CHANGED
|
@@ -103,4 +103,11 @@ 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;
|
|
106
113
|
}
|
|
@@ -39,6 +39,7 @@ 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"));
|
|
42
43
|
// Only an own key counts as already set. `key in obj` also matches every member of
|
|
43
44
|
// Object.prototype, which silently drops a transporter default legitimately named
|
|
44
45
|
// toString or constructor.
|
|
@@ -300,6 +301,26 @@ class MailMessage {
|
|
|
300
301
|
});
|
|
301
302
|
});
|
|
302
303
|
}
|
|
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
|
+
}
|
|
303
324
|
/** @internal */
|
|
304
325
|
_getListHeaders(listData) {
|
|
305
326
|
// make sure an url looks like <protocol:url>
|
|
@@ -360,5 +381,38 @@ class MailMessage {
|
|
|
360
381
|
}
|
|
361
382
|
}
|
|
362
383
|
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
|
+
}
|
|
363
417
|
module.exports = exports.default;
|
|
364
418
|
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -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();
|
|
@@ -654,15 +654,18 @@ class MimeNode {
|
|
|
654
654
|
return;
|
|
655
655
|
}
|
|
656
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
|
|
657
660
|
case 'Content-Disposition':
|
|
658
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
661
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
659
662
|
if (this.filename) {
|
|
660
663
|
structured.params.filename = this.filename;
|
|
661
664
|
}
|
|
662
665
|
value = mimeFuncs.buildHeaderValue(structured);
|
|
663
666
|
break;
|
|
664
667
|
case 'Content-Type':
|
|
665
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
668
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
666
669
|
// the type token decides multipart and charset below, so clean it before
|
|
667
670
|
// those run and not just on the way out, otherwise a control char makes
|
|
668
671
|
// the checks miss and the header ends up claiming a type it is not set up for
|
|
@@ -1299,7 +1302,7 @@ class MimeNode {
|
|
|
1299
1302
|
.replace(/[\s\x00-\x20\x7f:]/g, '')
|
|
1300
1303
|
.toLowerCase()
|
|
1301
1304
|
// use uppercase words, except MIME
|
|
1302
|
-
.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())
|
|
1303
1306
|
// special case
|
|
1304
1307
|
.replace(/^Content-Features$/i, 'Content-features');
|
|
1305
1308
|
return key;
|
|
@@ -1371,7 +1374,7 @@ class MimeNode {
|
|
|
1371
1374
|
// values enclosed in <>
|
|
1372
1375
|
case 'Message-ID':
|
|
1373
1376
|
case 'In-Reply-To':
|
|
1374
|
-
case 'Content-
|
|
1377
|
+
case 'Content-ID':
|
|
1375
1378
|
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1376
1379
|
// be read as literal text. drop the characters that can not appear in a header
|
|
1377
1380
|
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
@@ -1386,6 +1389,14 @@ class MimeNode {
|
|
|
1386
1389
|
value = value + '>';
|
|
1387
1390
|
}
|
|
1388
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, '');
|
|
1389
1400
|
// space separated list of values enclosed in <>
|
|
1390
1401
|
case 'References': {
|
|
1391
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, read by the mailer */
|
|
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 */
|
|
@@ -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;
|
package/dist/esm/mailer/index.js
CHANGED
|
@@ -103,4 +103,11 @@ 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;
|
|
106
113
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
4
5
|
// Only an own key counts as already set. `key in obj` also matches every member of
|
|
5
6
|
// Object.prototype, which silently drops a transporter default legitimately named
|
|
6
7
|
// toString or constructor.
|
|
@@ -262,6 +263,26 @@ export default class MailMessage {
|
|
|
262
263
|
});
|
|
263
264
|
});
|
|
264
265
|
}
|
|
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
|
+
}
|
|
265
286
|
/** @internal */
|
|
266
287
|
_getListHeaders(listData) {
|
|
267
288
|
// make sure an url looks like <protocol:url>
|
|
@@ -321,3 +342,36 @@ export default class MailMessage {
|
|
|
321
342
|
return '<http://' + url + '>';
|
|
322
343
|
}
|
|
323
344
|
}
|
|
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
|
+
}
|
|
@@ -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
|
|
@@ -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();
|
|
@@ -616,15 +616,18 @@ class MimeNode {
|
|
|
616
616
|
return;
|
|
617
617
|
}
|
|
618
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
|
|
619
622
|
case 'Content-Disposition':
|
|
620
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
623
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
621
624
|
if (this.filename) {
|
|
622
625
|
structured.params.filename = this.filename;
|
|
623
626
|
}
|
|
624
627
|
value = mimeFuncs.buildHeaderValue(structured);
|
|
625
628
|
break;
|
|
626
629
|
case 'Content-Type':
|
|
627
|
-
structured = mimeFuncs.parseHeaderValue(value);
|
|
630
|
+
structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
|
|
628
631
|
// the type token decides multipart and charset below, so clean it before
|
|
629
632
|
// those run and not just on the way out, otherwise a control char makes
|
|
630
633
|
// the checks miss and the header ends up claiming a type it is not set up for
|
|
@@ -1261,7 +1264,7 @@ class MimeNode {
|
|
|
1261
1264
|
.replace(/[\s\x00-\x20\x7f:]/g, '')
|
|
1262
1265
|
.toLowerCase()
|
|
1263
1266
|
// use uppercase words, except MIME
|
|
1264
|
-
.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())
|
|
1265
1268
|
// special case
|
|
1266
1269
|
.replace(/^Content-Features$/i, 'Content-features');
|
|
1267
1270
|
return key;
|
|
@@ -1333,7 +1336,7 @@ class MimeNode {
|
|
|
1333
1336
|
// values enclosed in <>
|
|
1334
1337
|
case 'Message-ID':
|
|
1335
1338
|
case 'In-Reply-To':
|
|
1336
|
-
case 'Content-
|
|
1339
|
+
case 'Content-ID':
|
|
1337
1340
|
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1338
1341
|
// be read as literal text. drop the characters that can not appear in a header
|
|
1339
1342
|
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
@@ -1348,6 +1351,14 @@ class MimeNode {
|
|
|
1348
1351
|
value = value + '>';
|
|
1349
1352
|
}
|
|
1350
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, '');
|
|
1351
1362
|
// space separated list of values enclosed in <>
|
|
1352
1363
|
case 'References': {
|
|
1353
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.0",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/nodemailer.js",
|
|
@@ -153,24 +153,24 @@
|
|
|
153
153
|
},
|
|
154
154
|
"homepage": "https://nodemailer.com/",
|
|
155
155
|
"devDependencies": {
|
|
156
|
-
"@aws-sdk/client-sesv2": "3.
|
|
156
|
+
"@aws-sdk/client-sesv2": "3.1156.0",
|
|
157
157
|
"@types/node": "20.19.43",
|
|
158
158
|
"bunyan": "1.8.15",
|
|
159
159
|
"c8": "12.0.0",
|
|
160
160
|
"eslint": "10.13.0",
|
|
161
161
|
"eslint-config-prettier": "10.1.8",
|
|
162
162
|
"globals": "17.13.0",
|
|
163
|
-
"libbase64": "1.
|
|
164
|
-
"libmime": "5.
|
|
165
|
-
"libqp": "
|
|
166
|
-
"mailauth": "
|
|
163
|
+
"libbase64": "1.4.1",
|
|
164
|
+
"libmime": "5.6.3",
|
|
165
|
+
"libqp": "3.0.1",
|
|
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.
|
|
170
|
+
"smtp-server": "3.21.1",
|
|
171
171
|
"tsx": "4.23.15",
|
|
172
172
|
"typescript": "6.0.3",
|
|
173
|
-
"typescript-eslint": "8.
|
|
173
|
+
"typescript-eslint": "8.72.0"
|
|
174
174
|
},
|
|
175
175
|
"engines": {
|
|
176
176
|
"node": ">=20.0.0"
|