@mavogel/awscdk-rootmail 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +3 -3
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/node_modules/@zone-eu/mailsplit/lib/headers.js +15 -4
- package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +34 -23
- package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +61 -5
- package/node_modules/@zone-eu/mailsplit/package.json +3 -3
- package/node_modules/libmime/CHANGELOG.md +8 -0
- package/node_modules/libmime/lib/charset.js +14 -2
- package/node_modules/libmime/lib/libmime.js +30 -6
- package/node_modules/libmime/package.json +2 -2
- package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +75 -0
- package/node_modules/libqp/CHANGELOG.md +7 -0
- package/node_modules/libqp/SECURITY.md +147 -0
- package/node_modules/libqp/lib/libqp.js +96 -24
- package/node_modules/libqp/package.json +1 -1
- package/node_modules/libqp/test/libqp-test.js +108 -0
- package/node_modules/mailparser/.release-please-manifest.json +1 -1
- package/node_modules/mailparser/CHANGELOG.md +9 -0
- package/node_modules/mailparser/README.md +2 -1
- package/node_modules/mailparser/lib/mail-parser.js +160 -65
- package/node_modules/mailparser/lib/simple-parser.js +3 -3
- package/node_modules/mailparser/package.json +4 -4
- package/node_modules/nodemailer/CHANGELOG.md +7 -0
- package/node_modules/nodemailer/dist/cjs/dkim/index.js +3 -2
- package/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +5 -0
- package/node_modules/nodemailer/dist/cjs/fetch/index.js +30 -0
- package/node_modules/nodemailer/dist/cjs/mailer/index.js +4 -1
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -1
- package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +25 -13
- package/node_modules/nodemailer/dist/cjs/mime-node/index.js +49 -6
- package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +16 -1
- package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +12 -1
- package/node_modules/nodemailer/dist/cjs/well-known/services.js +7 -0
- package/node_modules/nodemailer/dist/cjs/xoauth2/index.js +8 -2
- package/node_modules/nodemailer/dist/esm/dkim/index.js +3 -2
- package/node_modules/nodemailer/dist/esm/fetch/index.d.ts +5 -0
- package/node_modules/nodemailer/dist/esm/fetch/index.js +30 -0
- package/node_modules/nodemailer/dist/esm/mailer/index.js +4 -1
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -1
- package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +25 -13
- package/node_modules/nodemailer/dist/esm/mime-node/index.js +49 -6
- package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +16 -1
- package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +12 -1
- package/node_modules/nodemailer/dist/esm/well-known/services.js +7 -0
- package/node_modules/nodemailer/dist/esm/xoauth2/index.js +8 -2
- package/node_modules/nodemailer/dist/well-known/services.json +8 -0
- package/node_modules/nodemailer/package.json +1 -1
- package/package.json +2 -2
|
@@ -82,27 +82,121 @@ linkify.add('@', {
|
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
// Lists the parameters a header value gives in RFC 2231 form (name*=, name*0= or
|
|
86
|
+
// name*0*=). libmime decodes those while parsing the header, so the result must not go
|
|
87
|
+
// through decodeWords as well: filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D would
|
|
88
|
+
// otherwise come out as "a.exe" while a parser that decodes once sees the encoded word.
|
|
89
|
+
function rfc2231ParamNames(value) {
|
|
90
|
+
let names = new Set();
|
|
91
|
+
let quote = false;
|
|
92
|
+
let escaped = false;
|
|
93
|
+
let start = 0;
|
|
94
|
+
|
|
95
|
+
let addSegment = end => {
|
|
96
|
+
let segment = value.substring(start, end);
|
|
97
|
+
let eq = segment.indexOf('=');
|
|
98
|
+
if (eq < 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let match = segment
|
|
102
|
+
.substring(0, eq)
|
|
103
|
+
.trim()
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.match(/^(.+?)\*(\d+\*?)?$/);
|
|
106
|
+
if (match) {
|
|
107
|
+
names.add(match[1]);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < value.length; i++) {
|
|
112
|
+
let chr = value.charAt(i);
|
|
113
|
+
if (escaped) {
|
|
114
|
+
escaped = false;
|
|
115
|
+
} else if (chr === '\\') {
|
|
116
|
+
escaped = true;
|
|
117
|
+
} else if (chr === '"') {
|
|
118
|
+
quote = !quote;
|
|
119
|
+
} else if (chr === ';' && !quote) {
|
|
120
|
+
addSegment(i);
|
|
121
|
+
start = i + 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
addSegment(value.length);
|
|
125
|
+
|
|
126
|
+
return names;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Deepest element nesting html-to-text is asked to convert. htmlparser2 keeps its open
|
|
130
|
+
// element stack at the front of an array, so parsing alone is quadratic in the nesting
|
|
131
|
+
// depth (200k nested <b> took 9 s), and html-to-text's recursive walk overflows the call
|
|
132
|
+
// stack at about 2000 levels. Real markup stays far below this: Chrome's own parser
|
|
133
|
+
// flattens anything nested deeper than 512 levels.
|
|
134
|
+
const MAX_HTML_NESTING_DEPTH = 1000;
|
|
135
|
+
|
|
136
|
+
// Elements that never hold content, so a start tag does not open a level
|
|
137
|
+
const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
138
|
+
|
|
139
|
+
// Elements the HTML parser closes implicitly when a sibling opens (li after li, td after
|
|
140
|
+
// td, p after p), so an unclosed run of them does not nest. They are left out of the depth
|
|
141
|
+
// count: a report with thousands of unclosed table cells is sloppy but common markup
|
|
142
|
+
const HTML_IMPLIED_END_ELEMENTS = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'rt', 'rp']);
|
|
143
|
+
|
|
144
|
+
function isTagNameChar(code) {
|
|
145
|
+
return (
|
|
146
|
+
(code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x3a || code === 0x5f
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reports whether `html` nests elements deeper than `maxDepth`, in one linear pass that
|
|
151
|
+
// follows what htmlparser2 builds: a start tag opens an element (a trailing "/" does not
|
|
152
|
+
// close it in HTML mode), an end tag closes down to the nearest open element of that name
|
|
153
|
+
// and is ignored otherwise, comments are skipped. Returns as soon as the limit is crossed.
|
|
154
|
+
function exceedsHtmlNestingDepth(html, maxDepth) {
|
|
155
|
+
let stack = [];
|
|
156
|
+
let open = new Map(); // element name -> how many of them are on the stack
|
|
157
|
+
let len = html.length;
|
|
158
|
+
let i = html.indexOf('<');
|
|
159
|
+
while (i >= 0) {
|
|
160
|
+
let pos = i + 1;
|
|
161
|
+
if (html.startsWith('!--', pos)) {
|
|
162
|
+
let end = html.indexOf('-->', pos + 3);
|
|
163
|
+
i = end < 0 ? -1 : html.indexOf('<', end + 3);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
let closing = html.charCodeAt(pos) === 0x2f; // "/"
|
|
167
|
+
if (closing) {
|
|
168
|
+
pos++;
|
|
169
|
+
}
|
|
170
|
+
let nameStart = pos;
|
|
171
|
+
while (pos < len && isTagNameChar(html.charCodeAt(pos))) {
|
|
172
|
+
pos++;
|
|
173
|
+
}
|
|
174
|
+
if (pos === nameStart) {
|
|
175
|
+
// not a tag, "<" is text here
|
|
176
|
+
i = html.indexOf('<', pos);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let name = html.slice(nameStart, pos).toLowerCase();
|
|
180
|
+
let end = html.indexOf('>', pos);
|
|
181
|
+
if (end < 0) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (!HTML_VOID_ELEMENTS.has(name) && !HTML_IMPLIED_END_ELEMENTS.has(name)) {
|
|
185
|
+
if (!closing) {
|
|
186
|
+
stack.push(name);
|
|
187
|
+
open.set(name, (open.get(name) || 0) + 1);
|
|
188
|
+
if (stack.length > maxDepth) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
} else if (open.get(name)) {
|
|
192
|
+
let popped;
|
|
193
|
+
do {
|
|
194
|
+
popped = stack.pop();
|
|
195
|
+
open.set(popped, open.get(popped) - 1);
|
|
196
|
+
} while (popped !== name);
|
|
100
197
|
}
|
|
101
|
-
start = -1;
|
|
102
|
-
at = -1;
|
|
103
|
-
} else if (chr === '@' && start >= 0 && at < 0 && i > start + 1) {
|
|
104
|
-
at = i;
|
|
105
198
|
}
|
|
199
|
+
i = html.indexOf('<', end + 1);
|
|
106
200
|
}
|
|
107
201
|
return false;
|
|
108
202
|
}
|
|
@@ -379,12 +473,17 @@ class MailParser extends Transform {
|
|
|
379
473
|
switch (key) {
|
|
380
474
|
case 'content-type':
|
|
381
475
|
case 'content-disposition':
|
|
382
|
-
case 'dkim-signature':
|
|
476
|
+
case 'dkim-signature': {
|
|
477
|
+
// a parameter given in RFC 2231 form is decoded by parseHeaderValue already
|
|
478
|
+
let rfc2231 = rfc2231ParamNames(value);
|
|
383
479
|
value = this.libmime.parseHeaderValue(value);
|
|
384
480
|
if (value.value) {
|
|
385
481
|
value.value = this.libmime.decodeWords(value.value);
|
|
386
482
|
}
|
|
387
483
|
Object.keys((value && value.params) || {}).forEach(key => {
|
|
484
|
+
if (rfc2231.has(key)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
388
487
|
try {
|
|
389
488
|
value.params[key] = this.libmime.decodeWords(value.params[key]);
|
|
390
489
|
} catch (E) {
|
|
@@ -392,6 +491,7 @@ class MailParser extends Transform {
|
|
|
392
491
|
}
|
|
393
492
|
});
|
|
394
493
|
break;
|
|
494
|
+
}
|
|
395
495
|
case 'date': {
|
|
396
496
|
let dateValue = new Date(value);
|
|
397
497
|
if (isNaN(dateValue)) {
|
|
@@ -464,7 +564,12 @@ class MailParser extends Transform {
|
|
|
464
564
|
if (!headers.has(key)) {
|
|
465
565
|
headers.set(key, [].concat(value || []));
|
|
466
566
|
} else if (Array.isArray(value)) {
|
|
467
|
-
|
|
567
|
+
// appended in place: rebuilding the list with concat for every repeated
|
|
568
|
+
// header was quadratic in the number of References lines
|
|
569
|
+
let list = headers.get(key);
|
|
570
|
+
for (let entry of value) {
|
|
571
|
+
list.push(entry);
|
|
572
|
+
}
|
|
468
573
|
} else {
|
|
469
574
|
headers.get(key).push(value);
|
|
470
575
|
}
|
|
@@ -495,7 +600,9 @@ class MailParser extends Transform {
|
|
|
495
600
|
headers.forEach((value, key) => {
|
|
496
601
|
if (Array.isArray(value)) {
|
|
497
602
|
if (singleKeys.includes(key) && value.length) {
|
|
498
|
-
|
|
603
|
+
// the first occurrence: the one mailsplit built the MIME structure from,
|
|
604
|
+
// and the one mail clients display when the header is duplicated
|
|
605
|
+
headers.set(key, value[0]);
|
|
499
606
|
} else if (value.length === 1) {
|
|
500
607
|
headers.set(key, value[0]);
|
|
501
608
|
}
|
|
@@ -593,38 +700,14 @@ class MailParser extends Transform {
|
|
|
593
700
|
}
|
|
594
701
|
|
|
595
702
|
decodeAddresses(addresses) {
|
|
596
|
-
let processedAddress = new WeakSet();
|
|
597
703
|
for (let i = 0; i < addresses.length; i++) {
|
|
598
704
|
let address = addresses[i];
|
|
599
705
|
address.name = (address.name || '').toString().trim();
|
|
600
706
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// must not be re-interpreted as an address -- RFC 2047 Section 5 prohibits
|
|
606
|
-
// encoded-words in addr-spec, and re-parsing fabricates addresses not present
|
|
607
|
-
// in the original header.
|
|
608
|
-
if (hasBracketedAddress(decoded)) {
|
|
609
|
-
let parsed = addressparser(decoded);
|
|
610
|
-
if (parsed.length) {
|
|
611
|
-
parsed.forEach(entry => {
|
|
612
|
-
processedAddress.add(entry);
|
|
613
|
-
addresses.push(entry);
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// remove current element
|
|
618
|
-
addresses.splice(i, 1);
|
|
619
|
-
i--;
|
|
620
|
-
continue;
|
|
621
|
-
} else {
|
|
622
|
-
// Treat decoded content as display name only
|
|
623
|
-
address.name = decoded;
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
707
|
+
// An encoded word decodes into the display name and nothing else, whatever it
|
|
708
|
+
// decodes to. RFC 2047 section 5 keeps encoded words out of the addr-spec, so a
|
|
709
|
+
// name that decodes to "CEO" <ceo@bank.com> is still only a name: parsing it
|
|
710
|
+
// again would report an address the header never carried
|
|
628
711
|
if (address.name) {
|
|
629
712
|
try {
|
|
630
713
|
address.name = this.libmime.decodeWords(address.name);
|
|
@@ -762,6 +845,7 @@ class MailParser extends Transform {
|
|
|
762
845
|
getTextContent() {
|
|
763
846
|
let text = [];
|
|
764
847
|
let html = [];
|
|
848
|
+
let textConversionError = null;
|
|
765
849
|
let processNode = (alternative, level, node) => {
|
|
766
850
|
if (node.showMeta) {
|
|
767
851
|
let meta = ['From', 'Subject', 'Date', 'To', 'Cc', 'Bcc']
|
|
@@ -845,27 +929,34 @@ class MailParser extends Transform {
|
|
|
845
929
|
html.push(this.textToHtml(node.textContent));
|
|
846
930
|
}
|
|
847
931
|
} else if (node.contentType === 'text/html') {
|
|
848
|
-
let failedToParseHtml = false;
|
|
849
932
|
if ((!alternative && this.hasText) || (node.root && !this.hasText)) {
|
|
850
933
|
if (this.options.skipHtmlToText) {
|
|
851
934
|
text.push('');
|
|
852
|
-
} else if (node.textContent.length > this.options.maxHtmlLengthToParse) {
|
|
853
|
-
this.emit('error', new Error(`HTML too long for parsing ${node.textContent.length} bytes`));
|
|
854
|
-
text.push('Invalid HTML content (too long)');
|
|
855
|
-
failedToParseHtml = true;
|
|
856
935
|
} else {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
936
|
+
// HTML that can not be converted is no reason to lose the message:
|
|
937
|
+
// the text stays empty, the HTML is kept and the reason travels with
|
|
938
|
+
// the result as textConversionError. An 'error' event here used to
|
|
939
|
+
// reject the whole simpleParser() promise, headers and attachments
|
|
940
|
+
// included, after burning the CPU the conversion attempt took
|
|
941
|
+
let err = null;
|
|
942
|
+
if (node.textContent.length > this.options.maxHtmlLengthToParse) {
|
|
943
|
+
err = new Error(`HTML too long for parsing ${node.textContent.length} bytes`);
|
|
944
|
+
} else if (exceedsHtmlNestingDepth(node.textContent, MAX_HTML_NESTING_DEPTH)) {
|
|
945
|
+
err = new Error(`HTML nested deeper than ${MAX_HTML_NESTING_DEPTH} levels`);
|
|
946
|
+
} else {
|
|
947
|
+
try {
|
|
948
|
+
text.push(htmlToText(node.textContent, { limits: { maxDepth: MAX_HTML_NESTING_DEPTH } }));
|
|
949
|
+
} catch (cause) {
|
|
950
|
+
err = new Error('Failed to parse HTML', { cause });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (err) {
|
|
954
|
+
text.push('');
|
|
955
|
+
textConversionError = textConversionError || err;
|
|
863
956
|
}
|
|
864
957
|
}
|
|
865
958
|
}
|
|
866
|
-
|
|
867
|
-
html.push(node.textContent);
|
|
868
|
-
}
|
|
959
|
+
html.push(node.textContent);
|
|
869
960
|
}
|
|
870
961
|
}
|
|
871
962
|
alternative = alternative || node.contentType === 'multipart/alternative';
|
|
@@ -888,6 +979,9 @@ class MailParser extends Transform {
|
|
|
888
979
|
this.text = response.text = text.join('\n');
|
|
889
980
|
this.textAsHtml = response.textAsHtml = text.map(part => this.textToHtml(part)).join('<br/>\n');
|
|
890
981
|
}
|
|
982
|
+
if (textConversionError) {
|
|
983
|
+
response.textConversionError = textConversionError;
|
|
984
|
+
}
|
|
891
985
|
return response;
|
|
892
986
|
}
|
|
893
987
|
|
|
@@ -1147,7 +1241,8 @@ class MailParser extends Transform {
|
|
|
1147
1241
|
.map(address => {
|
|
1148
1242
|
let str = '';
|
|
1149
1243
|
if (address.name) {
|
|
1150
|
-
|
|
1244
|
+
// a quoted-string, so a quote or backslash inside the name is escaped
|
|
1245
|
+
str += `"${address.name.replace(/["\\]/g, '\\$&')}"` + (address.group ? ': ' : '');
|
|
1151
1246
|
}
|
|
1152
1247
|
if (address.address) {
|
|
1153
1248
|
let link = address.address;
|
|
@@ -19,8 +19,8 @@ module.exports = (input, options, callback) => {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// MailParser may emit a non-fatal 'error'
|
|
23
|
-
//
|
|
22
|
+
// MailParser may emit a non-fatal 'error' and still reach 'end', so settle only once:
|
|
23
|
+
// the first error or the result wins
|
|
24
24
|
let settled = false;
|
|
25
25
|
let settle = callback;
|
|
26
26
|
callback = (...args) => {
|
|
@@ -62,7 +62,7 @@ module.exports = (input, options, callback) => {
|
|
|
62
62
|
|
|
63
63
|
if (data.type === 'text') {
|
|
64
64
|
Object.keys(data).forEach(key => {
|
|
65
|
-
if (['text', 'html', 'textAsHtml'].includes(key)) {
|
|
65
|
+
if (['text', 'html', 'textAsHtml', 'textConversionError'].includes(key)) {
|
|
66
66
|
mail[key] = data[key];
|
|
67
67
|
}
|
|
68
68
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailparser",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.40",
|
|
4
4
|
"description": "Parse e-mails",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
],
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@zone-eu/mailsplit": "5.4.23",
|
|
21
22
|
"encoding-japanese": "2.4.0",
|
|
22
23
|
"he": "1.2.0",
|
|
23
24
|
"html-to-text": "10.0.1",
|
|
24
25
|
"iconv-lite": "0.7.3",
|
|
25
|
-
"libmime": "5.4.
|
|
26
|
+
"libmime": "5.4.9",
|
|
26
27
|
"linkify-it": "5.0.2",
|
|
27
|
-
"
|
|
28
|
-
"nodemailer": "10.1.1",
|
|
28
|
+
"nodemailer": "10.1.2",
|
|
29
29
|
"punycode.js": "2.3.1",
|
|
30
30
|
"tlds": "1.261.0"
|
|
31
31
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.1.2](https://github.com/nodemailer/nodemailer/compare/v10.1.1...v10.1.2) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* bound the work a message can make the mailer do and keep per-message auth off the transport's token endpoint ([e347fa2](https://github.com/nodemailer/nodemailer/commit/e347fa2260a7e342e772a7e187937aa87ac96364))
|
|
9
|
+
|
|
3
10
|
## [10.1.1](https://github.com/nodemailer/nodemailer/compare/v10.1.0...v10.1.1) (2026-10-14)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -133,8 +133,9 @@ class DKIMSigner {
|
|
|
133
133
|
}
|
|
134
134
|
createWriteCache() {
|
|
135
135
|
this.output.usingCache = true;
|
|
136
|
-
// pipe remainings to cache file
|
|
137
|
-
|
|
136
|
+
// pipe remainings to cache file. It holds the message body and cacheDir is often a
|
|
137
|
+
// shared directory such as /tmp, so nobody but the owner gets to read it
|
|
138
|
+
this.cache = node_fs_1.default.createWriteStream(this.cachePath, { mode: 0o600 });
|
|
138
139
|
this.cache.once('error', err => {
|
|
139
140
|
// drain input
|
|
140
141
|
this.relaxedBody.unpipe(this.cache);
|
|
@@ -27,6 +27,8 @@ export interface FetchOptions {
|
|
|
27
27
|
} | undefined;
|
|
28
28
|
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
|
|
29
29
|
timeout?: number | undefined;
|
|
30
|
+
/** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
|
|
31
|
+
maxTime?: number | undefined;
|
|
30
32
|
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
|
|
31
33
|
maxBytes?: number | undefined;
|
|
32
34
|
/** Maximum number of redirects to follow (default 5) */
|
|
@@ -37,6 +39,8 @@ export interface FetchOptions {
|
|
|
37
39
|
redirects?: number | undefined;
|
|
38
40
|
/** Response stream shared across redirects, set by nmfetch itself */
|
|
39
41
|
fetchRes?: FetchResponse | undefined;
|
|
42
|
+
/** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
|
|
43
|
+
deadline?: number | undefined;
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* The stream nmfetch returns. The response body is piped into it, the status code and
|
|
@@ -50,6 +54,7 @@ declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
|
|
|
50
54
|
declare namespace nmfetch {
|
|
51
55
|
var Cookies: typeof import("./cookies.js").default;
|
|
52
56
|
var DEFAULT_TIMEOUT: number;
|
|
57
|
+
var DEFAULT_MAX_TIME: number;
|
|
53
58
|
}
|
|
54
59
|
type CookiesJar = Cookies;
|
|
55
60
|
/**
|
|
@@ -49,6 +49,10 @@ const objects_js_1 = require("../shared/objects.js");
|
|
|
49
49
|
const MAX_REDIRECTS = 5;
|
|
50
50
|
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
|
|
51
51
|
const DEFAULT_TIMEOUT = 60 * 1000;
|
|
52
|
+
// the inactivity timeout above is reset by every byte, so a server dripping one within each
|
|
53
|
+
// window could still hold the request, and the message being built from it, open for as
|
|
54
|
+
// long as it liked. The whole request, redirects included, has to be done within this
|
|
55
|
+
const DEFAULT_MAX_TIME = 2 * 60 * 1000;
|
|
52
56
|
// the body is usually buffered in memory by the caller, so an unbounded download is refused
|
|
53
57
|
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
54
58
|
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
|
|
@@ -126,6 +130,12 @@ function nmfetch(url, options) {
|
|
|
126
130
|
options.cookies = options.cookies || new cookies_js_1.default();
|
|
127
131
|
options.redirects = options.redirects || 0;
|
|
128
132
|
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
|
133
|
+
if (!options.deadline) {
|
|
134
|
+
options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
|
|
135
|
+
// the one deadline is shared with the requests the redirects lead to, every hop
|
|
136
|
+
// gets the time that is left rather than starting over
|
|
137
|
+
options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
|
|
138
|
+
}
|
|
129
139
|
const fetchRes = options.fetchRes;
|
|
130
140
|
const parsed = parseFetchUrl(url);
|
|
131
141
|
if (!parsed) {
|
|
@@ -271,12 +281,22 @@ function nmfetch(url, options) {
|
|
|
271
281
|
});
|
|
272
282
|
return fetchRes;
|
|
273
283
|
}
|
|
284
|
+
// the whole request has to be done by options.deadline, see maxTime. The timer is
|
|
285
|
+
// released on every way out of the request, so it never outlives it
|
|
286
|
+
let deadlineTimer;
|
|
287
|
+
const clearDeadline = () => {
|
|
288
|
+
if (deadlineTimer) {
|
|
289
|
+
clearTimeout(deadlineTimer);
|
|
290
|
+
deadlineTimer = undefined;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
274
293
|
// reports the first failure of this request on fetchRes and releases the request
|
|
275
294
|
const fail = (err, sourceUrl = url) => {
|
|
276
295
|
if (finished) {
|
|
277
296
|
return;
|
|
278
297
|
}
|
|
279
298
|
finished = true;
|
|
299
|
+
clearDeadline();
|
|
280
300
|
setFetchCode(err);
|
|
281
301
|
err.sourceUrl = sourceUrl;
|
|
282
302
|
fetchRes.emit('error', err);
|
|
@@ -286,10 +306,16 @@ function nmfetch(url, options) {
|
|
|
286
306
|
if (timeout) {
|
|
287
307
|
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
|
|
288
308
|
}
|
|
309
|
+
if (options.deadline) {
|
|
310
|
+
deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
|
|
311
|
+
// a pending deadline is no reason to keep the process alive
|
|
312
|
+
deadlineTimer.unref();
|
|
313
|
+
}
|
|
289
314
|
req.on('error', (err) => fail(err));
|
|
290
315
|
// a consumer that destroys the response stream before it ended does not want the rest of
|
|
291
316
|
// the body, release the request now instead of when the timeout fires
|
|
292
317
|
fetchRes.once('close', () => {
|
|
318
|
+
clearDeadline();
|
|
293
319
|
if (finished || fetchRes.readableEnded) {
|
|
294
320
|
return;
|
|
295
321
|
}
|
|
@@ -358,6 +384,7 @@ function nmfetch(url, options) {
|
|
|
358
384
|
// this request is done with, release it so its socket and timeout do not
|
|
359
385
|
// outlive it and report a late error on the shared fetchRes
|
|
360
386
|
finished = true;
|
|
387
|
+
clearDeadline();
|
|
361
388
|
res.resume();
|
|
362
389
|
req.abort();
|
|
363
390
|
return nmfetch(redirectUrl, options);
|
|
@@ -368,6 +395,8 @@ function nmfetch(url, options) {
|
|
|
368
395
|
return fail(new Error('Invalid status code ' + res.statusCode));
|
|
369
396
|
}
|
|
370
397
|
res.on('error', (err) => fail(err));
|
|
398
|
+
// the response is in, whatever is left of it is in memory and no longer on the network
|
|
399
|
+
res.once('end', () => clearDeadline());
|
|
371
400
|
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
|
|
372
401
|
const source = inflate || res;
|
|
373
402
|
let received = 0;
|
|
@@ -405,6 +434,7 @@ function nmfetch(url, options) {
|
|
|
405
434
|
}
|
|
406
435
|
nmfetch.Cookies = cookies_js_1.default;
|
|
407
436
|
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
437
|
+
nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
|
|
408
438
|
exports.default = nmfetch;
|
|
409
439
|
module.exports = exports.default;
|
|
410
440
|
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -398,9 +398,12 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
398
398
|
return callback(err);
|
|
399
399
|
}
|
|
400
400
|
let cidCounter = 0;
|
|
401
|
+
// the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
|
|
402
|
+
// attribute value and with it the type, so one img tag never scans ahead to a
|
|
403
|
+
// ';' that belongs to a later tag, which was quadratic
|
|
401
404
|
html = (html || '')
|
|
402
405
|
.toString()
|
|
403
|
-
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]
|
|
406
|
+
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
|
404
407
|
const cid = node_crypto_1.default.randomBytes(10).toString('hex') + '@localhost';
|
|
405
408
|
if (!mail.data.attachments) {
|
|
406
409
|
mail.data.attachments = [];
|
|
@@ -23,7 +23,11 @@ export interface SendMailOptions extends MailComposerOptions {
|
|
|
23
23
|
dsn?: SMTPEnvelopeDsn | undefined;
|
|
24
24
|
/** SMTP transports: RFC 8689, send the REQUIRETLS parameter with MAIL FROM */
|
|
25
25
|
requireTLSExtensionEnabled?: boolean | undefined;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* SMTP transports: per-message authentication settings, used instead of the transport level auth.
|
|
28
|
+
* Only the credential fields are taken from it (type, user, pass, accessToken, refreshToken, expires),
|
|
29
|
+
* the token endpoint and client settings stay those of the transport
|
|
30
|
+
*/
|
|
27
31
|
auth?: SMTPTransportAuthOptions | undefined;
|
|
28
32
|
/** SES transport: extra SendEmailCommand parameters merged into the API call */
|
|
29
33
|
ses?: SESSendEmailRequest | undefined;
|
|
@@ -186,26 +186,38 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
|
|
|
186
186
|
*/
|
|
187
187
|
function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
|
|
188
188
|
maxLength = maxLength || 0;
|
|
189
|
-
// find first
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
// find the first and the last character that needs encoding, a quote or anything outside
|
|
190
|
+
// printable ascii. A scan rather than a regex: matching the last such word with a
|
|
191
|
+
// backtracking pattern took quadratic time on a long word of non-ascii characters
|
|
192
|
+
let firstSpecial = -1;
|
|
193
|
+
let lastSpecial = -1;
|
|
194
|
+
for (let i = 0; i < value.length; i++) {
|
|
195
|
+
const code = value.charCodeAt(i);
|
|
196
|
+
if (code === 0x22 || code >= 0x80) {
|
|
197
|
+
if (firstSpecial < 0) {
|
|
198
|
+
firstSpecial = i;
|
|
199
|
+
}
|
|
200
|
+
lastSpecial = i;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (firstSpecial < 0) {
|
|
192
204
|
return value;
|
|
193
205
|
}
|
|
194
206
|
if (encodeAll) {
|
|
195
207
|
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
|
|
196
208
|
return encodeWord(value, mimeWordEncoding, maxLength);
|
|
197
209
|
}
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
// encode from the start of the word holding the first such character to the end of the
|
|
211
|
+
// word holding the last one, so the encoded words stay separated from the plain text by
|
|
212
|
+
// the whitespace that was already there
|
|
213
|
+
let startIndex = firstSpecial;
|
|
214
|
+
while (startIndex > 0 && !/\s/.test(value.charAt(startIndex - 1))) {
|
|
215
|
+
startIndex--;
|
|
216
|
+
}
|
|
217
|
+
let endIndex = lastSpecial + 1;
|
|
218
|
+
while (endIndex < value.length && !/\s/.test(value.charAt(endIndex))) {
|
|
219
|
+
endIndex++;
|
|
203
220
|
}
|
|
204
|
-
const startIndex = firstMatch.index +
|
|
205
|
-
(firstMatch[0].match(/[^\s]/) || {
|
|
206
|
-
index: 0
|
|
207
|
-
}).index;
|
|
208
|
-
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
|
|
209
221
|
return ((startIndex ? value.substr(0, startIndex) : '') +
|
|
210
222
|
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
|
|
211
223
|
(endIndex < value.length ? value.substr(endIndex) : ''));
|