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
+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
|
||||
// its own, with the quoting put back on, and takes its turn like any other
|
||||
const local = data.quotedLocalAt[i];
|
||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
|
||||
? _quoteLocalPart(data.text[i])
|
||||
: '';
|
||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
|
||||
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
||||
data.address = [quotedLocal || candidates[i]];
|
||||
data.text.splice(i, 1);
|
||||
@@ -1020,8 +1018,7 @@ class Tokenizer {
|
||||
return;
|
||||
}
|
||||
if (this.node < 0) {
|
||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
||||
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||
}
|
||||
if (chr === '\n') {
|
||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||
|
||||
+1
-3
@@ -25,9 +25,7 @@ class DKIMSigner {
|
||||
this.chunks = [];
|
||||
this.chunklen = 0;
|
||||
this.readPos = 0;
|
||||
this.cachePath = this.cacheDir
|
||||
? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex'))
|
||||
: false;
|
||||
this.cachePath = this.cacheDir ? node_path_1.default.join(this.cacheDir, 'message.' + Date.now() + '-' + node_crypto_1.default.randomBytes(14).toString('hex')) : false;
|
||||
this.cache = false;
|
||||
this.headers = false;
|
||||
this.bodyHash = false;
|
||||
|
||||
+19
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
|
||||
code?: string | undefined;
|
||||
responseCode?: number | undefined;
|
||||
}) => 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
|
||||
* 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;
|
||||
/** Numeric SMTP response code */
|
||||
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 */
|
||||
sourceUrl?: string | undefined;
|
||||
/** Recipient address the error applies to */
|
||||
|
||||
+14
-1
@@ -10,7 +10,7 @@
|
||||
* err.code = errors.ECONNECTION;
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -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;
|
||||
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
|
||||
* 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
|
||||
// .foo.com also matches subdomains, foo.com does not
|
||||
if (urlparts.hostname !== cookie.domain &&
|
||||
(cookie.domain.charAt(0) !== '.' ||
|
||||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||
return false;
|
||||
}
|
||||
// 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
|
||||
const pathname = urlparts.pathname || '/';
|
||||
const cookiePath = cookie.path;
|
||||
const pathMatches = pathname === cookiePath ||
|
||||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||
const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||
if (!pathMatches) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+2
-8
@@ -262,11 +262,7 @@ function nmfetch(url, options) {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (parsed.protocol === 'https:' &&
|
||||
parsed.hostname &&
|
||||
parsed.hostname !== reqOptions.host &&
|
||||
!node_net_1.default.isIP(parsed.hostname) &&
|
||||
!reqOptions.servername) {
|
||||
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !node_net_1.default.isIP(parsed.hostname) && !reqOptions.servername) {
|
||||
reqOptions.servername = parsed.hostname;
|
||||
}
|
||||
try {
|
||||
@@ -369,9 +365,7 @@ function nmfetch(url, options) {
|
||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||
// request headers so an attacker who controls the redirect target cannot
|
||||
// harvest them.
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
||||
redirectParsed.hostname !== parsed.hostname ||
|
||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
if (options.headers && crossOrigin) {
|
||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
|
||||
+42
-9
@@ -54,6 +54,40 @@ function isContentObject(value) {
|
||||
const content = value;
|
||||
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 {
|
||||
constructor(mail) {
|
||||
this.mail = mail || {};
|
||||
@@ -136,8 +170,11 @@ class MailComposer {
|
||||
contentTransferEncoding = attachment.contentTransferEncoding;
|
||||
}
|
||||
else if (isMessageNode) {
|
||||
// the content might include non-ASCII bytes but at this point we do not know it yet
|
||||
contentTransferEncoding = '8bit';
|
||||
// a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
|
||||
// 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 {
|
||||
contentTransferEncoding = 'base64'; // the default
|
||||
@@ -152,9 +189,7 @@ class MailComposer {
|
||||
}
|
||||
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
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
@@ -315,8 +350,7 @@ class MailComposer {
|
||||
if (this.mail.icalEvent) {
|
||||
eventObject = Object.assign({}, this._getIcalEvent());
|
||||
eventObject.filename = false;
|
||||
eventObject.contentType =
|
||||
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
@@ -344,8 +378,7 @@ class MailComposer {
|
||||
alternative = this._processDataUrl(alternative);
|
||||
}
|
||||
const data = {
|
||||
contentType: alternative.contentType ||
|
||||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentTransferEncoding: alternative.contentTransferEncoding
|
||||
};
|
||||
if (alternative.filename) {
|
||||
|
||||
+5
-6
@@ -109,8 +109,7 @@ class MailMessage {
|
||||
if (!attachment.filename) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
}
|
||||
@@ -245,7 +244,7 @@ class MailMessage {
|
||||
value = (value && value.value) || value;
|
||||
if (value) {
|
||||
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;
|
||||
}
|
||||
@@ -257,10 +256,10 @@ class MailMessage {
|
||||
});
|
||||
}
|
||||
if (data.references) {
|
||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
||||
data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
|
||||
}
|
||||
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);
|
||||
});
|
||||
@@ -329,7 +328,7 @@ class MailMessage {
|
||||
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
||||
// List-ID expects a bare domain-like identifier, so strip the
|
||||
// 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)
|
||||
// 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]/;
|
||||
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
|
||||
* 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];
|
||||
// a parameter name is a token too and it is emitted without any quoting around it
|
||||
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 => {
|
||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||
@@ -286,7 +292,7 @@ function buildHeaderParam(key, data, maxLength) {
|
||||
let i, len;
|
||||
maxLength = maxLength || 50;
|
||||
// process ascii only text
|
||||
if (isPlainText(data, true)) {
|
||||
if (isPlainParamValue(data)) {
|
||||
// check if conversion is even needed
|
||||
if (encodedStr.length <= maxLength) {
|
||||
return [
|
||||
|
||||
+3
-12
@@ -1104,10 +1104,7 @@ const extensions = new Map([
|
||||
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
||||
['bed', 'application/vnd.realvnc.bed'],
|
||||
['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'],
|
||||
['bmi', 'application/vnd.bmi'],
|
||||
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
||||
@@ -1152,10 +1149,7 @@ const extensions = new Map([
|
||||
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
||||
['cil', 'application/vnd.ms-artgalry'],
|
||||
['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'],
|
||||
['clkp', 'application/vnd.crick.clicker.palette'],
|
||||
['clkt', 'application/vnd.crick.clicker.template'],
|
||||
@@ -1760,10 +1754,7 @@ const extensions = new Map([
|
||||
['sbml', 'application/sbml+xml'],
|
||||
['sc', 'application/vnd.ibm.secure-container'],
|
||||
['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'],
|
||||
['scs', 'application/scvp-cv-response'],
|
||||
['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
|
||||
*/
|
||||
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
|
||||
* setHeader and addHeader accept
|
||||
@@ -319,6 +326,31 @@ declare class MimeNode {
|
||||
* @param raw Raw MIME contents
|
||||
*/
|
||||
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
|
||||
@@ -327,7 +359,8 @@ declare namespace MimeNode {
|
||||
type Options = MimeNodeOptions;
|
||||
type Addresses = MimeNodeAddresses;
|
||||
type Envelope = MimeNodeEnvelope;
|
||||
type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
|
||||
}
|
||||
/** 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;
|
||||
+35
-13
@@ -155,9 +155,7 @@ function exceedsDomainLimits(domain) {
|
||||
*/
|
||||
function normalizeDomain(domain, toUnicode) {
|
||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||
if (typeof node_url_1.default.domainToASCII === 'function' &&
|
||||
typeof node_url_1.default.domainToUnicode === 'function' &&
|
||||
!URL_PARSER_UNSAFE.test(domain)) {
|
||||
if (typeof node_url_1.default.domainToASCII === 'function' && 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:
|
||||
// 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
|
||||
@@ -670,9 +668,7 @@ class MimeNode {
|
||||
// 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, '');
|
||||
this._handleContentType(structured);
|
||||
if (structured.value.match(/^text\/plain\b/) &&
|
||||
typeof this.content === 'string' &&
|
||||
/[\u0080-\uFFFF]/.test(this.content)) {
|
||||
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
||||
structured.params.charset = 'utf-8';
|
||||
}
|
||||
if (this.filename) {
|
||||
@@ -713,7 +709,7 @@ class MimeNode {
|
||||
}
|
||||
break;
|
||||
}
|
||||
value = this._encodeHeaderValue(key, value);
|
||||
value = this.encodeHeaderValue(key, value);
|
||||
// skip empty lines
|
||||
if (!(value || '').toString().trim()) {
|
||||
return;
|
||||
@@ -1331,8 +1327,7 @@ class MimeNode {
|
||||
// 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.
|
||||
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
||||
this.boundary = structured.params.boundary =
|
||||
declared || _stripBoundaryControls(this._generateBoundary());
|
||||
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
||||
}
|
||||
else {
|
||||
this.boundary = false;
|
||||
@@ -1350,13 +1345,19 @@ class MimeNode {
|
||||
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
|
||||
* @internal
|
||||
* @returns Encoded header value
|
||||
*/
|
||||
_encodeHeaderValue(key, value) {
|
||||
encodeHeaderValue(key, value) {
|
||||
key = this._normalizeHeaderKey(key);
|
||||
switch (key) {
|
||||
// Structured headers
|
||||
@@ -1421,6 +1422,27 @@ class MimeNode {
|
||||
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
|
||||
*
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
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/";
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.homepage = exports.version = exports.name = void 0;
|
||||
exports.name = 'nodemailer';
|
||||
exports.version = '10.1.6';
|
||||
exports.version = '10.2.0';
|
||||
exports.homepage = 'https://nodemailer.com/';
|
||||
+3
-12
@@ -50,15 +50,9 @@ function encodeBytes(buffer, next, binary, previous) {
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ord = buffer[i];
|
||||
const following = i + 1 < len ? buffer[i + 1] : next;
|
||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
||||
? ord === 0x0d
|
||||
? following === 0x0a
|
||||
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
||||
: true;
|
||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (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 (QP_LITERAL[ord] &&
|
||||
lineBreakByte &&
|
||||
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||
output[pos++] = ord;
|
||||
continue;
|
||||
}
|
||||
@@ -119,10 +113,7 @@ function wrap(str, lineLength) {
|
||||
line = line.substr(0, line.length - match[0].length);
|
||||
}
|
||||
// ensure that utf-8 sequences are not split
|
||||
while (line.length > 3 &&
|
||||
line.length < len - pos &&
|
||||
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
||||
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
||||
while (line.length > 3 && 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);
|
||||
if (code < 128) {
|
||||
break;
|
||||
|
||||
+1
-3
@@ -109,9 +109,7 @@ class SendmailTransport {
|
||||
return done(err);
|
||||
}
|
||||
// force -i to keep single dots
|
||||
const args = this.args
|
||||
? ['-i'].concat(this.args).concat(envelope.to)
|
||||
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||
const callback = (err) => {
|
||||
if (returned) {
|
||||
// ignore any additional responses, already done
|
||||
|
||||
+2
-6
@@ -263,9 +263,7 @@ const resolveHostname = (options, callback) => {
|
||||
return respond(err);
|
||||
}
|
||||
// Get all supported addresses from dns.lookup
|
||||
const supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
if (['tls', 'auth'].includes(key) &&
|
||||
source[key] &&
|
||||
typeof source[key] === 'object') {
|
||||
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
||||
// 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
|
||||
// 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.
|
||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||
// 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) {
|
||||
if (typeof tlsOptions === 'function') {
|
||||
callback = tlsOptions;
|
||||
|
||||
+6
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
||||
messageSize: number;
|
||||
/** Final server response for the message */
|
||||
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
|
||||
|
||||
+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 node_stream_1 = require("node:stream");
|
||||
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");
|
||||
// default timeout values in ms
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const connectFn = secure
|
||||
? node_tls_1.default.connect
|
||||
: node_net_1.default.connect;
|
||||
const connectFn = secure ? node_tls_1.default.connect : node_net_1.default.connect;
|
||||
try {
|
||||
this._socket = connectFn(opts, () => this._onConnect());
|
||||
this._setupConnectionHandlers();
|
||||
@@ -739,6 +738,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
result.messageTime = Date.now() - envelopeTime;
|
||||
result.messageSize = stream.outByteCount;
|
||||
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);
|
||||
});
|
||||
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;
|
||||
if (responseCode) {
|
||||
err.responseCode = responseCode;
|
||||
err.permanent = (0, errors_js_1.isPermanentResponseCode)(responseCode);
|
||||
}
|
||||
if (command) {
|
||||
err.command = command;
|
||||
@@ -1008,12 +1018,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (!failureResponse &&
|
||||
this.stage === 'connected' &&
|
||||
!this._responseActions.length &&
|
||||
!this._pendingSend &&
|
||||
!this._destroyed &&
|
||||
!this._closing) {
|
||||
if (!failureResponse && 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
|
||||
// (usually after its idle timeout), not a failure
|
||||
this.logger.info({
|
||||
@@ -1234,9 +1239,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
const args = [];
|
||||
let useSmtpUtf8 = false;
|
||||
this._envelope = (envelope || {});
|
||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
||||
.toString()
|
||||
.trim();
|
||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
||||
this._envelope.to = []
|
||||
.concat(this._envelope.to || [])
|
||||
.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
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
||||
+25
-1
@@ -100,6 +100,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
this._queue = [];
|
||||
this._connections = [];
|
||||
this._connectionCounter = 0;
|
||||
this._pending = 0;
|
||||
this.idling = true;
|
||||
setImmediate(() => {
|
||||
if (this.idling) {
|
||||
@@ -132,10 +133,20 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
setImmediate(() => callback(err));
|
||||
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({
|
||||
mail,
|
||||
requeueAttempts: 0,
|
||||
callback
|
||||
callback: (err, info) => {
|
||||
if (counted) {
|
||||
counted = false;
|
||||
this._pending--;
|
||||
}
|
||||
callback(err, info);
|
||||
}
|
||||
});
|
||||
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
||||
this.idling = false;
|
||||
@@ -479,6 +490,19 @@ class SMTPPool extends node_events_1.EventEmitter {
|
||||
isIdle() {
|
||||
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) {
|
||||
let promise;
|
||||
if (!callback) {
|
||||
|
||||
+2
-5
@@ -57,8 +57,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
||||
case 'OAUTH2': {
|
||||
const oauth2 = new index_js_3.default(this.options.auth, this.logger);
|
||||
oauth2.provisionCallback =
|
||||
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
this.auth = {
|
||||
type: 'OAUTH2',
|
||||
user: this.options.auth.user,
|
||||
@@ -246,9 +245,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
||||
this.connection.close();
|
||||
return;
|
||||
}
|
||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
||||
err.responseCode !== 421 &&
|
||||
!this.connection._destroyed) {
|
||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
||||
// The server refused this message, the connection itself is fine. Reset the
|
||||
// session and keep using it instead of opening a new one
|
||||
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
|
||||
// its own, with the quoting put back on, and takes its turn like any other
|
||||
const local = data.quotedLocalAt[i];
|
||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
|
||||
? _quoteLocalPart(data.text[i])
|
||||
: '';
|
||||
const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
|
||||
if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
|
||||
data.address = [quotedLocal || candidates[i]];
|
||||
data.text.splice(i, 1);
|
||||
@@ -1018,8 +1016,7 @@ class Tokenizer {
|
||||
return;
|
||||
}
|
||||
if (this.node < 0) {
|
||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
||||
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
||||
}
|
||||
if (chr === '\n') {
|
||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||
|
||||
+1
-3
@@ -20,9 +20,7 @@ class DKIMSigner {
|
||||
this.chunks = [];
|
||||
this.chunklen = 0;
|
||||
this.readPos = 0;
|
||||
this.cachePath = this.cacheDir
|
||||
? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
|
||||
: false;
|
||||
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
|
||||
this.cache = false;
|
||||
this.headers = false;
|
||||
this.bodyHash = false;
|
||||
|
||||
+19
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
|
||||
code?: string | undefined;
|
||||
responseCode?: number | undefined;
|
||||
}) => 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
|
||||
* 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;
|
||||
/** Numeric SMTP response code */
|
||||
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 */
|
||||
sourceUrl?: string | undefined;
|
||||
/** 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
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
// .foo.com also matches subdomains, foo.com does not
|
||||
if (urlparts.hostname !== cookie.domain &&
|
||||
(cookie.domain.charAt(0) !== '.' ||
|
||||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||
return false;
|
||||
}
|
||||
// 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
|
||||
const pathname = urlparts.pathname || '/';
|
||||
const cookiePath = cookie.path;
|
||||
const pathMatches = pathname === cookiePath ||
|
||||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||
const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||
if (!pathMatches) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+2
-8
@@ -224,11 +224,7 @@ function nmfetch(url, options) {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (parsed.protocol === 'https:' &&
|
||||
parsed.hostname &&
|
||||
parsed.hostname !== reqOptions.host &&
|
||||
!net.isIP(parsed.hostname) &&
|
||||
!reqOptions.servername) {
|
||||
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
||||
reqOptions.servername = parsed.hostname;
|
||||
}
|
||||
try {
|
||||
@@ -331,9 +327,7 @@ function nmfetch(url, options) {
|
||||
// downgrade would also put them on the wire in cleartext). Strip sensitive
|
||||
// request headers so an attacker who controls the redirect target cannot
|
||||
// harvest them.
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
|
||||
redirectParsed.hostname !== parsed.hostname ||
|
||||
fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
|
||||
if (options.headers && crossOrigin) {
|
||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
|
||||
+42
-9
@@ -16,6 +16,40 @@ function isContentObject(value) {
|
||||
const content = value;
|
||||
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 {
|
||||
constructor(mail) {
|
||||
this.mail = mail || {};
|
||||
@@ -98,8 +132,11 @@ class MailComposer {
|
||||
contentTransferEncoding = attachment.contentTransferEncoding;
|
||||
}
|
||||
else if (isMessageNode) {
|
||||
// the content might include non-ASCII bytes but at this point we do not know it yet
|
||||
contentTransferEncoding = '8bit';
|
||||
// a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
|
||||
// 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 {
|
||||
contentTransferEncoding = 'base64'; // the default
|
||||
@@ -114,9 +151,7 @@ class MailComposer {
|
||||
}
|
||||
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
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
@@ -277,8 +312,7 @@ class MailComposer {
|
||||
if (this.mail.icalEvent) {
|
||||
eventObject = Object.assign({}, this._getIcalEvent());
|
||||
eventObject.filename = false;
|
||||
eventObject.contentType =
|
||||
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
@@ -306,8 +340,7 @@ class MailComposer {
|
||||
alternative = this._processDataUrl(alternative);
|
||||
}
|
||||
const data = {
|
||||
contentType: alternative.contentType ||
|
||||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentTransferEncoding: alternative.contentTransferEncoding
|
||||
};
|
||||
if (alternative.filename) {
|
||||
|
||||
+5
-6
@@ -71,8 +71,7 @@ export default class MailMessage {
|
||||
if (!attachment.filename) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
}
|
||||
@@ -207,7 +206,7 @@ export default class MailMessage {
|
||||
value = (value && value.value) || value;
|
||||
if (value) {
|
||||
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;
|
||||
}
|
||||
@@ -219,10 +218,10 @@ export default class MailMessage {
|
||||
});
|
||||
}
|
||||
if (data.references) {
|
||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
||||
data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
|
||||
}
|
||||
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);
|
||||
});
|
||||
@@ -291,7 +290,7 @@ export default class MailMessage {
|
||||
comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
|
||||
// List-ID expects a bare domain-like identifier, so strip the
|
||||
// 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)
|
||||
// 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]/;
|
||||
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
|
||||
* 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];
|
||||
// a parameter name is a token too and it is emitted without any quoting around it
|
||||
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 => {
|
||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||
@@ -237,7 +243,7 @@ export function buildHeaderParam(key, data, maxLength) {
|
||||
let i, len;
|
||||
maxLength = maxLength || 50;
|
||||
// process ascii only text
|
||||
if (isPlainText(data, true)) {
|
||||
if (isPlainParamValue(data)) {
|
||||
// check if conversion is even needed
|
||||
if (encodedStr.length <= maxLength) {
|
||||
return [
|
||||
|
||||
+3
-12
@@ -1097,10 +1097,7 @@ const extensions = new Map([
|
||||
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
||||
['bed', 'application/vnd.realvnc.bed'],
|
||||
['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'],
|
||||
['bmi', 'application/vnd.bmi'],
|
||||
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
||||
@@ -1145,10 +1142,7 @@ const extensions = new Map([
|
||||
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
||||
['cil', 'application/vnd.ms-artgalry'],
|
||||
['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'],
|
||||
['clkp', 'application/vnd.crick.clicker.palette'],
|
||||
['clkt', 'application/vnd.crick.clicker.template'],
|
||||
@@ -1753,10 +1747,7 @@ const extensions = new Map([
|
||||
['sbml', 'application/sbml+xml'],
|
||||
['sc', 'application/vnd.ibm.secure-container'],
|
||||
['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'],
|
||||
['scs', 'application/scvp-cv-response'],
|
||||
['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
|
||||
*/
|
||||
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
|
||||
* setHeader and addHeader accept
|
||||
@@ -319,6 +326,31 @@ declare class MimeNode {
|
||||
* @param raw Raw MIME contents
|
||||
*/
|
||||
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
|
||||
@@ -327,7 +359,8 @@ declare namespace MimeNode {
|
||||
type Options = MimeNodeOptions;
|
||||
type Addresses = MimeNodeAddresses;
|
||||
type Envelope = MimeNodeEnvelope;
|
||||
type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
|
||||
}
|
||||
/** 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;
|
||||
+35
-13
@@ -117,9 +117,7 @@ function exceedsDomainLimits(domain) {
|
||||
*/
|
||||
function normalizeDomain(domain, toUnicode) {
|
||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||
if (typeof urlModule.domainToASCII === 'function' &&
|
||||
typeof urlModule.domainToUnicode === 'function' &&
|
||||
!URL_PARSER_UNSAFE.test(domain)) {
|
||||
if (typeof urlModule.domainToASCII === 'function' && 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:
|
||||
// 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
|
||||
@@ -632,9 +630,7 @@ class MimeNode {
|
||||
// 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, '');
|
||||
this._handleContentType(structured);
|
||||
if (structured.value.match(/^text\/plain\b/) &&
|
||||
typeof this.content === 'string' &&
|
||||
/[\u0080-\uFFFF]/.test(this.content)) {
|
||||
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
||||
structured.params.charset = 'utf-8';
|
||||
}
|
||||
if (this.filename) {
|
||||
@@ -675,7 +671,7 @@ class MimeNode {
|
||||
}
|
||||
break;
|
||||
}
|
||||
value = this._encodeHeaderValue(key, value);
|
||||
value = this.encodeHeaderValue(key, value);
|
||||
// skip empty lines
|
||||
if (!(value || '').toString().trim()) {
|
||||
return;
|
||||
@@ -1293,8 +1289,7 @@ class MimeNode {
|
||||
// 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.
|
||||
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
||||
this.boundary = structured.params.boundary =
|
||||
declared || _stripBoundaryControls(this._generateBoundary());
|
||||
this.boundary = structured.params.boundary = declared || _stripBoundaryControls(this._generateBoundary());
|
||||
}
|
||||
else {
|
||||
this.boundary = false;
|
||||
@@ -1312,13 +1307,19 @@ class MimeNode {
|
||||
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
|
||||
* @internal
|
||||
* @returns Encoded header value
|
||||
*/
|
||||
_encodeHeaderValue(key, value) {
|
||||
encodeHeaderValue(key, value) {
|
||||
key = this._normalizeHeaderKey(key);
|
||||
switch (key) {
|
||||
// Structured headers
|
||||
@@ -1383,6 +1384,27 @@ class MimeNode {
|
||||
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
|
||||
*
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
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/";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
||||
export const name = 'nodemailer';
|
||||
export const version = '10.1.6';
|
||||
export const version = '10.2.0';
|
||||
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++) {
|
||||
const ord = buffer[i];
|
||||
const following = i + 1 < len ? buffer[i + 1] : next;
|
||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
|
||||
? ord === 0x0d
|
||||
? following === 0x0a
|
||||
: (i > 0 ? buffer[i - 1] : previous) === 0x0d
|
||||
: true;
|
||||
const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (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 (QP_LITERAL[ord] &&
|
||||
lineBreakByte &&
|
||||
!(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||
if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
|
||||
output[pos++] = ord;
|
||||
continue;
|
||||
}
|
||||
@@ -114,10 +108,7 @@ export function wrap(str, lineLength) {
|
||||
line = line.substr(0, line.length - match[0].length);
|
||||
}
|
||||
// ensure that utf-8 sequences are not split
|
||||
while (line.length > 3 &&
|
||||
line.length < len - pos &&
|
||||
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
|
||||
(match = line.match(/[=][\da-f]{2}$/gi))) {
|
||||
while (line.length > 3 && 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);
|
||||
if (code < 128) {
|
||||
break;
|
||||
|
||||
+1
-3
@@ -71,9 +71,7 @@ class SendmailTransport {
|
||||
return done(err);
|
||||
}
|
||||
// force -i to keep single dots
|
||||
const args = this.args
|
||||
? ['-i'].concat(this.args).concat(envelope.to)
|
||||
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||
const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||
const callback = (err) => {
|
||||
if (returned) {
|
||||
// ignore any additional responses, already done
|
||||
|
||||
+2
-6
@@ -217,9 +217,7 @@ export const resolveHostname = (options, callback) => {
|
||||
return respond(err);
|
||||
}
|
||||
// Get all supported addresses from dns.lookup
|
||||
const supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
if (['tls', 'auth'].includes(key) &&
|
||||
source[key] &&
|
||||
typeof source[key] === 'object') {
|
||||
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
||||
// 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
|
||||
// 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.
|
||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||
// 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) {
|
||||
if (typeof tlsOptions === 'function') {
|
||||
callback = tlsOptions;
|
||||
|
||||
+6
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
|
||||
messageSize: number;
|
||||
/** Final server response for the message */
|
||||
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
|
||||
|
||||
+16
-13
@@ -7,7 +7,8 @@ import crypto from 'node:crypto';
|
||||
import DataStream from './data-stream.js';
|
||||
import { PassThrough } from 'node:stream';
|
||||
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
|
||||
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
|
||||
@@ -361,9 +362,7 @@ class SMTPConnection extends EventEmitter {
|
||||
if (this._destroyed || this._closing) {
|
||||
return;
|
||||
}
|
||||
const connectFn = secure
|
||||
? tls.connect
|
||||
: net.connect;
|
||||
const connectFn = secure ? tls.connect : net.connect;
|
||||
try {
|
||||
this._socket = connectFn(opts, () => this._onConnect());
|
||||
this._setupConnectionHandlers();
|
||||
@@ -701,6 +700,16 @@ class SMTPConnection extends EventEmitter {
|
||||
result.messageTime = Date.now() - envelopeTime;
|
||||
result.messageSize = stream.outByteCount;
|
||||
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);
|
||||
});
|
||||
if (typeof message.pipe === 'function') {
|
||||
@@ -928,6 +937,7 @@ class SMTPConnection extends EventEmitter {
|
||||
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
||||
if (responseCode) {
|
||||
err.responseCode = responseCode;
|
||||
err.permanent = isPermanentResponseCode(responseCode);
|
||||
}
|
||||
if (command) {
|
||||
err.command = command;
|
||||
@@ -970,12 +980,7 @@ class SMTPConnection extends EventEmitter {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (!failureResponse &&
|
||||
this.stage === 'connected' &&
|
||||
!this._responseActions.length &&
|
||||
!this._pendingSend &&
|
||||
!this._destroyed &&
|
||||
!this._closing) {
|
||||
if (!failureResponse && 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
|
||||
// (usually after its idle timeout), not a failure
|
||||
this.logger.info({
|
||||
@@ -1196,9 +1201,7 @@ class SMTPConnection extends EventEmitter {
|
||||
const args = [];
|
||||
let useSmtpUtf8 = false;
|
||||
this._envelope = (envelope || {});
|
||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
|
||||
.toString()
|
||||
.trim();
|
||||
this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
|
||||
this._envelope.to = []
|
||||
.concat(this._envelope.to || [])
|
||||
.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
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
||||
+25
-1
@@ -62,6 +62,7 @@ class SMTPPool extends EventEmitter {
|
||||
this._queue = [];
|
||||
this._connections = [];
|
||||
this._connectionCounter = 0;
|
||||
this._pending = 0;
|
||||
this.idling = true;
|
||||
setImmediate(() => {
|
||||
if (this.idling) {
|
||||
@@ -94,10 +95,20 @@ class SMTPPool extends EventEmitter {
|
||||
setImmediate(() => callback(err));
|
||||
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({
|
||||
mail,
|
||||
requeueAttempts: 0,
|
||||
callback
|
||||
callback: (err, info) => {
|
||||
if (counted) {
|
||||
counted = false;
|
||||
this._pending--;
|
||||
}
|
||||
callback(err, info);
|
||||
}
|
||||
});
|
||||
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
||||
this.idling = false;
|
||||
@@ -441,6 +452,19 @@ class SMTPPool extends EventEmitter {
|
||||
isIdle() {
|
||||
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) {
|
||||
let promise;
|
||||
if (!callback) {
|
||||
|
||||
+2
-5
@@ -19,8 +19,7 @@ export default class PoolResource extends EventEmitter {
|
||||
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
||||
case 'OAUTH2': {
|
||||
const oauth2 = new XOAuth2(this.options.auth, this.logger);
|
||||
oauth2.provisionCallback =
|
||||
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
this.auth = {
|
||||
type: 'OAUTH2',
|
||||
user: this.options.auth.user,
|
||||
@@ -208,9 +207,7 @@ export default class PoolResource extends EventEmitter {
|
||||
this.connection.close();
|
||||
return;
|
||||
}
|
||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
|
||||
err.responseCode !== 421 &&
|
||||
!this.connection._destroyed) {
|
||||
if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
|
||||
// The server refused this message, the connection itself is fine. Reset the
|
||||
// session and keep using it instead of opening a new one
|
||||
this.connection.reset(resetErr => {
|
||||
|
||||
Reference in new issue
Block a user