mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-10-23 06:55:45 +07:00
node_modules: update (#352)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
1 parent
4026a70a6b
commit
47af4a10b5
53 files changed
+630
-217
No files matched your search
+3
-3
@@ -94,9 +94,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "10.1.6",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.2.0.tgz",
|
||||||
"integrity": "sha512-8j/UEmA4AXr51URr3Ob0h+RCBtr0NKJ76Y2P9wJT6Eu3dRyruj2ULh2e6aiKrGg8nlUj1oX6N5h8dHA82EouaQ==",
|
"integrity": "sha512-/XyGw6kX2stMdKGUJdAQmYAeY1FscWfdRydfd2NtK3A3KDfUUXCqZzExSNAt2xhh90X2qLCR+o2Gd48YPJcJwA==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
+23
@@ -1,5 +1,28 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [10.2.0](https://github.com/nodemailer/nodemailer/compare/v10.1.7...v10.2.0) (2026-10-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add info.serverMessageId from the final 250 reply ([108dddd](https://github.com/nodemailer/nodemailer/commit/108dddd88d9d71f708630b7aba7fa2a7d2f14076))
|
||||||
|
* add pendingCount() to the SMTP pool ([6cffb70](https://github.com/nodemailer/nodemailer/commit/6cffb7037269a6012d992c263a8927fcaf0563f2))
|
||||||
|
* make header value encoding public with MimeNode encodeHeaderValue() ([3b048c7](https://github.com/nodemailer/nodemailer/commit/3b048c7a0fb60f946985b09b3143b0680194e47b))
|
||||||
|
* mark SMTP errors as permanent or transient with err.permanent ([6a20e2a](https://github.com/nodemailer/nodemailer/commit/6a20e2ae109eeb5662235d72d1e5a2d38515997a))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* label an ASCII message/rfc822 attachment 7bit instead of 8bit ([ad66fa1](https://github.com/nodemailer/nodemailer/commit/ad66fa1a048135867a629bcb7b6dc308ee06a945))
|
||||||
|
* report the recipients an LMTP server refuses after DATA in rejectedErrors ([dd64eb3](https://github.com/nodemailer/nodemailer/commit/dd64eb3171e3ab5629828084d3944a7859790968))
|
||||||
|
|
||||||
|
## [10.1.7](https://github.com/nodemailer/nodemailer/compare/v10.1.6...v10.1.7) (2026-10-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **mime-funcs:** encode parameter values that look like encoded words ([651d79b](https://github.com/nodemailer/nodemailer/commit/651d79b0e1067b93b0ae8fc1afbdf383a46c214d))
|
||||||
|
|
||||||
## [10.1.6](https://github.com/nodemailer/nodemailer/compare/v10.1.5...v10.1.6) (2026-10-18)
|
## [10.1.6](https://github.com/nodemailer/nodemailer/compare/v10.1.5...v10.1.6) (2026-10-18)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -693,9 +693,7 @@ function _handleAddress(tokens, from, to) {
|
|||||||
// A run that is a quoted local part with its '@' and domain is an addr-spec of
|
// A run that is a quoted local part with its '@' and domain is an addr-spec of
|
||||||
// its own, with the quoting put back on, and takes its turn like any other
|
// its own, with the quoting put back on, and takes its turn like any other
|
||||||
const local = data.quotedLocalAt[i];
|
const local = data.quotedLocalAt[i];
|
||||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
|
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
|
||||||
? _quoteLocalPart(data.text[i])
|
|
||||||
: '';
|
|
||||||
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
||||||
data.address = [quotedLocal || candidates[i]];
|
data.address = [quotedLocal || candidates[i]];
|
||||||
data.text.splice(i, 1);
|
data.text.splice(i, 1);
|
||||||
@@ -1020,8 +1018,7 @@ class Tokenizer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.node < 0) {
|
if (this.node < 0) {
|
||||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||||
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
||||||
}
|
}
|
||||||
if (chr === '\n') {
|
if (chr === '\n') {
|
||||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||||
|
|||||||
+1
-3
@@ -25,9 +25,7 @@ class DKIMSigner {
|
|||||||
this.chunks = [];
|
this.chunks = [];
|
||||||
this.chunklen = 0;
|
this.chunklen = 0;
|
||||||
this.readPos = 0;
|
this.readPos = 0;
|
||||||
this.cachePath = this.cacheDir
|
this.cachePath = this.cacheDir ? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex')) : false;
|
||||||
? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex'))
|
|
||||||
: false;
|
|
||||||
this.cache = false;
|
this.cache = false;
|
||||||
this.headers = false;
|
this.headers = false;
|
||||||
this.bodyHash = false;
|
this.bodyHash = false;
|
||||||
|
|||||||
+19
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
|
|||||||
code?: string | undefined;
|
code?: string | undefined;
|
||||||
responseCode?: number | undefined;
|
responseCode?: number | undefined;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
|
/**
|
||||||
|
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
||||||
|
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
||||||
|
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
||||||
|
* says the session was in the wrong state for the command rather than that the message was
|
||||||
|
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
||||||
|
* only an error when the client expected something else, and are not permanent either.
|
||||||
|
*
|
||||||
|
* @param responseCode SMTP reply code
|
||||||
|
* @returns true for a permanent rejection
|
||||||
|
*/
|
||||||
|
export declare const isPermanentResponseCode: (responseCode: number) => boolean;
|
||||||
/**
|
/**
|
||||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||||
@@ -89,6 +101,13 @@ export interface NodemailerError extends NodeJS.ErrnoException {
|
|||||||
response?: string | undefined;
|
response?: string | undefined;
|
||||||
/** Numeric SMTP response code */
|
/** Numeric SMTP response code */
|
||||||
responseCode?: number | undefined;
|
responseCode?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Set together with responseCode, from the reply class: true when the server rejected the
|
||||||
|
* message for good (5xx except 503), false when the same message may be accepted on a later
|
||||||
|
* attempt (4xx, 503, or an unexpected 2xx or 3xx). Not set on an error that carries no SMTP
|
||||||
|
* reply, such as a connection failure or missing credentials, see isPermanentResponseCode
|
||||||
|
*/
|
||||||
|
permanent?: boolean | undefined;
|
||||||
/** URL of the resource that could not be fetched */
|
/** URL of the resource that could not be fetched */
|
||||||
sourceUrl?: string | undefined;
|
sourceUrl?: string | undefined;
|
||||||
/** Recipient address the error applies to */
|
/** Recipient address the error applies to */
|
||||||
|
|||||||
+14
-1
@@ -10,7 +10,7 @@
|
|||||||
* err.code = errors.ECONNECTION;
|
* err.code = errors.ECONNECTION;
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ERR_ACCESS_DENIED = exports.isTransientError = exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
|
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;
|
||||||
/**
|
/**
|
||||||
* Error code descriptions for documentation and debugging
|
* Error code descriptions for documentation and debugging
|
||||||
*/
|
*/
|
||||||
@@ -73,6 +73,19 @@ exports.EFETCH = 'EFETCH';
|
|||||||
*/
|
*/
|
||||||
const isTransientError = (err) => err.responseCode === 421 || err.code === exports.ECONNECTION || err.code === exports.ESOCKET || err.code === exports.ETIMEDOUT;
|
const isTransientError = (err) => err.responseCode === 421 || err.code === exports.ECONNECTION || err.code === exports.ESOCKET || err.code === exports.ETIMEDOUT;
|
||||||
exports.isTransientError = isTransientError;
|
exports.isTransientError = isTransientError;
|
||||||
|
/**
|
||||||
|
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
||||||
|
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
||||||
|
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
||||||
|
* says the session was in the wrong state for the command rather than that the message was
|
||||||
|
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
||||||
|
* only an error when the client expected something else, and are not permanent either.
|
||||||
|
*
|
||||||
|
* @param responseCode SMTP reply code
|
||||||
|
* @returns true for a permanent rejection
|
||||||
|
*/
|
||||||
|
const isPermanentResponseCode = (responseCode) => responseCode >= 500 && responseCode < 600 && responseCode !== 503;
|
||||||
|
exports.isPermanentResponseCode = isPermanentResponseCode;
|
||||||
/**
|
/**
|
||||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||||
|
|||||||
+2
-4
@@ -198,16 +198,14 @@ class Cookies {
|
|||||||
// check if hostname matches
|
// check if hostname matches
|
||||||
// .foo.com also matches subdomains, foo.com does not
|
// .foo.com also matches subdomains, foo.com does not
|
||||||
if (urlparts.hostname !== cookie.domain &&
|
if (urlparts.hostname !== cookie.domain &&
|
||||||
(cookie.domain.charAt(0) !== '.' ||
|
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
||||||
// identical paths match, otherwise the cookie path must be a directory prefix
|
// identical paths match, otherwise the cookie path must be a directory prefix
|
||||||
const pathname = urlparts.pathname || '/';
|
const pathname = urlparts.pathname || '/';
|
||||||
const cookiePath = cookie.path;
|
const cookiePath = cookie.path;
|
||||||
const pathMatches = pathname === cookiePath ||
|
const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
|
||||||
if (!pathMatches) {
|
if (!pathMatches) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -262,11 +262,7 @@ function nmfetch(url, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (parsed.protocol === 'https:' &&
|
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !node_net_1.default.isIP(parsed.hostname) && !reqOptions.servername) {
|
||||||
parsed.hostname &&
|
|
||||||
parsed.hostname !== reqOptions.host &&
|
|
||||||
!node_net_1.default.isIP(parsed.hostname) &&
|
|
||||||
!reqOptions.servername) {
|
|
||||||
reqOptions.servername = parsed.hostname;
|
reqOptions.servername = parsed.hostname;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -369,9 +365,7 @@ function nmfetch(url, options) {
|
|||||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||||
// request headers so an attacker who controls the redirect target cannot
|
// request headers so an attacker who controls the redirect target cannot
|
||||||
// harvest them.
|
// harvest them.
|
||||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||||
redirectParsed.hostname !== parsed.hostname ||
|
|
||||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
|
||||||
if (options.headers && crossOrigin) {
|
if (options.headers && crossOrigin) {
|
||||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||||
Object.keys(options.headers).forEach(key => {
|
Object.keys(options.headers).forEach(key => {
|
||||||
|
|||||||
+42
-9
@@ -54,6 +54,40 @@ function isContentObject(value) {
|
|||||||
const content = value;
|
const content = value;
|
||||||
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
|
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Tells whether attachment content is a string or a Buffer that 7bit can describe: ASCII
|
||||||
|
* only, no NUL and no line over the 998 octets of RFC 5322. Anything else, a stream or a
|
||||||
|
* file path included, can not be checked here
|
||||||
|
*/
|
||||||
|
function fitsSevenBit(content) {
|
||||||
|
let at;
|
||||||
|
let length;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
at = i => content.charCodeAt(i);
|
||||||
|
length = content.length;
|
||||||
|
}
|
||||||
|
else if (Buffer.isBuffer(content)) {
|
||||||
|
at = i => content[i];
|
||||||
|
length = content.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let lineLength = 0;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const c = at(i);
|
||||||
|
if (c === 0 || c > 0x7f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (c === 0x0a || c === 0x0d) {
|
||||||
|
lineLength = 0;
|
||||||
|
}
|
||||||
|
else if (++lineLength > 998) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
class MailComposer {
|
class MailComposer {
|
||||||
constructor(mail) {
|
constructor(mail) {
|
||||||
this.mail = mail || {};
|
this.mail = mail || {};
|
||||||
@@ -136,8 +170,11 @@ class MailComposer {
|
|||||||
contentTransferEncoding = attachment.contentTransferEncoding;
|
contentTransferEncoding = attachment.contentTransferEncoding;
|
||||||
}
|
}
|
||||||
else if (isMessageNode) {
|
else if (isMessageNode) {
|
||||||
// the content might include non-ASCII bytes but at this point we do not know it yet
|
// a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
|
||||||
contentTransferEncoding = '8bit';
|
// the content as it is. A stream or a file might hold non-ASCII bytes that can not be
|
||||||
|
// checked here, so only content at hand that fits 7bit is labelled as such
|
||||||
|
// content with an `encoding` is decoded later, so what it decodes to is not known here either
|
||||||
|
contentTransferEncoding = !attachment.encoding && fitsSevenBit(attachment.content) ? '7bit' : '8bit';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
contentTransferEncoding = 'base64'; // the default
|
contentTransferEncoding = 'base64'; // the default
|
||||||
@@ -152,9 +189,7 @@ class MailComposer {
|
|||||||
}
|
}
|
||||||
else if (!isMessageNode && attachment.filename !== false) {
|
else if (!isMessageNode && attachment.filename !== false) {
|
||||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||||
data.filename =
|
data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
|
||||||
'attachment-' + (i + 1);
|
|
||||||
if (data.filename.indexOf('.') < 0) {
|
if (data.filename.indexOf('.') < 0) {
|
||||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||||
}
|
}
|
||||||
@@ -315,8 +350,7 @@ class MailComposer {
|
|||||||
if (this.mail.icalEvent) {
|
if (this.mail.icalEvent) {
|
||||||
eventObject = Object.assign({}, this._getIcalEvent());
|
eventObject = Object.assign({}, this._getIcalEvent());
|
||||||
eventObject.filename = false;
|
eventObject.filename = false;
|
||||||
eventObject.contentType =
|
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||||
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
|
||||||
if (!eventObject.headers) {
|
if (!eventObject.headers) {
|
||||||
eventObject.headers = {};
|
eventObject.headers = {};
|
||||||
}
|
}
|
||||||
@@ -344,8 +378,7 @@ class MailComposer {
|
|||||||
alternative = this._processDataUrl(alternative);
|
alternative = this._processDataUrl(alternative);
|
||||||
}
|
}
|
||||||
const data = {
|
const data = {
|
||||||
contentType: alternative.contentType ||
|
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
|
||||||
contentTransferEncoding: alternative.contentTransferEncoding
|
contentTransferEncoding: alternative.contentTransferEncoding
|
||||||
};
|
};
|
||||||
if (alternative.filename) {
|
if (alternative.filename) {
|
||||||
|
|||||||
+5
-6
@@ -109,8 +109,7 @@ class MailMessage {
|
|||||||
if (!attachment.filename) {
|
if (!attachment.filename) {
|
||||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||||
attachment.filename =
|
attachment.filename =
|
||||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||||
'attachment-' + (i + 1);
|
|
||||||
if (attachment.filename.indexOf('.') < 0) {
|
if (attachment.filename.indexOf('.') < 0) {
|
||||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||||
}
|
}
|
||||||
@@ -245,7 +244,7 @@ class MailMessage {
|
|||||||
value = (value && value.value) || value;
|
value = (value && value.value) || value;
|
||||||
if (value) {
|
if (value) {
|
||||||
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
||||||
value = this.message._encodeHeaderValue(key, value);
|
value = this.message.encodeHeaderValue(key, value);
|
||||||
}
|
}
|
||||||
data.normalizedHeaders[key] = value;
|
data.normalizedHeaders[key] = value;
|
||||||
}
|
}
|
||||||
@@ -257,10 +256,10 @@ class MailMessage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data.references) {
|
if (data.references) {
|
||||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
|
||||||
}
|
}
|
||||||
if (data.inReplyTo) {
|
if (data.inReplyTo) {
|
||||||
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
|
data.normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', data.inReplyTo);
|
||||||
}
|
}
|
||||||
return callback(null, data);
|
return callback(null, data);
|
||||||
});
|
});
|
||||||
@@ -329,7 +328,7 @@ class MailMessage {
|
|||||||
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
||||||
// List-ID expects a bare domain-like identifier, so strip the
|
// List-ID expects a bare domain-like identifier, so strip the
|
||||||
// scheme prefix that _formatListUrl adds or passes through
|
// scheme prefix that _formatListUrl adds or passes through
|
||||||
return ((value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<'));
|
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<');
|
||||||
}
|
}
|
||||||
// List-*: <http://domain> (comment)
|
// List-*: <http://domain> (comment)
|
||||||
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
|
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
|
||||||
|
|||||||
+8
-2
@@ -70,6 +70,12 @@ function isPlainText(value, isParam) {
|
|||||||
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
||||||
return typeof value === 'string' && !re.test(value);
|
return typeof value === 'string' && !re.test(value);
|
||||||
}
|
}
|
||||||
|
// A parameter value goes out as is only when it is plain text that does not look like an encoded
|
||||||
|
// word. Receivers decode encoded words found in parameter values (libmime does so for unencoded
|
||||||
|
// continuations), so as plain text it would come back as something else
|
||||||
|
function isPlainParamValue(value) {
|
||||||
|
return isPlainText(value, true) && !value.includes('=?');
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
||||||
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
||||||
@@ -237,7 +243,7 @@ function buildHeaderValue(structured) {
|
|||||||
const value = structured.params[key];
|
const value = structured.params[key];
|
||||||
// a parameter name is a token too and it is emitted without any quoting around it
|
// a parameter name is a token too and it is emitted without any quoting around it
|
||||||
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
||||||
if (!isPlainText(value, true) || value.length >= 75) {
|
if (!isPlainParamValue(value) || value.length >= 75) {
|
||||||
buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
||||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||||
@@ -286,7 +292,7 @@ function buildHeaderParam(key, data, maxLength) {
|
|||||||
let i, len;
|
let i, len;
|
||||||
maxLength = maxLength || 50;
|
maxLength = maxLength || 50;
|
||||||
// process ascii only text
|
// process ascii only text
|
||||||
if (isPlainText(data, true)) {
|
if (isPlainParamValue(data)) {
|
||||||
// check if conversion is even needed
|
// check if conversion is even needed
|
||||||
if (encodedStr.length <= maxLength) {
|
if (encodedStr.length <= maxLength) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+3
-12
@@ -1104,10 +1104,7 @@ const extensions = new Map([
|
|||||||
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
||||||
['bed', 'application/vnd.realvnc.bed'],
|
['bed', 'application/vnd.realvnc.bed'],
|
||||||
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
||||||
[
|
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
|
||||||
'bin',
|
|
||||||
['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
|
|
||||||
],
|
|
||||||
['bm', 'image/bmp'],
|
['bm', 'image/bmp'],
|
||||||
['bmi', 'application/vnd.bmi'],
|
['bmi', 'application/vnd.bmi'],
|
||||||
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
||||||
@@ -1152,10 +1149,7 @@ const extensions = new Map([
|
|||||||
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
||||||
['cil', 'application/vnd.ms-artgalry'],
|
['cil', 'application/vnd.ms-artgalry'],
|
||||||
['cla', 'application/vnd.claymore'],
|
['cla', 'application/vnd.claymore'],
|
||||||
[
|
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
|
||||||
'class',
|
|
||||||
['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
|
|
||||||
],
|
|
||||||
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
||||||
['clkp', 'application/vnd.crick.clicker.palette'],
|
['clkp', 'application/vnd.crick.clicker.palette'],
|
||||||
['clkt', 'application/vnd.crick.clicker.template'],
|
['clkt', 'application/vnd.crick.clicker.template'],
|
||||||
@@ -1760,10 +1754,7 @@ const extensions = new Map([
|
|||||||
['sbml', 'application/sbml+xml'],
|
['sbml', 'application/sbml+xml'],
|
||||||
['sc', 'application/vnd.ibm.secure-container'],
|
['sc', 'application/vnd.ibm.secure-container'],
|
||||||
['scd', 'application/x-msschedule'],
|
['scd', 'application/x-msschedule'],
|
||||||
[
|
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
|
||||||
'scm',
|
|
||||||
['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
|
|
||||||
],
|
|
||||||
['scq', 'application/scvp-cv-request'],
|
['scq', 'application/scvp-cv-request'],
|
||||||
['scs', 'application/scvp-cv-response'],
|
['scs', 'application/scvp-cv-response'],
|
||||||
['sct', 'text/scriptlet'],
|
['sct', 'text/scriptlet'],
|
||||||
|
|||||||
+34
-1
@@ -58,6 +58,13 @@ export interface MimeNodePreparedHeaderValue {
|
|||||||
* objects (for the address headers), a prepared value object, or a list of these
|
* objects (for the address headers), a prepared value object, or a list of these
|
||||||
*/
|
*/
|
||||||
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
|
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
|
||||||
|
/**
|
||||||
|
* Options for the static MimeNode.encodeHeaderValue
|
||||||
|
*/
|
||||||
|
export interface MimeNodeHeaderEncodingOptions {
|
||||||
|
/** Either 'Q' (the default) or 'B' */
|
||||||
|
textEncoding?: string | undefined;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* A single header, the shape the headers are stored in and one of the list forms
|
* A single header, the shape the headers are stored in and one of the list forms
|
||||||
* setHeader and addHeader accept
|
* setHeader and addHeader accept
|
||||||
@@ -319,6 +326,31 @@ declare class MimeNode {
|
|||||||
* @param raw Raw MIME contents
|
* @param raw Raw MIME contents
|
||||||
*/
|
*/
|
||||||
setRaw(raw: MimeNodeContent): this;
|
setRaw(raw: MimeNodeContent): this;
|
||||||
|
/**
|
||||||
|
* Encodes a header value the way this node encodes it in the generated message:
|
||||||
|
* address headers are punycoded and mime encoded, message id headers get their
|
||||||
|
* angle brackets, a Date object is formatted and any other value is mime encoded
|
||||||
|
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
||||||
|
*
|
||||||
|
* The result is a single unfolded line without the key. Fold it before writing it
|
||||||
|
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
encodeHeaderValue(key: string, value: MimeNodeHeaderValue): string;
|
||||||
|
/**
|
||||||
|
* Encodes a header value without a node at hand. Same as the instance method of a
|
||||||
|
* node created with the given options.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @param [options] Encoding options
|
||||||
|
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
static encodeHeaderValue(key: string, value: MimeNodeHeaderValue, options?: MimeNodeHeaderEncodingOptions): string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
|
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
|
||||||
@@ -327,7 +359,8 @@ declare namespace MimeNode {
|
|||||||
type Options = MimeNodeOptions;
|
type Options = MimeNodeOptions;
|
||||||
type Addresses = MimeNodeAddresses;
|
type Addresses = MimeNodeAddresses;
|
||||||
type Envelope = MimeNodeEnvelope;
|
type Envelope = MimeNodeEnvelope;
|
||||||
|
type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
|
||||||
}
|
}
|
||||||
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
||||||
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope };
|
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope, MimeNodeHeaderEncodingOptions as HeaderEncodingOptions };
|
||||||
export default MimeNode;
|
export default MimeNode;
|
||||||
+35
-13
@@ -155,9 +155,7 @@ function exceedsDomainLimits(domain) {
|
|||||||
*/
|
*/
|
||||||
function normalizeDomain(domain, toUnicode) {
|
function normalizeDomain(domain, toUnicode) {
|
||||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||||
if (typeof node_url_1.default.domainToASCII === 'function' &&
|
if (typeof node_url_1.default.domainToASCII === 'function' && typeof node_url_1.default.domainToUnicode === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
||||||
typeof node_url_1.default.domainToUnicode === 'function' &&
|
|
||||||
!URL_PARSER_UNSAFE.test(domain)) {
|
|
||||||
// The U-label form is decoded from the A-label form rather than from the input:
|
// The U-label form is decoded from the A-label form rather than from the input:
|
||||||
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
||||||
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
||||||
@@ -670,9 +668,7 @@ class MimeNode {
|
|||||||
// the checks miss and the header ends up claiming a type it is not set up for
|
// the checks miss and the header ends up claiming a type it is not set up for
|
||||||
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
||||||
this._handleContentType(structured);
|
this._handleContentType(structured);
|
||||||
if (structured.value.match(/^text\/plain\b/) &&
|
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
||||||
typeof this.content === 'string' &&
|
|
||||||
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
||||||
structured.params.charset = 'utf-8';
|
structured.params.charset = 'utf-8';
|
||||||
}
|
}
|
||||||
if (this.filename) {
|
if (this.filename) {
|
||||||
@@ -713,7 +709,7 @@ class MimeNode {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value = this._encodeHeaderValue(key, value);
|
value = this.encodeHeaderValue(key, value);
|
||||||
// skip empty lines
|
// skip empty lines
|
||||||
if (!(value || '').toString().trim()) {
|
if (!(value || '').toString().trim()) {
|
||||||
return;
|
return;
|
||||||
@@ -1331,8 +1327,7 @@ class MimeNode {
|
|||||||
// where it came from. MimeNode is exported and subclassable, so the generator is
|
// where it came from. MimeNode is exported and subclassable, so the generator is
|
||||||
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
||||||
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
||||||
this.boundary = structured.params.boundary =
|
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
||||||
declared || _stripBoundaryControls(this._generateBoundary());
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.boundary = false;
|
this.boundary = false;
|
||||||
@@ -1350,13 +1345,19 @@ class MimeNode {
|
|||||||
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Encodes a header value for use in the generated rfc2822 email.
|
* Encodes a header value the way this node encodes it in the generated message:
|
||||||
|
* address headers are punycoded and mime encoded, message id headers get their
|
||||||
|
* angle brackets, a Date object is formatted and any other value is mime encoded
|
||||||
|
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
||||||
*
|
*
|
||||||
* @param key Header key
|
* The result is a single unfolded line without the key. Fold it before writing it
|
||||||
|
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
* @param value Header value
|
* @param value Header value
|
||||||
* @internal
|
* @returns Encoded header value
|
||||||
*/
|
*/
|
||||||
_encodeHeaderValue(key, value) {
|
encodeHeaderValue(key, value) {
|
||||||
key = this._normalizeHeaderKey(key);
|
key = this._normalizeHeaderKey(key);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
// Structured headers
|
// Structured headers
|
||||||
@@ -1421,6 +1422,27 @@ class MimeNode {
|
|||||||
return this._encodeHeaderText(value);
|
return this._encodeHeaderText(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Encodes a header value without a node at hand. Same as the instance method of a
|
||||||
|
* node created with the given options.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @param [options] Encoding options
|
||||||
|
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
static encodeHeaderValue(key, value, options) {
|
||||||
|
return new MimeNode(false, { textEncoding: options?.textEncoding }).encodeHeaderValue(key, value);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Alias of encodeHeaderValue, kept for callers that used the method before it was public
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_encodeHeaderValue(key, value) {
|
||||||
|
return this.encodeHeaderValue(key, value);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Rebuilds address object using punycode and other adjustments
|
* Rebuilds address object using punycode and other adjustments
|
||||||
*
|
*
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
export declare const name = "nodemailer";
|
export declare const name = "nodemailer";
|
||||||
export declare const version = "10.1.6";
|
export declare const version = "10.2.0";
|
||||||
export declare const homepage = "https://nodemailer.com/";
|
export declare const homepage = "https://nodemailer.com/";
|
||||||
+1
-1
@@ -3,5 +3,5 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.homepage = exports.version = exports.name = void 0;
|
exports.homepage = exports.version = exports.name = void 0;
|
||||||
exports.name = 'nodemailer';
|
exports.name = 'nodemailer';
|
||||||
exports.version = '10.1.6';
|
exports.version = '10.2.0';
|
||||||
exports.homepage = 'https://nodemailer.com/';
|
exports.homepage = 'https://nodemailer.com/';
|
||||||
+3
-12
@@ -50,15 +50,9 @@ function encodeBytes(buffer, next, binary, previous) {
|
|||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const ord = buffer[i];
|
const ord = buffer[i];
|
||||||
const following = i + 1 < len ? buffer[i + 1] : next;
|
const following = i + 1 < len ? buffer[i + 1] : next;
|
||||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (ord === 0x0d ? following === 0x0a : (i > 0 ? buffer[i - 1] : previous) === 0x0d) : true;
|
||||||
? ord === 0x0d
|
|
||||||
? following === 0x0a
|
|
||||||
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
|
||||||
: true;
|
|
||||||
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
||||||
if (QP_LITERAL[ord] &&
|
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||||
lineBreakByte &&
|
|
||||||
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
||||||
output[pos++] = ord;
|
output[pos++] = ord;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -119,10 +113,7 @@ function wrap(str, lineLength) {
|
|||||||
line = line.substr(0, line.length - match[0].length);
|
line = line.substr(0, line.length - match[0].length);
|
||||||
}
|
}
|
||||||
// ensure that utf-8 sequences are not split
|
// ensure that utf-8 sequences are not split
|
||||||
while (line.length > 3 &&
|
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
|
||||||
line.length < len - pos &&
|
|
||||||
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
|
||||||
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
||||||
code = parseInt(match[0].substr(1, 2), 16);
|
code = parseInt(match[0].substr(1, 2), 16);
|
||||||
if (code < 128) {
|
if (code < 128) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
+1
-3
@@ -109,9 +109,7 @@ class SendmailTransport {
|
|||||||
return done(err);
|
return done(err);
|
||||||
}
|
}
|
||||||
// force -i to keep single dots
|
// force -i to keep single dots
|
||||||
const args = this.args
|
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||||
? ['-i'].concat(this.args).concat(envelope.to)
|
|
||||||
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
||||||
const callback = (err) => {
|
const callback = (err) => {
|
||||||
if (returned) {
|
if (returned) {
|
||||||
// ignore any additional responses, already done
|
// ignore any additional responses, already done
|
||||||
|
|||||||
+2
-6
@@ -263,9 +263,7 @@ const resolveHostname = (options, callback) => {
|
|||||||
return respond(err);
|
return respond(err);
|
||||||
}
|
}
|
||||||
// Get all supported addresses from dns.lookup
|
// Get all supported addresses from dns.lookup
|
||||||
const supportedAddresses = addresses
|
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
||||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
|
||||||
: [];
|
|
||||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||||
// there are addresses but none can be used
|
// there are addresses but none can be used
|
||||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||||
@@ -601,9 +599,7 @@ const assign = function (...args) {
|
|||||||
if ((0, objects_js_1.isProtoKey)(key)) {
|
if ((0, objects_js_1.isProtoKey)(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['tls', 'auth'].includes(key) &&
|
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
||||||
source[key] &&
|
|
||||||
typeof source[key] === 'object') {
|
|
||||||
// tls and auth are special keys that need to be enumerated separately
|
// tls and auth are special keys that need to be enumerated separately
|
||||||
// other objects are passed as is. Enumerating is a copy of user supplied
|
// other objects are passed as is. Enumerating is a copy of user supplied
|
||||||
// keys just like the loop above, so it gets the same treatment
|
// keys just like the loop above, so it gets the same treatment
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ const errors = __importStar(require("../errors.js"));
|
|||||||
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
||||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||||
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
||||||
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
const unbracket = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
||||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
||||||
if (typeof tlsOptions === 'function') {
|
if (typeof tlsOptions === 'function') {
|
||||||
callback = tlsOptions;
|
callback = tlsOptions;
|
||||||
|
|||||||
+6
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
|||||||
messageSize: number;
|
messageSize: number;
|
||||||
/** Final server response for the message */
|
/** Final server response for the message */
|
||||||
response: string;
|
response: string;
|
||||||
|
/**
|
||||||
|
* Message-ID the server gave the message, with angle brackets, when its final reply names
|
||||||
|
* one in a known format (Microsoft 365 and Outlook.com, Amazon SES). Such a server replaces
|
||||||
|
* the Message-ID header, so the delivered copy carries this id instead of messageId
|
||||||
|
*/
|
||||||
|
serverMessageId?: string | undefined;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Callback for send(), receives the result once the server accepted the message. The error
|
* Callback for send(), receives the result once the server accepted the message. The error
|
||||||
|
|||||||
+15
-12
@@ -45,6 +45,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|||||||
const data_stream_js_1 = __importDefault(require("./data-stream.js"));
|
const data_stream_js_1 = __importDefault(require("./data-stream.js"));
|
||||||
const node_stream_1 = require("node:stream");
|
const node_stream_1 = require("node:stream");
|
||||||
const shared = __importStar(require("../shared/index.js"));
|
const shared = __importStar(require("../shared/index.js"));
|
||||||
|
const server_message_id_js_1 = require("./server-message-id.js");
|
||||||
const errors_js_1 = require("../errors.js");
|
const errors_js_1 = require("../errors.js");
|
||||||
// default timeout values in ms
|
// default timeout values in ms
|
||||||
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
||||||
@@ -399,9 +400,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
if (this._destroyed || this._closing) {
|
if (this._destroyed || this._closing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const connectFn = secure
|
const connectFn = secure ? node_tls_1.default.connect : node_net_1.default.connect;
|
||||||
? node_tls_1.default.connect
|
|
||||||
: node_net_1.default.connect;
|
|
||||||
try {
|
try {
|
||||||
this._socket = connectFn(opts, () => this._onConnect());
|
this._socket = connectFn(opts, () => this._onConnect());
|
||||||
this._setupConnectionHandlers();
|
this._setupConnectionHandlers();
|
||||||
@@ -739,6 +738,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
result.messageTime = Date.now() - envelopeTime;
|
result.messageTime = Date.now() - envelopeTime;
|
||||||
result.messageSize = stream.outByteCount;
|
result.messageSize = stream.outByteCount;
|
||||||
result.response = str;
|
result.response = str;
|
||||||
|
const serverMessageId = (0, server_message_id_js_1.parseServerMessageId)(result.response, this.options.host);
|
||||||
|
if (serverMessageId) {
|
||||||
|
result.serverMessageId = serverMessageId;
|
||||||
|
}
|
||||||
|
// an LMTP server answers the message once per recipient, so recipients can be
|
||||||
|
// refused after the envelope info was built without any rejected errors on it
|
||||||
|
const rejectedErrors = this._envelope?.rejectedErrors;
|
||||||
|
if (!result.rejectedErrors && rejectedErrors && rejectedErrors.length) {
|
||||||
|
result.rejectedErrors = rejectedErrors;
|
||||||
|
}
|
||||||
return callback(null, result);
|
return callback(null, result);
|
||||||
});
|
});
|
||||||
if (typeof message.pipe === 'function') {
|
if (typeof message.pipe === 'function') {
|
||||||
@@ -966,6 +975,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
||||||
if (responseCode) {
|
if (responseCode) {
|
||||||
err.responseCode = responseCode;
|
err.responseCode = responseCode;
|
||||||
|
err.permanent = (0, errors_js_1.isPermanentResponseCode)(responseCode);
|
||||||
}
|
}
|
||||||
if (command) {
|
if (command) {
|
||||||
err.command = command;
|
err.command = command;
|
||||||
@@ -1008,12 +1018,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!failureResponse &&
|
if (!failureResponse && this.stage === 'connected' && !this._responseActions.length && !this._pendingSend && !this._destroyed && !this._closing) {
|
||||||
this.stage === 'connected' &&
|
|
||||||
!this._responseActions.length &&
|
|
||||||
!this._pendingSend &&
|
|
||||||
!this._destroyed &&
|
|
||||||
!this._closing) {
|
|
||||||
// nothing was waiting for the server, so this is the server ending an idle session
|
// nothing was waiting for the server, so this is the server ending an idle session
|
||||||
// (usually after its idle timeout), not a failure
|
// (usually after its idle timeout), not a failure
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
@@ -1234,9 +1239,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
const args = [];
|
const args = [];
|
||||||
let useSmtpUtf8 = false;
|
let useSmtpUtf8 = false;
|
||||||
this._envelope = (envelope || {});
|
this._envelope = (envelope || {});
|
||||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
this._envelope.to = []
|
this._envelope.to = []
|
||||||
.concat(this._envelope.to || [])
|
.concat(this._envelope.to || [])
|
||||||
.map(to => ((to && to.address) || to || '').toString().trim());
|
.map(to => ((to && to.address) || to || '').toString().trim());
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
||||||
|
* servers replace the Message-ID header of a submitted message with one of their own, and
|
||||||
|
* the copy the recipients and the sent folder get carries that one instead of the id the
|
||||||
|
* message was built with. Only replies in a format known to carry such an id are read.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Reads the server assigned Message-ID out of the final reply to a message
|
||||||
|
*
|
||||||
|
* @param response Final server reply for the message
|
||||||
|
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
||||||
|
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
||||||
|
*/
|
||||||
|
export declare function parseServerMessageId(response: string | undefined, host?: string): string | undefined;
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
||||||
|
* servers replace the Message-ID header of a submitted message with one of their own, and
|
||||||
|
* the copy the recipients and the sent folder get carries that one instead of the id the
|
||||||
|
* message was built with. Only replies in a format known to carry such an id are read.
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.parseServerMessageId = parseServerMessageId;
|
||||||
|
// Microsoft 365 and Outlook.com: "250 2.0.0 OK <id@host.prod.outlook.com> [Hostname=...]"
|
||||||
|
const OUTLOOK_REPLY = /^250 2\.0\.0 OK (<[^<>\s]+\.prod\.outlook\.com>)/i;
|
||||||
|
// Amazon SES: "250 Ok 0100018b5c3c0a4f-...-000000", the local part of the Message-ID it sets
|
||||||
|
const SES_REPLY = /^250 Ok ([0-9a-f]+(?:-[0-9a-f]+)+)$/i;
|
||||||
|
// an AWS region name such as us-east-1 or ap-southeast-2
|
||||||
|
const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
||||||
|
/**
|
||||||
|
* Finds the AWS region an SES SMTP endpoint is in from its hostname, such as
|
||||||
|
* email-smtp.eu-west-1.amazonaws.com or a VPC endpoint name under it
|
||||||
|
*
|
||||||
|
* @param host SMTP hostname
|
||||||
|
* @returns Region name, or undefined for a host outside amazonaws.com and awsapps.com
|
||||||
|
*/
|
||||||
|
function sesRegion(host) {
|
||||||
|
const labels = host.toLowerCase().replace(/\.$/, '').split('.');
|
||||||
|
const domain = labels.slice(-2).join('.');
|
||||||
|
if (domain !== 'amazonaws.com' && domain !== 'awsapps.com') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return labels.slice(0, -2).find(label => AWS_REGION.test(label));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reads the server assigned Message-ID out of the final reply to a message
|
||||||
|
*
|
||||||
|
* @param response Final server reply for the message
|
||||||
|
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
||||||
|
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
||||||
|
*/
|
||||||
|
function parseServerMessageId(response, host) {
|
||||||
|
const reply = (response || '').toString().trim();
|
||||||
|
const outlook = reply.match(OUTLOOK_REPLY);
|
||||||
|
if (outlook) {
|
||||||
|
return outlook[1];
|
||||||
|
}
|
||||||
|
const ses = reply.match(SES_REPLY);
|
||||||
|
if (ses && host) {
|
||||||
|
const region = sesRegion(host);
|
||||||
|
if (region) {
|
||||||
|
// the SES Message-ID domain is email.amazonses.com in us-east-1 and the region elsewhere
|
||||||
|
return '<' + ses[1].toLowerCase() + '@' + (region === 'us-east-1' ? 'email' : region) + '.amazonses.com>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
+11
@@ -110,6 +110,17 @@ declare class SMTPPool extends EventEmitter {
|
|||||||
* Returns true if there are free slots in the queue
|
* Returns true if there are free slots in the queue
|
||||||
*/
|
*/
|
||||||
isIdle(): boolean;
|
isIdle(): boolean;
|
||||||
|
/**
|
||||||
|
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
||||||
|
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
||||||
|
* pool can take another message, it is already true while a message is still being sent,
|
||||||
|
* so use this to check that nothing would be lost by closing the pool. A message that is
|
||||||
|
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
||||||
|
* it is no longer counted by the time its callback runs.
|
||||||
|
*
|
||||||
|
* @returns Number of queued and in-flight messages
|
||||||
|
*/
|
||||||
|
pendingCount(): number;
|
||||||
/**
|
/**
|
||||||
* Verifies SMTP configuration
|
* Verifies SMTP configuration
|
||||||
*
|
*
|
||||||
|
|||||||
+25
-1
@@ -100,6 +100,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|||||||
this._queue = [];
|
this._queue = [];
|
||||||
this._connections = [];
|
this._connections = [];
|
||||||
this._connectionCounter = 0;
|
this._connectionCounter = 0;
|
||||||
|
this._pending = 0;
|
||||||
this.idling = true;
|
this.idling = true;
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
if (this.idling) {
|
if (this.idling) {
|
||||||
@@ -132,10 +133,20 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|||||||
setImmediate(() => callback(err));
|
setImmediate(() => callback(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// counted until the callback runs, whichever path runs it: delivered, failed, requeued
|
||||||
|
// until it gives up, or drained by close()
|
||||||
|
let counted = true;
|
||||||
|
this._pending++;
|
||||||
this._queue.push({
|
this._queue.push({
|
||||||
mail,
|
mail,
|
||||||
requeueAttempts: 0,
|
requeueAttempts: 0,
|
||||||
callback
|
callback: (err, info) => {
|
||||||
|
if (counted) {
|
||||||
|
counted = false;
|
||||||
|
this._pending--;
|
||||||
|
}
|
||||||
|
callback(err, info);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
||||||
this.idling = false;
|
this.idling = false;
|
||||||
@@ -479,6 +490,19 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|||||||
isIdle() {
|
isIdle() {
|
||||||
return this.idling;
|
return this.idling;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
||||||
|
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
||||||
|
* pool can take another message, it is already true while a message is still being sent,
|
||||||
|
* so use this to check that nothing would be lost by closing the pool. A message that is
|
||||||
|
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
||||||
|
* it is no longer counted by the time its callback runs.
|
||||||
|
*
|
||||||
|
* @returns Number of queued and in-flight messages
|
||||||
|
*/
|
||||||
|
pendingCount() {
|
||||||
|
return this._pending;
|
||||||
|
}
|
||||||
verify(callback) {
|
verify(callback) {
|
||||||
let promise;
|
let promise;
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
|
|||||||
+2
-5
@@ -57,8 +57,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|||||||
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
||||||
case 'OAUTH2': {
|
case 'OAUTH2': {
|
||||||
const oauth2 = new index_js_3.default(this.options.auth, this.logger);
|
const oauth2 = new index_js_3.default(this.options.auth, this.logger);
|
||||||
oauth2.provisionCallback =
|
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||||
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
||||||
this.auth = {
|
this.auth = {
|
||||||
type: 'OAUTH2',
|
type: 'OAUTH2',
|
||||||
user: this.options.auth.user,
|
user: this.options.auth.user,
|
||||||
@@ -246,9 +245,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|||||||
this.connection.close();
|
this.connection.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
||||||
err.responseCode !== 421 &&
|
|
||||||
!this.connection._destroyed) {
|
|
||||||
// The server refused this message, the connection itself is fine. Reset the
|
// The server refused this message, the connection itself is fine. Reset the
|
||||||
// session and keep using it instead of opening a new one
|
// session and keep using it instead of opening a new one
|
||||||
this.connection.reset(resetErr => {
|
this.connection.reset(resetErr => {
|
||||||
|
|||||||
+2
-5
@@ -691,9 +691,7 @@ function _handleAddress(tokens, from, to) {
|
|||||||
// A run that is a quoted local part with its '@' and domain is an addr-spec of
|
// A run that is a quoted local part with its '@' and domain is an addr-spec of
|
||||||
// its own, with the quoting put back on, and takes its turn like any other
|
// its own, with the quoting put back on, and takes its turn like any other
|
||||||
const local = data.quotedLocalAt[i];
|
const local = data.quotedLocalAt[i];
|
||||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
|
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
|
||||||
? _quoteLocalPart(data.text[i])
|
|
||||||
: '';
|
|
||||||
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
||||||
data.address = [quotedLocal || candidates[i]];
|
data.address = [quotedLocal || candidates[i]];
|
||||||
data.text.splice(i, 1);
|
data.text.splice(i, 1);
|
||||||
@@ -1018,8 +1016,7 @@ class Tokenizer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.node < 0) {
|
if (this.node < 0) {
|
||||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||||
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
||||||
}
|
}
|
||||||
if (chr === '\n') {
|
if (chr === '\n') {
|
||||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||||
|
|||||||
+1
-3
@@ -20,9 +20,7 @@ class DKIMSigner {
|
|||||||
this.chunks = [];
|
this.chunks = [];
|
||||||
this.chunklen = 0;
|
this.chunklen = 0;
|
||||||
this.readPos = 0;
|
this.readPos = 0;
|
||||||
this.cachePath = this.cacheDir
|
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
|
||||||
? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
|
|
||||||
: false;
|
|
||||||
this.cache = false;
|
this.cache = false;
|
||||||
this.headers = false;
|
this.headers = false;
|
||||||
this.bodyHash = false;
|
this.bodyHash = false;
|
||||||
|
|||||||
+19
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
|
|||||||
code?: string | undefined;
|
code?: string | undefined;
|
||||||
responseCode?: number | undefined;
|
responseCode?: number | undefined;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
|
/**
|
||||||
|
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
||||||
|
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
||||||
|
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
||||||
|
* says the session was in the wrong state for the command rather than that the message was
|
||||||
|
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
||||||
|
* only an error when the client expected something else, and are not permanent either.
|
||||||
|
*
|
||||||
|
* @param responseCode SMTP reply code
|
||||||
|
* @returns true for a permanent rejection
|
||||||
|
*/
|
||||||
|
export declare const isPermanentResponseCode: (responseCode: number) => boolean;
|
||||||
/**
|
/**
|
||||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||||
@@ -89,6 +101,13 @@ export interface NodemailerError extends NodeJS.ErrnoException {
|
|||||||
response?: string | undefined;
|
response?: string | undefined;
|
||||||
/** Numeric SMTP response code */
|
/** Numeric SMTP response code */
|
||||||
responseCode?: number | undefined;
|
responseCode?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Set together with responseCode, from the reply class: true when the server rejected the
|
||||||
|
* message for good (5xx except 503), false when the same message may be accepted on a later
|
||||||
|
* attempt (4xx, 503, or an unexpected 2xx or 3xx). Not set on an error that carries no SMTP
|
||||||
|
* reply, such as a connection failure or missing credentials, see isPermanentResponseCode
|
||||||
|
*/
|
||||||
|
permanent?: boolean | undefined;
|
||||||
/** URL of the resource that could not be fetched */
|
/** URL of the resource that could not be fetched */
|
||||||
sourceUrl?: string | undefined;
|
sourceUrl?: string | undefined;
|
||||||
/** Recipient address the error applies to */
|
/** Recipient address the error applies to */
|
||||||
|
|||||||
+12
@@ -69,6 +69,18 @@ export const EFETCH = 'EFETCH';
|
|||||||
* Tells whether the error ended the connection rather than rejected what was sent over it
|
* Tells whether the error ended the connection rather than rejected what was sent over it
|
||||||
*/
|
*/
|
||||||
export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
|
export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
|
||||||
|
/**
|
||||||
|
* Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
|
||||||
|
* message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
|
||||||
|
* and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
|
||||||
|
* says the session was in the wrong state for the command rather than that the message was
|
||||||
|
* refused, so the same message can go through on a new connection. 2yz and 3yz replies are
|
||||||
|
* only an error when the client expected something else, and are not permanent either.
|
||||||
|
*
|
||||||
|
* @param responseCode SMTP reply code
|
||||||
|
* @returns true for a permanent rejection
|
||||||
|
*/
|
||||||
|
export const isPermanentResponseCode = (responseCode) => responseCode >= 500 && responseCode < 600 && responseCode !== 503;
|
||||||
/**
|
/**
|
||||||
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
||||||
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
||||||
|
|||||||
+2
-4
@@ -160,16 +160,14 @@ class Cookies {
|
|||||||
// check if hostname matches
|
// check if hostname matches
|
||||||
// .foo.com also matches subdomains, foo.com does not
|
// .foo.com also matches subdomains, foo.com does not
|
||||||
if (urlparts.hostname !== cookie.domain &&
|
if (urlparts.hostname !== cookie.domain &&
|
||||||
(cookie.domain.charAt(0) !== '.' ||
|
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
||||||
// identical paths match, otherwise the cookie path must be a directory prefix
|
// identical paths match, otherwise the cookie path must be a directory prefix
|
||||||
const pathname = urlparts.pathname || '/';
|
const pathname = urlparts.pathname || '/';
|
||||||
const cookiePath = cookie.path;
|
const cookiePath = cookie.path;
|
||||||
const pathMatches = pathname === cookiePath ||
|
const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
|
||||||
if (!pathMatches) {
|
if (!pathMatches) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -224,11 +224,7 @@ function nmfetch(url, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (parsed.protocol === 'https:' &&
|
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
||||||
parsed.hostname &&
|
|
||||||
parsed.hostname !== reqOptions.host &&
|
|
||||||
!net.isIP(parsed.hostname) &&
|
|
||||||
!reqOptions.servername) {
|
|
||||||
reqOptions.servername = parsed.hostname;
|
reqOptions.servername = parsed.hostname;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -331,9 +327,7 @@ function nmfetch(url, options) {
|
|||||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||||
// request headers so an attacker who controls the redirect target cannot
|
// request headers so an attacker who controls the redirect target cannot
|
||||||
// harvest them.
|
// harvest them.
|
||||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||||
redirectParsed.hostname !== parsed.hostname ||
|
|
||||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
|
||||||
if (options.headers && crossOrigin) {
|
if (options.headers && crossOrigin) {
|
||||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||||
Object.keys(options.headers).forEach(key => {
|
Object.keys(options.headers).forEach(key => {
|
||||||
|
|||||||
+42
-9
@@ -16,6 +16,40 @@ function isContentObject(value) {
|
|||||||
const content = value;
|
const content = value;
|
||||||
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
|
return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Tells whether attachment content is a string or a Buffer that 7bit can describe: ASCII
|
||||||
|
* only, no NUL and no line over the 998 octets of RFC 5322. Anything else, a stream or a
|
||||||
|
* file path included, can not be checked here
|
||||||
|
*/
|
||||||
|
function fitsSevenBit(content) {
|
||||||
|
let at;
|
||||||
|
let length;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
at = i => content.charCodeAt(i);
|
||||||
|
length = content.length;
|
||||||
|
}
|
||||||
|
else if (Buffer.isBuffer(content)) {
|
||||||
|
at = i => content[i];
|
||||||
|
length = content.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let lineLength = 0;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const c = at(i);
|
||||||
|
if (c === 0 || c > 0x7f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (c === 0x0a || c === 0x0d) {
|
||||||
|
lineLength = 0;
|
||||||
|
}
|
||||||
|
else if (++lineLength > 998) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
class MailComposer {
|
class MailComposer {
|
||||||
constructor(mail) {
|
constructor(mail) {
|
||||||
this.mail = mail || {};
|
this.mail = mail || {};
|
||||||
@@ -98,8 +132,11 @@ class MailComposer {
|
|||||||
contentTransferEncoding = attachment.contentTransferEncoding;
|
contentTransferEncoding = attachment.contentTransferEncoding;
|
||||||
}
|
}
|
||||||
else if (isMessageNode) {
|
else if (isMessageNode) {
|
||||||
// the content might include non-ASCII bytes but at this point we do not know it yet
|
// a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
|
||||||
contentTransferEncoding = '8bit';
|
// the content as it is. A stream or a file might hold non-ASCII bytes that can not be
|
||||||
|
// checked here, so only content at hand that fits 7bit is labelled as such
|
||||||
|
// content with an `encoding` is decoded later, so what it decodes to is not known here either
|
||||||
|
contentTransferEncoding = !attachment.encoding && fitsSevenBit(attachment.content) ? '7bit' : '8bit';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
contentTransferEncoding = 'base64'; // the default
|
contentTransferEncoding = 'base64'; // the default
|
||||||
@@ -114,9 +151,7 @@ class MailComposer {
|
|||||||
}
|
}
|
||||||
else if (!isMessageNode && attachment.filename !== false) {
|
else if (!isMessageNode && attachment.filename !== false) {
|
||||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||||
data.filename =
|
data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
|
||||||
'attachment-' + (i + 1);
|
|
||||||
if (data.filename.indexOf('.') < 0) {
|
if (data.filename.indexOf('.') < 0) {
|
||||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||||
}
|
}
|
||||||
@@ -277,8 +312,7 @@ class MailComposer {
|
|||||||
if (this.mail.icalEvent) {
|
if (this.mail.icalEvent) {
|
||||||
eventObject = Object.assign({}, this._getIcalEvent());
|
eventObject = Object.assign({}, this._getIcalEvent());
|
||||||
eventObject.filename = false;
|
eventObject.filename = false;
|
||||||
eventObject.contentType =
|
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||||
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
|
||||||
if (!eventObject.headers) {
|
if (!eventObject.headers) {
|
||||||
eventObject.headers = {};
|
eventObject.headers = {};
|
||||||
}
|
}
|
||||||
@@ -306,8 +340,7 @@ class MailComposer {
|
|||||||
alternative = this._processDataUrl(alternative);
|
alternative = this._processDataUrl(alternative);
|
||||||
}
|
}
|
||||||
const data = {
|
const data = {
|
||||||
contentType: alternative.contentType ||
|
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
|
||||||
contentTransferEncoding: alternative.contentTransferEncoding
|
contentTransferEncoding: alternative.contentTransferEncoding
|
||||||
};
|
};
|
||||||
if (alternative.filename) {
|
if (alternative.filename) {
|
||||||
|
|||||||
+5
-6
@@ -71,8 +71,7 @@ export default class MailMessage {
|
|||||||
if (!attachment.filename) {
|
if (!attachment.filename) {
|
||||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||||
attachment.filename =
|
attachment.filename =
|
||||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||||
'attachment-' + (i + 1);
|
|
||||||
if (attachment.filename.indexOf('.') < 0) {
|
if (attachment.filename.indexOf('.') < 0) {
|
||||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||||
}
|
}
|
||||||
@@ -207,7 +206,7 @@ export default class MailMessage {
|
|||||||
value = (value && value.value) || value;
|
value = (value && value.value) || value;
|
||||||
if (value) {
|
if (value) {
|
||||||
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
||||||
value = this.message._encodeHeaderValue(key, value);
|
value = this.message.encodeHeaderValue(key, value);
|
||||||
}
|
}
|
||||||
data.normalizedHeaders[key] = value;
|
data.normalizedHeaders[key] = value;
|
||||||
}
|
}
|
||||||
@@ -219,10 +218,10 @@ export default class MailMessage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data.references) {
|
if (data.references) {
|
||||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
|
||||||
}
|
}
|
||||||
if (data.inReplyTo) {
|
if (data.inReplyTo) {
|
||||||
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
|
data.normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', data.inReplyTo);
|
||||||
}
|
}
|
||||||
return callback(null, data);
|
return callback(null, data);
|
||||||
});
|
});
|
||||||
@@ -291,7 +290,7 @@ export default class MailMessage {
|
|||||||
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
||||||
// List-ID expects a bare domain-like identifier, so strip the
|
// List-ID expects a bare domain-like identifier, so strip the
|
||||||
// scheme prefix that _formatListUrl adds or passes through
|
// scheme prefix that _formatListUrl adds or passes through
|
||||||
return ((value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<'));
|
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<');
|
||||||
}
|
}
|
||||||
// List-*: <http://domain> (comment)
|
// List-*: <http://domain> (comment)
|
||||||
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
|
// the ctext specials go out as quoted-pairs, otherwise a ")" closes the
|
||||||
|
|||||||
+8
-2
@@ -21,6 +21,12 @@ export function isPlainText(value, isParam) {
|
|||||||
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
||||||
return typeof value === 'string' && !re.test(value);
|
return typeof value === 'string' && !re.test(value);
|
||||||
}
|
}
|
||||||
|
// A parameter value goes out as is only when it is plain text that does not look like an encoded
|
||||||
|
// word. Receivers decode encoded words found in parameter values (libmime does so for unencoded
|
||||||
|
// continuations), so as plain text it would come back as something else
|
||||||
|
function isPlainParamValue(value) {
|
||||||
|
return isPlainText(value, true) && !value.includes('=?');
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
||||||
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
||||||
@@ -188,7 +194,7 @@ export function buildHeaderValue(structured) {
|
|||||||
const value = structured.params[key];
|
const value = structured.params[key];
|
||||||
// a parameter name is a token too and it is emitted without any quoting around it
|
// a parameter name is a token too and it is emitted without any quoting around it
|
||||||
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
||||||
if (!isPlainText(value, true) || value.length >= 75) {
|
if (!isPlainParamValue(value) || value.length >= 75) {
|
||||||
buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
||||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||||
@@ -237,7 +243,7 @@ export function buildHeaderParam(key, data, maxLength) {
|
|||||||
let i, len;
|
let i, len;
|
||||||
maxLength = maxLength || 50;
|
maxLength = maxLength || 50;
|
||||||
// process ascii only text
|
// process ascii only text
|
||||||
if (isPlainText(data, true)) {
|
if (isPlainParamValue(data)) {
|
||||||
// check if conversion is even needed
|
// check if conversion is even needed
|
||||||
if (encodedStr.length <= maxLength) {
|
if (encodedStr.length <= maxLength) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+3
-12
@@ -1097,10 +1097,7 @@ const extensions = new Map([
|
|||||||
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
||||||
['bed', 'application/vnd.realvnc.bed'],
|
['bed', 'application/vnd.realvnc.bed'],
|
||||||
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
||||||
[
|
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
|
||||||
'bin',
|
|
||||||
['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
|
|
||||||
],
|
|
||||||
['bm', 'image/bmp'],
|
['bm', 'image/bmp'],
|
||||||
['bmi', 'application/vnd.bmi'],
|
['bmi', 'application/vnd.bmi'],
|
||||||
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
||||||
@@ -1145,10 +1142,7 @@ const extensions = new Map([
|
|||||||
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
||||||
['cil', 'application/vnd.ms-artgalry'],
|
['cil', 'application/vnd.ms-artgalry'],
|
||||||
['cla', 'application/vnd.claymore'],
|
['cla', 'application/vnd.claymore'],
|
||||||
[
|
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
|
||||||
'class',
|
|
||||||
['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
|
|
||||||
],
|
|
||||||
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
||||||
['clkp', 'application/vnd.crick.clicker.palette'],
|
['clkp', 'application/vnd.crick.clicker.palette'],
|
||||||
['clkt', 'application/vnd.crick.clicker.template'],
|
['clkt', 'application/vnd.crick.clicker.template'],
|
||||||
@@ -1753,10 +1747,7 @@ const extensions = new Map([
|
|||||||
['sbml', 'application/sbml+xml'],
|
['sbml', 'application/sbml+xml'],
|
||||||
['sc', 'application/vnd.ibm.secure-container'],
|
['sc', 'application/vnd.ibm.secure-container'],
|
||||||
['scd', 'application/x-msschedule'],
|
['scd', 'application/x-msschedule'],
|
||||||
[
|
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
|
||||||
'scm',
|
|
||||||
['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
|
|
||||||
],
|
|
||||||
['scq', 'application/scvp-cv-request'],
|
['scq', 'application/scvp-cv-request'],
|
||||||
['scs', 'application/scvp-cv-response'],
|
['scs', 'application/scvp-cv-response'],
|
||||||
['sct', 'text/scriptlet'],
|
['sct', 'text/scriptlet'],
|
||||||
|
|||||||
+34
-1
@@ -58,6 +58,13 @@ export interface MimeNodePreparedHeaderValue {
|
|||||||
* objects (for the address headers), a prepared value object, or a list of these
|
* objects (for the address headers), a prepared value object, or a list of these
|
||||||
*/
|
*/
|
||||||
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
|
export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
|
||||||
|
/**
|
||||||
|
* Options for the static MimeNode.encodeHeaderValue
|
||||||
|
*/
|
||||||
|
export interface MimeNodeHeaderEncodingOptions {
|
||||||
|
/** Either 'Q' (the default) or 'B' */
|
||||||
|
textEncoding?: string | undefined;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* A single header, the shape the headers are stored in and one of the list forms
|
* A single header, the shape the headers are stored in and one of the list forms
|
||||||
* setHeader and addHeader accept
|
* setHeader and addHeader accept
|
||||||
@@ -319,6 +326,31 @@ declare class MimeNode {
|
|||||||
* @param raw Raw MIME contents
|
* @param raw Raw MIME contents
|
||||||
*/
|
*/
|
||||||
setRaw(raw: MimeNodeContent): this;
|
setRaw(raw: MimeNodeContent): this;
|
||||||
|
/**
|
||||||
|
* Encodes a header value the way this node encodes it in the generated message:
|
||||||
|
* address headers are punycoded and mime encoded, message id headers get their
|
||||||
|
* angle brackets, a Date object is formatted and any other value is mime encoded
|
||||||
|
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
||||||
|
*
|
||||||
|
* The result is a single unfolded line without the key. Fold it before writing it
|
||||||
|
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
encodeHeaderValue(key: string, value: MimeNodeHeaderValue): string;
|
||||||
|
/**
|
||||||
|
* Encodes a header value without a node at hand. Same as the instance method of a
|
||||||
|
* node created with the given options.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @param [options] Encoding options
|
||||||
|
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
static encodeHeaderValue(key: string, value: MimeNodeHeaderValue, options?: MimeNodeHeaderEncodingOptions): string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
|
* Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
|
||||||
@@ -327,7 +359,8 @@ declare namespace MimeNode {
|
|||||||
type Options = MimeNodeOptions;
|
type Options = MimeNodeOptions;
|
||||||
type Addresses = MimeNodeAddresses;
|
type Addresses = MimeNodeAddresses;
|
||||||
type Envelope = MimeNodeEnvelope;
|
type Envelope = MimeNodeEnvelope;
|
||||||
|
type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
|
||||||
}
|
}
|
||||||
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
/** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
|
||||||
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope };
|
export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope, MimeNodeHeaderEncodingOptions as HeaderEncodingOptions };
|
||||||
export default MimeNode;
|
export default MimeNode;
|
||||||
+35
-13
@@ -117,9 +117,7 @@ function exceedsDomainLimits(domain) {
|
|||||||
*/
|
*/
|
||||||
function normalizeDomain(domain, toUnicode) {
|
function normalizeDomain(domain, toUnicode) {
|
||||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||||
if (typeof urlModule.domainToASCII === 'function' &&
|
if (typeof urlModule.domainToASCII === 'function' && typeof urlModule.domainToUnicode === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
||||||
typeof urlModule.domainToUnicode === 'function' &&
|
|
||||||
!URL_PARSER_UNSAFE.test(domain)) {
|
|
||||||
// The U-label form is decoded from the A-label form rather than from the input:
|
// The U-label form is decoded from the A-label form rather than from the input:
|
||||||
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
||||||
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
||||||
@@ -632,9 +630,7 @@ class MimeNode {
|
|||||||
// the checks miss and the header ends up claiming a type it is not set up for
|
// the checks miss and the header ends up claiming a type it is not set up for
|
||||||
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
||||||
this._handleContentType(structured);
|
this._handleContentType(structured);
|
||||||
if (structured.value.match(/^text\/plain\b/) &&
|
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
||||||
typeof this.content === 'string' &&
|
|
||||||
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
||||||
structured.params.charset = 'utf-8';
|
structured.params.charset = 'utf-8';
|
||||||
}
|
}
|
||||||
if (this.filename) {
|
if (this.filename) {
|
||||||
@@ -675,7 +671,7 @@ class MimeNode {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value = this._encodeHeaderValue(key, value);
|
value = this.encodeHeaderValue(key, value);
|
||||||
// skip empty lines
|
// skip empty lines
|
||||||
if (!(value || '').toString().trim()) {
|
if (!(value || '').toString().trim()) {
|
||||||
return;
|
return;
|
||||||
@@ -1293,8 +1289,7 @@ class MimeNode {
|
|||||||
// where it came from. MimeNode is exported and subclassable, so the generator is
|
// where it came from. MimeNode is exported and subclassable, so the generator is
|
||||||
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
||||||
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
||||||
this.boundary = structured.params.boundary =
|
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
||||||
declared || _stripBoundaryControls(this._generateBoundary());
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.boundary = false;
|
this.boundary = false;
|
||||||
@@ -1312,13 +1307,19 @@ class MimeNode {
|
|||||||
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Encodes a header value for use in the generated rfc2822 email.
|
* Encodes a header value the way this node encodes it in the generated message:
|
||||||
|
* address headers are punycoded and mime encoded, message id headers get their
|
||||||
|
* angle brackets, a Date object is formatted and any other value is mime encoded
|
||||||
|
* when it holds non-ascii or control characters (using the node's `textEncoding`).
|
||||||
*
|
*
|
||||||
* @param key Header key
|
* The result is a single unfolded line without the key. Fold it before writing it
|
||||||
|
* into a message, `key + ': ' + value` folded at 76 characters is what the node does.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
* @param value Header value
|
* @param value Header value
|
||||||
* @internal
|
* @returns Encoded header value
|
||||||
*/
|
*/
|
||||||
_encodeHeaderValue(key, value) {
|
encodeHeaderValue(key, value) {
|
||||||
key = this._normalizeHeaderKey(key);
|
key = this._normalizeHeaderKey(key);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
// Structured headers
|
// Structured headers
|
||||||
@@ -1383,6 +1384,27 @@ class MimeNode {
|
|||||||
return this._encodeHeaderText(value);
|
return this._encodeHeaderText(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Encodes a header value without a node at hand. Same as the instance method of a
|
||||||
|
* node created with the given options.
|
||||||
|
*
|
||||||
|
* @param key Header key, case insensitive
|
||||||
|
* @param value Header value
|
||||||
|
* @param [options] Encoding options
|
||||||
|
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
||||||
|
* @returns Encoded header value
|
||||||
|
*/
|
||||||
|
static encodeHeaderValue(key, value, options) {
|
||||||
|
return new MimeNode(false, { textEncoding: options?.textEncoding }).encodeHeaderValue(key, value);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Alias of encodeHeaderValue, kept for callers that used the method before it was public
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_encodeHeaderValue(key, value) {
|
||||||
|
return this.encodeHeaderValue(key, value);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Rebuilds address object using punycode and other adjustments
|
* Rebuilds address object using punycode and other adjustments
|
||||||
*
|
*
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
export declare const name = "nodemailer";
|
export declare const name = "nodemailer";
|
||||||
export declare const version = "10.1.6";
|
export declare const version = "10.2.0";
|
||||||
export declare const homepage = "https://nodemailer.com/";
|
export declare const homepage = "https://nodemailer.com/";
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
||||||
export const name = 'nodemailer';
|
export const name = 'nodemailer';
|
||||||
export const version = '10.1.6';
|
export const version = '10.2.0';
|
||||||
export const homepage = 'https://nodemailer.com/';
|
export const homepage = 'https://nodemailer.com/';
|
||||||
+3
-12
@@ -45,15 +45,9 @@ function encodeBytes(buffer, next, binary, previous) {
|
|||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const ord = buffer[i];
|
const ord = buffer[i];
|
||||||
const following = i + 1 < len ? buffer[i + 1] : next;
|
const following = i + 1 < len ? buffer[i + 1] : next;
|
||||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (ord === 0x0d ? following === 0x0a : (i > 0 ? buffer[i - 1] : previous) === 0x0d) : true;
|
||||||
? ord === 0x0d
|
|
||||||
? following === 0x0a
|
|
||||||
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
|
||||||
: true;
|
|
||||||
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
||||||
if (QP_LITERAL[ord] &&
|
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||||
lineBreakByte &&
|
|
||||||
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
|
||||||
output[pos++] = ord;
|
output[pos++] = ord;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -114,10 +108,7 @@ export function wrap(str, lineLength) {
|
|||||||
line = line.substr(0, line.length - match[0].length);
|
line = line.substr(0, line.length - match[0].length);
|
||||||
}
|
}
|
||||||
// ensure that utf-8 sequences are not split
|
// ensure that utf-8 sequences are not split
|
||||||
while (line.length > 3 &&
|
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
|
||||||
line.length < len - pos &&
|
|
||||||
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
|
||||||
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
|
||||||
code = parseInt(match[0].substr(1, 2), 16);
|
code = parseInt(match[0].substr(1, 2), 16);
|
||||||
if (code < 128) {
|
if (code < 128) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
+1
-3
@@ -71,9 +71,7 @@ class SendmailTransport {
|
|||||||
return done(err);
|
return done(err);
|
||||||
}
|
}
|
||||||
// force -i to keep single dots
|
// force -i to keep single dots
|
||||||
const args = this.args
|
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||||
? ['-i'].concat(this.args).concat(envelope.to)
|
|
||||||
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
|
||||||
const callback = (err) => {
|
const callback = (err) => {
|
||||||
if (returned) {
|
if (returned) {
|
||||||
// ignore any additional responses, already done
|
// ignore any additional responses, already done
|
||||||
|
|||||||
+2
-6
@@ -217,9 +217,7 @@ export const resolveHostname = (options, callback) => {
|
|||||||
return respond(err);
|
return respond(err);
|
||||||
}
|
}
|
||||||
// Get all supported addresses from dns.lookup
|
// Get all supported addresses from dns.lookup
|
||||||
const supportedAddresses = addresses
|
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
||||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
|
||||||
: [];
|
|
||||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||||
// there are addresses but none can be used
|
// there are addresses but none can be used
|
||||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||||
@@ -549,9 +547,7 @@ export const assign = function (...args) {
|
|||||||
if (isProtoKey(key)) {
|
if (isProtoKey(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['tls', 'auth'].includes(key) &&
|
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
||||||
source[key] &&
|
|
||||||
typeof source[key] === 'object') {
|
|
||||||
// tls and auth are special keys that need to be enumerated separately
|
// tls and auth are special keys that need to be enumerated separately
|
||||||
// other objects are passed as is. Enumerating is a copy of user supplied
|
// other objects are passed as is. Enumerating is a copy of user supplied
|
||||||
// keys just like the loop above, so it gets the same treatment
|
// keys just like the loop above, so it gets the same treatment
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import * as errors from '../errors.js';
|
|||||||
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
||||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||||
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
||||||
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
const unbracket = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
||||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
||||||
if (typeof tlsOptions === 'function') {
|
if (typeof tlsOptions === 'function') {
|
||||||
callback = tlsOptions;
|
callback = tlsOptions;
|
||||||
|
|||||||
+6
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
|||||||
messageSize: number;
|
messageSize: number;
|
||||||
/** Final server response for the message */
|
/** Final server response for the message */
|
||||||
response: string;
|
response: string;
|
||||||
|
/**
|
||||||
|
* Message-ID the server gave the message, with angle brackets, when its final reply names
|
||||||
|
* one in a known format (Microsoft 365 and Outlook.com, Amazon SES). Such a server replaces
|
||||||
|
* the Message-ID header, so the delivered copy carries this id instead of messageId
|
||||||
|
*/
|
||||||
|
serverMessageId?: string | undefined;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Callback for send(), receives the result once the server accepted the message. The error
|
* Callback for send(), receives the result once the server accepted the message. The error
|
||||||
|
|||||||
+16
-13
@@ -7,7 +7,8 @@ import crypto from 'node:crypto';
|
|||||||
import DataStream from './data-stream.js';
|
import DataStream from './data-stream.js';
|
||||||
import { PassThrough } from 'node:stream';
|
import { PassThrough } from 'node:stream';
|
||||||
import * as shared from '../shared/index.js';
|
import * as shared from '../shared/index.js';
|
||||||
import { ERR_ACCESS_DENIED, isTransientError } from '../errors.js';
|
import { parseServerMessageId } from './server-message-id.js';
|
||||||
|
import { ERR_ACCESS_DENIED, isPermanentResponseCode, isTransientError } from '../errors.js';
|
||||||
// default timeout values in ms
|
// default timeout values in ms
|
||||||
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
|
||||||
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
||||||
@@ -361,9 +362,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
if (this._destroyed || this._closing) {
|
if (this._destroyed || this._closing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const connectFn = secure
|
const connectFn = secure ? tls.connect : net.connect;
|
||||||
? tls.connect
|
|
||||||
: net.connect;
|
|
||||||
try {
|
try {
|
||||||
this._socket = connectFn(opts, () => this._onConnect());
|
this._socket = connectFn(opts, () => this._onConnect());
|
||||||
this._setupConnectionHandlers();
|
this._setupConnectionHandlers();
|
||||||
@@ -701,6 +700,16 @@ class SMTPConnection extends EventEmitter {
|
|||||||
result.messageTime = Date.now() - envelopeTime;
|
result.messageTime = Date.now() - envelopeTime;
|
||||||
result.messageSize = stream.outByteCount;
|
result.messageSize = stream.outByteCount;
|
||||||
result.response = str;
|
result.response = str;
|
||||||
|
const serverMessageId = parseServerMessageId(result.response, this.options.host);
|
||||||
|
if (serverMessageId) {
|
||||||
|
result.serverMessageId = serverMessageId;
|
||||||
|
}
|
||||||
|
// an LMTP server answers the message once per recipient, so recipients can be
|
||||||
|
// refused after the envelope info was built without any rejected errors on it
|
||||||
|
const rejectedErrors = this._envelope?.rejectedErrors;
|
||||||
|
if (!result.rejectedErrors && rejectedErrors && rejectedErrors.length) {
|
||||||
|
result.rejectedErrors = rejectedErrors;
|
||||||
|
}
|
||||||
return callback(null, result);
|
return callback(null, result);
|
||||||
});
|
});
|
||||||
if (typeof message.pipe === 'function') {
|
if (typeof message.pipe === 'function') {
|
||||||
@@ -928,6 +937,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
||||||
if (responseCode) {
|
if (responseCode) {
|
||||||
err.responseCode = responseCode;
|
err.responseCode = responseCode;
|
||||||
|
err.permanent = isPermanentResponseCode(responseCode);
|
||||||
}
|
}
|
||||||
if (command) {
|
if (command) {
|
||||||
err.command = command;
|
err.command = command;
|
||||||
@@ -970,12 +980,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!failureResponse &&
|
if (!failureResponse && this.stage === 'connected' && !this._responseActions.length && !this._pendingSend && !this._destroyed && !this._closing) {
|
||||||
this.stage === 'connected' &&
|
|
||||||
!this._responseActions.length &&
|
|
||||||
!this._pendingSend &&
|
|
||||||
!this._destroyed &&
|
|
||||||
!this._closing) {
|
|
||||||
// nothing was waiting for the server, so this is the server ending an idle session
|
// nothing was waiting for the server, so this is the server ending an idle session
|
||||||
// (usually after its idle timeout), not a failure
|
// (usually after its idle timeout), not a failure
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
@@ -1196,9 +1201,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
const args = [];
|
const args = [];
|
||||||
let useSmtpUtf8 = false;
|
let useSmtpUtf8 = false;
|
||||||
this._envelope = (envelope || {});
|
this._envelope = (envelope || {});
|
||||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
this._envelope.to = []
|
this._envelope.to = []
|
||||||
.concat(this._envelope.to || [])
|
.concat(this._envelope.to || [])
|
||||||
.map(to => ((to && to.address) || to || '').toString().trim());
|
.map(to => ((to && to.address) || to || '').toString().trim());
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
||||||
|
* servers replace the Message-ID header of a submitted message with one of their own, and
|
||||||
|
* the copy the recipients and the sent folder get carries that one instead of the id the
|
||||||
|
* message was built with. Only replies in a format known to carry such an id are read.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Reads the server assigned Message-ID out of the final reply to a message
|
||||||
|
*
|
||||||
|
* @param response Final server reply for the message
|
||||||
|
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
||||||
|
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
||||||
|
*/
|
||||||
|
export declare function parseServerMessageId(response: string | undefined, host?: string): string | undefined;
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
|
||||||
|
* servers replace the Message-ID header of a submitted message with one of their own, and
|
||||||
|
* the copy the recipients and the sent folder get carries that one instead of the id the
|
||||||
|
* message was built with. Only replies in a format known to carry such an id are read.
|
||||||
|
*/
|
||||||
|
// Microsoft 365 and Outlook.com: "250 2.0.0 OK <id@host.prod.outlook.com> [Hostname=...]"
|
||||||
|
const OUTLOOK_REPLY = /^250 2\.0\.0 OK (<[^<>\s]+\.prod\.outlook\.com>)/i;
|
||||||
|
// Amazon SES: "250 Ok 0100018b5c3c0a4f-...-000000", the local part of the Message-ID it sets
|
||||||
|
const SES_REPLY = /^250 Ok ([0-9a-f]+(?:-[0-9a-f]+)+)$/i;
|
||||||
|
// an AWS region name such as us-east-1 or ap-southeast-2
|
||||||
|
const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
||||||
|
/**
|
||||||
|
* Finds the AWS region an SES SMTP endpoint is in from its hostname, such as
|
||||||
|
* email-smtp.eu-west-1.amazonaws.com or a VPC endpoint name under it
|
||||||
|
*
|
||||||
|
* @param host SMTP hostname
|
||||||
|
* @returns Region name, or undefined for a host outside amazonaws.com and awsapps.com
|
||||||
|
*/
|
||||||
|
function sesRegion(host) {
|
||||||
|
const labels = host.toLowerCase().replace(/\.$/, '').split('.');
|
||||||
|
const domain = labels.slice(-2).join('.');
|
||||||
|
if (domain !== 'amazonaws.com' && domain !== 'awsapps.com') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return labels.slice(0, -2).find(label => AWS_REGION.test(label));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reads the server assigned Message-ID out of the final reply to a message
|
||||||
|
*
|
||||||
|
* @param response Final server reply for the message
|
||||||
|
* @param [host] Hostname the connection was made to, SES replies are only read for an SES host
|
||||||
|
* @returns The Message-ID with its angle brackets, or undefined when the reply names none
|
||||||
|
*/
|
||||||
|
export function parseServerMessageId(response, host) {
|
||||||
|
const reply = (response || '').toString().trim();
|
||||||
|
const outlook = reply.match(OUTLOOK_REPLY);
|
||||||
|
if (outlook) {
|
||||||
|
return outlook[1];
|
||||||
|
}
|
||||||
|
const ses = reply.match(SES_REPLY);
|
||||||
|
if (ses && host) {
|
||||||
|
const region = sesRegion(host);
|
||||||
|
if (region) {
|
||||||
|
// the SES Message-ID domain is email.amazonses.com in us-east-1 and the region elsewhere
|
||||||
|
return '<' + ses[1].toLowerCase() + '@' + (region === 'us-east-1' ? 'email' : region) + '.amazonses.com>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
+11
@@ -110,6 +110,17 @@ declare class SMTPPool extends EventEmitter {
|
|||||||
* Returns true if there are free slots in the queue
|
* Returns true if there are free slots in the queue
|
||||||
*/
|
*/
|
||||||
isIdle(): boolean;
|
isIdle(): boolean;
|
||||||
|
/**
|
||||||
|
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
||||||
|
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
||||||
|
* pool can take another message, it is already true while a message is still being sent,
|
||||||
|
* so use this to check that nothing would be lost by closing the pool. A message that is
|
||||||
|
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
||||||
|
* it is no longer counted by the time its callback runs.
|
||||||
|
*
|
||||||
|
* @returns Number of queued and in-flight messages
|
||||||
|
*/
|
||||||
|
pendingCount(): number;
|
||||||
/**
|
/**
|
||||||
* Verifies SMTP configuration
|
* Verifies SMTP configuration
|
||||||
*
|
*
|
||||||
|
|||||||
+25
-1
@@ -62,6 +62,7 @@ class SMTPPool extends EventEmitter {
|
|||||||
this._queue = [];
|
this._queue = [];
|
||||||
this._connections = [];
|
this._connections = [];
|
||||||
this._connectionCounter = 0;
|
this._connectionCounter = 0;
|
||||||
|
this._pending = 0;
|
||||||
this.idling = true;
|
this.idling = true;
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
if (this.idling) {
|
if (this.idling) {
|
||||||
@@ -94,10 +95,20 @@ class SMTPPool extends EventEmitter {
|
|||||||
setImmediate(() => callback(err));
|
setImmediate(() => callback(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// counted until the callback runs, whichever path runs it: delivered, failed, requeued
|
||||||
|
// until it gives up, or drained by close()
|
||||||
|
let counted = true;
|
||||||
|
this._pending++;
|
||||||
this._queue.push({
|
this._queue.push({
|
||||||
mail,
|
mail,
|
||||||
requeueAttempts: 0,
|
requeueAttempts: 0,
|
||||||
callback
|
callback: (err, info) => {
|
||||||
|
if (counted) {
|
||||||
|
counted = false;
|
||||||
|
this._pending--;
|
||||||
|
}
|
||||||
|
callback(err, info);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
||||||
this.idling = false;
|
this.idling = false;
|
||||||
@@ -441,6 +452,19 @@ class SMTPPool extends EventEmitter {
|
|||||||
isIdle() {
|
isIdle() {
|
||||||
return this.idling;
|
return this.idling;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Counts the messages the pool has not finished with yet: the ones waiting in the queue
|
||||||
|
* plus the ones a connection is transmitting right now. isIdle() only tells whether the
|
||||||
|
* pool can take another message, it is already true while a message is still being sent,
|
||||||
|
* so use this to check that nothing would be lost by closing the pool. A message that is
|
||||||
|
* re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
|
||||||
|
* it is no longer counted by the time its callback runs.
|
||||||
|
*
|
||||||
|
* @returns Number of queued and in-flight messages
|
||||||
|
*/
|
||||||
|
pendingCount() {
|
||||||
|
return this._pending;
|
||||||
|
}
|
||||||
verify(callback) {
|
verify(callback) {
|
||||||
let promise;
|
let promise;
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
|
|||||||
+2
-5
@@ -19,8 +19,7 @@ export default class PoolResource extends EventEmitter {
|
|||||||
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
||||||
case 'OAUTH2': {
|
case 'OAUTH2': {
|
||||||
const oauth2 = new XOAuth2(this.options.auth, this.logger);
|
const oauth2 = new XOAuth2(this.options.auth, this.logger);
|
||||||
oauth2.provisionCallback =
|
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||||
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
|
||||||
this.auth = {
|
this.auth = {
|
||||||
type: 'OAUTH2',
|
type: 'OAUTH2',
|
||||||
user: this.options.auth.user,
|
user: this.options.auth.user,
|
||||||
@@ -208,9 +207,7 @@ export default class PoolResource extends EventEmitter {
|
|||||||
this.connection.close();
|
this.connection.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
||||||
err.responseCode !== 421 &&
|
|
||||||
!this.connection._destroyed) {
|
|
||||||
// The server refused this message, the connection itself is fine. Reset the
|
// The server refused this message, the connection itself is fine. Reset the
|
||||||
// session and keep using it instead of opening a new one
|
// session and keep using it instead of opening a new one
|
||||||
this.connection.reset(resetErr => {
|
this.connection.reset(resetErr => {
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemailer",
|
"name": "nodemailer",
|
||||||
"version": "10.1.6",
|
"version": "10.2.0",
|
||||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/cjs/nodemailer.js",
|
"main": "./dist/cjs/nodemailer.js",
|
||||||
@@ -128,6 +128,8 @@
|
|||||||
"test": "node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
"test": "node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
||||||
"pretest:coverage": "npm run build",
|
"pretest:coverage": "npm run build",
|
||||||
"test:coverage": "c8 node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
"test:coverage": "c8 node --import tsx --test --test-concurrency=1 $(find test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
||||||
|
"pretest:cjs": "npm run build",
|
||||||
|
"test:cjs": "NODE_OPTIONS=--no-experimental-require-module node test/package/cjs-smoke.cjs",
|
||||||
"pretest:bun": "npm run build",
|
"pretest:bun": "npm run build",
|
||||||
"test:bun": "bun test --timeout 60000 $(find ./test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
"test:bun": "bun test --timeout 60000 $(find ./test \\( -name '*-test.ts' -o -name '*.test.ts' \\))",
|
||||||
"format": "prettier --write \"**/*.{js,cjs,ts,json,md}\"",
|
"format": "prettier --write \"**/*.{js,cjs,ts,json,md}\"",
|
||||||
@@ -155,7 +157,7 @@
|
|||||||
"@types/node": "20.19.43",
|
"@types/node": "20.19.43",
|
||||||
"bunyan": "1.8.15",
|
"bunyan": "1.8.15",
|
||||||
"c8": "12.0.0",
|
"c8": "12.0.0",
|
||||||
"eslint": "10.12.0",
|
"eslint": "10.13.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"globals": "17.13.0",
|
"globals": "17.13.0",
|
||||||
"libbase64": "1.3.2",
|
"libbase64": "1.3.2",
|
||||||
|
|||||||
Reference in new issue
Block a user