@limetech/lime-elements 39.45.2 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-action-bar_3.cjs.entry.js +1 -1
- package/dist/cjs/limel-ai-avatar.cjs.entry.js +1 -1
- package/dist/cjs/limel-callout.cjs.entry.js +1 -1
- package/dist/cjs/limel-chart.cjs.entry.js +1 -1
- package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
- package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
- package/dist/cjs/limel-code-diff.cjs.entry.js +1 -1
- package/dist/cjs/limel-code-editor.cjs.entry.js +1 -1
- package/dist/cjs/limel-collapsible-section.cjs.entry.js +1 -1
- package/dist/cjs/limel-drag-handle.cjs.entry.js +1 -1
- package/dist/cjs/limel-email-viewer.cjs.entry.js +1 -1
- package/dist/cjs/limel-file-viewer.cjs.entry.js +701 -345
- package/dist/cjs/limel-file.cjs.entry.js +1 -1
- package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +1 -1
- package/dist/cjs/limel-form.cjs.entry.js +17 -43
- package/dist/cjs/limel-list-item.cjs.entry.js +1 -1
- package/dist/cjs/limel-picker.cjs.entry.js +1 -1
- package/dist/cjs/limel-profile-picture.cjs.entry.js +1 -1
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +40 -40
- package/dist/cjs/limel-slider.cjs.entry.js +140 -16
- package/dist/cjs/limel-snackbar.cjs.entry.js +1 -1
- package/dist/cjs/limel-table.cjs.entry.js +1 -1
- package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{translations-Bnw00bkf.js → translations-Do_D3pxu.js} +39 -2
- package/dist/collection/components/slider/slider.css +95 -0
- package/dist/collection/components/slider/slider.js +174 -21
- package/dist/collection/global/translations.js +15 -2
- package/dist/collection/translations/da.js +3 -0
- package/dist/collection/translations/de.js +3 -0
- package/dist/collection/translations/en.js +3 -0
- package/dist/collection/translations/fi.js +3 -0
- package/dist/collection/translations/fr.js +3 -0
- package/dist/collection/translations/nl.js +3 -0
- package/dist/collection/translations/no.js +3 -0
- package/dist/collection/translations/sv.js +3 -0
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-action-bar_3.entry.js +1 -1
- package/dist/esm/limel-ai-avatar.entry.js +1 -1
- package/dist/esm/limel-callout.entry.js +1 -1
- package/dist/esm/limel-chart.entry.js +1 -1
- package/dist/esm/limel-chip-set.entry.js +1 -1
- package/dist/esm/limel-chip_2.entry.js +1 -1
- package/dist/esm/limel-code-diff.entry.js +1 -1
- package/dist/esm/limel-code-editor.entry.js +1 -1
- package/dist/esm/limel-collapsible-section.entry.js +1 -1
- package/dist/esm/limel-drag-handle.entry.js +1 -1
- package/dist/esm/limel-email-viewer.entry.js +1 -1
- package/dist/esm/limel-file-viewer.entry.js +701 -345
- package/dist/esm/limel-file.entry.js +1 -1
- package/dist/esm/limel-flatpickr-adapter.entry.js +1 -1
- package/dist/esm/limel-form.entry.js +17 -43
- package/dist/esm/limel-list-item.entry.js +1 -1
- package/dist/esm/limel-picker.entry.js +1 -1
- package/dist/esm/limel-profile-picture.entry.js +1 -1
- package/dist/esm/limel-prosemirror-adapter.entry.js +40 -40
- package/dist/esm/limel-slider.entry.js +140 -16
- package/dist/esm/limel-snackbar.entry.js +1 -1
- package/dist/esm/limel-table.entry.js +1 -1
- package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{translations-Cb9R_zmF.js → translations-DgqE77nk.js} +39 -2
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/{p-421612f9.entry.js → p-21b54196.entry.js} +4 -4
- package/dist/lime-elements/p-3e316685.entry.js +1 -0
- package/dist/lime-elements/{p-c8116a01.entry.js → p-565f688a.entry.js} +1 -1
- package/dist/lime-elements/{p-aa080a8f.entry.js → p-5dde5d72.entry.js} +1 -1
- package/dist/lime-elements/{p-31713caf.entry.js → p-5e432a44.entry.js} +1 -1
- package/dist/lime-elements/{p-4a09fb2e.entry.js → p-62a25deb.entry.js} +1 -1
- package/dist/lime-elements/{p-8aa4a38e.entry.js → p-72a1d432.entry.js} +1 -1
- package/dist/lime-elements/{p-9a04a686.entry.js → p-83bdbaf7.entry.js} +1 -1
- package/dist/lime-elements/{p-ca0b86ee.entry.js → p-8c3798ee.entry.js} +1 -1
- package/dist/lime-elements/{p-8aeeeb53.entry.js → p-8cf79fa1.entry.js} +1 -1
- package/dist/lime-elements/{p-7487d65d.entry.js → p-9ed9d0de.entry.js} +1 -1
- package/dist/lime-elements/p-DgqE77nk.js +1 -0
- package/dist/lime-elements/{p-2284caa7.entry.js → p-a6500046.entry.js} +1 -1
- package/dist/lime-elements/{p-e545c9e8.entry.js → p-b04c6602.entry.js} +1 -1
- package/dist/lime-elements/{p-d97df2cd.entry.js → p-b4b43404.entry.js} +1 -1
- package/dist/lime-elements/p-b6528083.entry.js +1 -0
- package/dist/lime-elements/{p-6722e6e2.entry.js → p-cafebc7d.entry.js} +1 -1
- package/dist/lime-elements/{p-2f223e46.entry.js → p-cdcd9880.entry.js} +1 -1
- package/dist/lime-elements/{p-2e5d5e79.entry.js → p-dd116bb1.entry.js} +1 -1
- package/dist/lime-elements/{p-d7bb4310.entry.js → p-dda63b8b.entry.js} +1 -1
- package/dist/lime-elements/{p-7f7e2180.entry.js → p-e8783848.entry.js} +1 -1
- package/dist/lime-elements/{p-19c81ded.entry.js → p-eeddce38.entry.js} +1 -1
- package/dist/lime-elements/{p-9ae7d11b.entry.js → p-f53d8bda.entry.js} +1 -1
- package/dist/lime-elements/{p-03fd2181.entry.js → p-fa4edcf5.entry.js} +1 -1
- package/dist/types/components/slider/slider.d.ts +67 -4
- package/dist/types/components.d.ts +25 -10
- package/dist/types/translations/da.d.ts +3 -0
- package/dist/types/translations/de.d.ts +3 -0
- package/dist/types/translations/en.d.ts +3 -0
- package/dist/types/translations/fi.d.ts +3 -0
- package/dist/types/translations/fr.d.ts +3 -0
- package/dist/types/translations/nl.d.ts +3 -0
- package/dist/types/translations/no.d.ts +3 -0
- package/dist/types/translations/sv.d.ts +3 -0
- package/package.json +9 -9
- package/dist/lime-elements/p-Cb9R_zmF.js +0 -1
- package/dist/lime-elements/p-ee58f0b4.entry.js +0 -1
- package/dist/lime-elements/p-f51b5651.entry.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var index$1 = require('./index-DYg_7kkT.js');
|
|
4
|
-
var translations = require('./translations-
|
|
4
|
+
var translations = require('./translations-Do_D3pxu.js');
|
|
5
5
|
var index = require('./index-ETPz91V5.js');
|
|
6
6
|
var image_template = require('./image.template-DZC6_5a4.js');
|
|
7
7
|
require('./_commonjsHelpers-CFO10eej.js');
|
|
@@ -134,25 +134,27 @@ for (let i = 0; i < base64Chars.length; i++) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function decodeBase64(base64) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let
|
|
137
|
+
// Padding carries no data, so the byte count comes from the payload alone. Sizing the
|
|
138
|
+
// buffer from the raw length instead treated '=' as a data character and left the
|
|
139
|
+
// output padded with NUL bytes, which then travelled into subjects and filenames.
|
|
140
|
+
let len = base64.length;
|
|
141
|
+
while (len > 0 && base64.charAt(len - 1) === '=') {
|
|
142
|
+
len--;
|
|
143
|
+
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
bufferLength -= 2;
|
|
146
|
-
} else if (base64[base64.length - 1] === '=') {
|
|
147
|
-
bufferLength--;
|
|
148
|
-
if (base64[base64.length - 2] === '=') {
|
|
149
|
-
bufferLength--;
|
|
150
|
-
}
|
|
145
|
+
// A remainder of one character cannot encode a byte, it is a truncated group
|
|
146
|
+
if (len % 4 === 1) {
|
|
147
|
+
len--;
|
|
151
148
|
}
|
|
152
149
|
|
|
150
|
+
const remainder = len % 4;
|
|
151
|
+
const bufferLength = Math.floor(len / 4) * 3 + (remainder ? remainder - 1 : 0);
|
|
152
|
+
|
|
153
153
|
const arrayBuffer = new ArrayBuffer(bufferLength);
|
|
154
154
|
const bytes = new Uint8Array(arrayBuffer);
|
|
155
155
|
|
|
156
|
+
let p = 0;
|
|
157
|
+
|
|
156
158
|
for (let i = 0; i < len; i += 4) {
|
|
157
159
|
let encoded1 = base64Lookup[base64.charCodeAt(i)];
|
|
158
160
|
let encoded2 = base64Lookup[base64.charCodeAt(i + 1)];
|
|
@@ -160,37 +162,107 @@ function decodeBase64(base64) {
|
|
|
160
162
|
let encoded4 = base64Lookup[base64.charCodeAt(i + 3)];
|
|
161
163
|
|
|
162
164
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
if (p < bufferLength) {
|
|
166
|
+
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
|
167
|
+
}
|
|
168
|
+
if (p < bufferLength) {
|
|
169
|
+
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
|
170
|
+
}
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
return arrayBuffer;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
// Charset
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
+
// Charset labels the WHATWG Encoding Standard does not list, but that name an encoding
|
|
177
|
+
// TextDecoder can decode. Without an entry the TextDecoder constructor throws and
|
|
178
|
+
// getDecoder falls back to windows-1252, turning the body into mojibake with nothing to
|
|
179
|
+
// signal that the wrong decoder was used.
|
|
180
|
+
//
|
|
181
|
+
// Keys are normalized (see normalizeCharset), so one entry covers every spelling of a
|
|
182
|
+
// label, eg. `eucjp` also catches `euc_jp`, `x-eucjp` and `x-euc-jp`. Only labels that
|
|
183
|
+
// need real encoding knowledge belong here; anything that differs from a supported
|
|
184
|
+
// label by nothing but an `x-` prefix or a separator is handled by normalization alone.
|
|
176
185
|
const charsetAliases = new Map([
|
|
177
|
-
|
|
178
|
-
['
|
|
186
|
+
// Hebrew. The logical and explicit ordering variants share the iso-8859-8 index.
|
|
187
|
+
['iso88598i', 'iso-8859-8'],
|
|
188
|
+
['iso88598e', 'iso-8859-8'],
|
|
189
|
+
|
|
190
|
+
// Japanese. WHATWG shift_jis is the Windows-31J index, so cp932 text decodes
|
|
191
|
+
// identically, including the NEC and IBM extension rows.
|
|
192
|
+
['shiftjis', 'shift_jis'],
|
|
193
|
+
['windows31j', 'shift_jis'],
|
|
194
|
+
['mskanji', 'shift_jis'],
|
|
195
|
+
['eucjp', 'euc-jp'],
|
|
196
|
+
|
|
197
|
+
// ISO-2022-JP, including the -1 / -2 supersets. Escape sequences outside plain
|
|
198
|
+
// ISO-2022-JP (JIS X 0212, the non-Japanese G2 sets, and the SO/SI katakana shifts
|
|
199
|
+
// cp50222 uses) decode to replacement characters, but the Japanese text around them
|
|
200
|
+
// still comes out right.
|
|
201
|
+
['iso2022jp', 'iso-2022-jp'],
|
|
202
|
+
['iso2022jp1', 'iso-2022-jp'],
|
|
203
|
+
['iso2022jp2', 'iso-2022-jp'],
|
|
204
|
+
['junet', 'iso-2022-jp'],
|
|
205
|
+
|
|
206
|
+
// Korean. The WHATWG euc-kr index is the extended cp949 / UHC index.
|
|
207
|
+
['euckr', 'euc-kr'],
|
|
208
|
+
['uhc', 'euc-kr'],
|
|
209
|
+
|
|
210
|
+
// Thai.
|
|
211
|
+
['tis620', 'windows-874']
|
|
179
212
|
]);
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
// Windows and IBM code page numbers, as written in labels like cp932, windows-932, ms932
|
|
215
|
+
// or ibm932. An explicit allowlist rather than a derived one, because plenty of code
|
|
216
|
+
// pages that appear in mail (cp437, cp850, cp1361) have no equivalent to map onto and
|
|
217
|
+
// have to keep falling back.
|
|
218
|
+
const codePageAliases = new Map([
|
|
219
|
+
['932', 'shift_jis'],
|
|
220
|
+
['936', 'gbk'],
|
|
221
|
+
['949', 'euc-kr'],
|
|
222
|
+
['950', 'big5'],
|
|
223
|
+
['874', 'windows-874'],
|
|
224
|
+
// Microsoft's EUC-JP and ISO-2022-JP variants. euc-jp and iso-2022-jp cover
|
|
225
|
+
// everything they can express.
|
|
226
|
+
['51932', 'euc-jp'],
|
|
227
|
+
['50220', 'iso-2022-jp'],
|
|
228
|
+
['50221', 'iso-2022-jp'],
|
|
229
|
+
['50222', 'iso-2022-jp']
|
|
230
|
+
]);
|
|
184
231
|
|
|
185
|
-
|
|
232
|
+
const codePagePattern = /^(?:cp|windows|ms|ibm)(\d+)$/;
|
|
186
233
|
|
|
234
|
+
// Strip the decorations mail clients add to an otherwise standard label: an x- vendor
|
|
235
|
+
// prefix, the IANA cs- prefix, and any separators.
|
|
236
|
+
function normalizeCharset(charset) {
|
|
237
|
+
return charset.replace(/^(?:x-ms-|x-|cs)/, '').replace(/[\s._-]+/g, '');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function tryDecoder(charset) {
|
|
187
241
|
try {
|
|
188
|
-
|
|
242
|
+
return new TextDecoder(charset);
|
|
189
243
|
} catch (err) {
|
|
190
|
-
|
|
244
|
+
return null;
|
|
191
245
|
}
|
|
246
|
+
}
|
|
192
247
|
|
|
193
|
-
|
|
248
|
+
function getDecoder(charset) {
|
|
249
|
+
charset = (charset || 'utf8').trim().toLowerCase();
|
|
250
|
+
|
|
251
|
+
// Try the label as written first, so the alias table only ever adds to what the
|
|
252
|
+
// runtime already supports instead of shadowing it.
|
|
253
|
+
const decoder = tryDecoder(charset);
|
|
254
|
+
if (decoder) {
|
|
255
|
+
return decoder;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const normalized = normalizeCharset(charset);
|
|
259
|
+
const codePage = normalized.match(codePagePattern);
|
|
260
|
+
|
|
261
|
+
// The normalized label is itself the last candidate, which resolves everything that
|
|
262
|
+
// differed from a supported label only by a prefix or a separator, eg. x-big5.
|
|
263
|
+
const alias = (codePage && codePageAliases.get(codePage[1])) || charsetAliases.get(normalized) || normalized;
|
|
264
|
+
|
|
265
|
+
return tryDecoder(alias) || new TextDecoder('windows-1252');
|
|
194
266
|
}
|
|
195
267
|
|
|
196
268
|
/**
|
|
@@ -218,15 +290,23 @@ async function blobToArrayBuffer(blob) {
|
|
|
218
290
|
});
|
|
219
291
|
}
|
|
220
292
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Numeric value of an ASCII hex digit
|
|
295
|
+
*
|
|
296
|
+
* @param {Number} c Byte to read
|
|
297
|
+
* @return {Number} Value 0-15, or -1 if the byte is not a hex digit
|
|
298
|
+
*/
|
|
299
|
+
function hexNibble(c) {
|
|
300
|
+
if (c >= 0x30 /* 0 */ && c <= 0x39 /* 9 */) {
|
|
301
|
+
return c - 0x30;
|
|
302
|
+
}
|
|
303
|
+
if (c >= 0x61 /* a */ && c <= 0x66 /* f */) {
|
|
304
|
+
return c - 0x61 + 10;
|
|
228
305
|
}
|
|
229
|
-
|
|
306
|
+
if (c >= 0x41 /* A */ && c <= 0x46 /* F */) {
|
|
307
|
+
return c - 0x41 + 10;
|
|
308
|
+
}
|
|
309
|
+
return -1;
|
|
230
310
|
}
|
|
231
311
|
|
|
232
312
|
/**
|
|
@@ -260,11 +340,10 @@ function decodeWord(charset, encoding, str) {
|
|
|
260
340
|
for (let i = 0, len = buf.length; i < len; i++) {
|
|
261
341
|
let c = buf[i];
|
|
262
342
|
if (i <= len - 2 && c === 0x3d /* = */) {
|
|
263
|
-
let
|
|
264
|
-
let
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
encodedBytes.push(c);
|
|
343
|
+
let high = hexNibble(buf[i + 1]);
|
|
344
|
+
let low = hexNibble(buf[i + 2]);
|
|
345
|
+
if (high >= 0 && low >= 0) {
|
|
346
|
+
encodedBytes.push((high << 4) | low);
|
|
268
347
|
i += 2;
|
|
269
348
|
continue;
|
|
270
349
|
}
|
|
@@ -286,59 +365,135 @@ function decodeWord(charset, encoding, str) {
|
|
|
286
365
|
return getDecoder(charset).decode(byteStr);
|
|
287
366
|
}
|
|
288
367
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.toString()
|
|
295
|
-
// find base64 words that can be joined
|
|
296
|
-
.replace(
|
|
297
|
-
/(=\?([^?]+)\?[Bb]\?([^?]*)\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g,
|
|
298
|
-
(match, left, chLeft, encodedLeftStr, chRight) => {
|
|
299
|
-
if (!joinString) {
|
|
300
|
-
return match;
|
|
301
|
-
}
|
|
302
|
-
// only mark b64 chunks to be joined if charsets match and left side does not end with =
|
|
303
|
-
if (chLeft === chRight && encodedLeftStr.length % 4 === 0 && !/=$/.test(encodedLeftStr)) {
|
|
304
|
-
// set a joiner marker
|
|
305
|
-
return left + '__\x00JOIN\x00__';
|
|
306
|
-
}
|
|
368
|
+
// A charset label runs to the next '?' so that labels containing punctuation, eg.
|
|
369
|
+
// ISO_8859-1:1987, are recognised. Whitespace is excluded so a stray '=?' in running text
|
|
370
|
+
// can not swallow the rest of the line.
|
|
371
|
+
const ENCODED_WORD_PATTERN = '=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=';
|
|
372
|
+
const ENCODED_WORD_REGEX = new RegExp(ENCODED_WORD_PATTERN, 'g');
|
|
307
373
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
374
|
+
// Only linear whitespace separates encoded words, the rest is content
|
|
375
|
+
const WORD_SEPARATOR_REGEX = /^[ \t\r\n]+$/;
|
|
376
|
+
|
|
377
|
+
const ENCODED_WORDS_ONLY_REGEX = new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Checks whether a string is nothing but RFC 2047 encoded words. Kept next to the grammar
|
|
381
|
+
* it depends on, so the pattern has a single definition.
|
|
382
|
+
*
|
|
383
|
+
* @param {String} str String to check
|
|
384
|
+
* @return {Boolean} true if the string holds encoded words and nothing else
|
|
385
|
+
*/
|
|
386
|
+
function isEncodedWordsOnly(str) {
|
|
387
|
+
return ENCODED_WORDS_ONLY_REGEX.test(str);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Splits a string into encoded words and the literal text around them.
|
|
392
|
+
*
|
|
393
|
+
* Working on a token list rather than marking joinable words with an in band sentinel
|
|
394
|
+
* means the input can not contain the marker, which previously let a sender delete text
|
|
395
|
+
* from a subject or a display name by writing the marker into the header themselves.
|
|
396
|
+
*
|
|
397
|
+
* @param {String} str String to split
|
|
398
|
+
* @return {Array} Array of `{text}` and `{charset, encoding, encodedText}` tokens
|
|
399
|
+
*/
|
|
400
|
+
function splitEncodedWords(str) {
|
|
401
|
+
const tokens = [];
|
|
402
|
+
|
|
403
|
+
ENCODED_WORD_REGEX.lastIndex = 0;
|
|
404
|
+
|
|
405
|
+
let pos = 0;
|
|
406
|
+
let match;
|
|
407
|
+
|
|
408
|
+
while ((match = ENCODED_WORD_REGEX.exec(str))) {
|
|
409
|
+
if (match.index > pos) {
|
|
410
|
+
tokens.push({ text: str.substring(pos, match.index) });
|
|
411
|
+
}
|
|
412
|
+
tokens.push({ charset: match[1], encoding: match[2], encodedText: match[3] });
|
|
413
|
+
pos = match.index + match[0].length;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (pos < str.length) {
|
|
417
|
+
tokens.push({ text: str.substring(pos) });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return tokens;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Checks if two adjacent encoded words may be decoded as a single unit. A multi byte
|
|
425
|
+
* character is often split across two words, so the bytes have to be concatenated before
|
|
426
|
+
* they are decoded. Base64 additionally needs the left chunk to end on a group boundary,
|
|
427
|
+
* otherwise the concatenation shifts every byte that follows.
|
|
428
|
+
*/
|
|
429
|
+
function canJoinWords(left, right) {
|
|
430
|
+
const encoding = left.encoding.toUpperCase();
|
|
431
|
+
|
|
432
|
+
if (left.charset !== right.charset || encoding !== right.encoding.toUpperCase()) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (encoding === 'B') {
|
|
437
|
+
return left.encodedText.length % 4 === 0 && !/=$/.test(left.encodedText);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Decodes a token list into a string, optionally merging adjacent encoded words.
|
|
445
|
+
*
|
|
446
|
+
* @param {Array} tokens Tokens from splitEncodedWords
|
|
447
|
+
* @param {Boolean} joinWords Whether adjacent encoded words may be decoded as one unit
|
|
448
|
+
* @return {String} Decoded string
|
|
449
|
+
*/
|
|
450
|
+
function renderTokens(tokens, joinWords) {
|
|
451
|
+
let result = '';
|
|
452
|
+
let pending = null;
|
|
453
|
+
|
|
454
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
455
|
+
const token = tokens[i];
|
|
456
|
+
|
|
457
|
+
if (token.text !== undefined) {
|
|
458
|
+
// whitespace between two encoded words is a folding artifact, not content
|
|
459
|
+
const nextToken = tokens[i + 1];
|
|
460
|
+
if (pending && nextToken && nextToken.text === undefined && WORD_SEPARATOR_REGEX.test(token.text)) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (pending) {
|
|
464
|
+
result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
465
|
+
pending = null;
|
|
466
|
+
}
|
|
467
|
+
result += token.text;
|
|
468
|
+
continue;
|
|
340
469
|
}
|
|
470
|
+
|
|
471
|
+
if (pending && joinWords && canJoinWords(pending, token)) {
|
|
472
|
+
pending.encodedText += token.encodedText;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (pending) {
|
|
477
|
+
result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
478
|
+
}
|
|
479
|
+
pending = { charset: token.charset, encoding: token.encoding, encodedText: token.encodedText };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (pending) {
|
|
483
|
+
result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
341
484
|
}
|
|
485
|
+
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function decodeWords(str) {
|
|
490
|
+
const tokens = splitEncodedWords((str || '').toString());
|
|
491
|
+
|
|
492
|
+
const result = renderTokens(tokens, true);
|
|
493
|
+
|
|
494
|
+
// A replacement character means the bytes did not decode, which happens when two
|
|
495
|
+
// words were joined that should have stayed apart. Retry keeping them separate.
|
|
496
|
+
return result.indexOf('\ufffd') < 0 ? result : renderTokens(tokens, false);
|
|
342
497
|
}
|
|
343
498
|
|
|
344
499
|
function decodeURIComponentWithCharset(encodedStr, charset) {
|
|
@@ -401,25 +556,45 @@ function decodeParameterValueContinuations(header) {
|
|
|
401
556
|
}
|
|
402
557
|
|
|
403
558
|
let value = header.params[key];
|
|
404
|
-
|
|
559
|
+
// RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded.
|
|
560
|
+
// A plain `name*0=` section is literal text, so decoding it invents characters
|
|
561
|
+
// that never appeared on the wire, turning `a%2F..%2Fetc` into a path traversal.
|
|
562
|
+
let encoded = match[0].charAt(match[0].length - 1) === '*';
|
|
563
|
+
|
|
564
|
+
if (nr === 0 && encoded && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
405
565
|
paramVal.charset = match[1] || 'utf-8';
|
|
406
566
|
value = match[2];
|
|
407
567
|
}
|
|
408
568
|
|
|
409
|
-
paramVal.values.push({ nr, value });
|
|
569
|
+
paramVal.values.push({ nr, value, encoded });
|
|
410
570
|
|
|
411
571
|
// remove the old reference
|
|
412
572
|
delete header.params[key];
|
|
413
573
|
});
|
|
414
574
|
|
|
415
575
|
paramKeys.forEach((paramVal, key) => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
576
|
+
let result = '';
|
|
577
|
+
// Adjacent encoded sections are decoded together, because a single multi byte
|
|
578
|
+
// character may be percent encoded across a section boundary.
|
|
579
|
+
let pending = '';
|
|
580
|
+
|
|
581
|
+
for (let part of paramVal.values.sort((a, b) => a.nr - b.nr)) {
|
|
582
|
+
if (part.encoded) {
|
|
583
|
+
pending += part.value;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (pending) {
|
|
587
|
+
result += decodeURIComponentWithCharset(pending, paramVal.charset);
|
|
588
|
+
pending = '';
|
|
589
|
+
}
|
|
590
|
+
result += part.value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (pending) {
|
|
594
|
+
result += decodeURIComponentWithCharset(pending, paramVal.charset);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
header.params[key] = result;
|
|
423
598
|
});
|
|
424
599
|
}
|
|
425
600
|
|
|
@@ -452,162 +627,155 @@ class Base64Decoder {
|
|
|
452
627
|
this.remainder = '';
|
|
453
628
|
}
|
|
454
629
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
this.remainder += str;
|
|
630
|
+
pushChunk(base64Str) {
|
|
631
|
+
if (base64Str.length) {
|
|
632
|
+
this.chunks.push(decodeBase64(base64Str));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
461
635
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
636
|
+
flushRemainder() {
|
|
637
|
+
this.pushChunk(this.remainder);
|
|
638
|
+
this.remainder = '';
|
|
639
|
+
}
|
|
465
640
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
641
|
+
update(buffer) {
|
|
642
|
+
let str = this.decoder.decode(buffer).replace(/[^a-zA-Z0-9+/=]+/g, '');
|
|
643
|
+
|
|
644
|
+
// '=' terminates a base64 unit. Some mailers pad every line, and erasing the
|
|
645
|
+
// padding used to concatenate the units, which knocked everything after the first
|
|
646
|
+
// embedded pad out of 4 character alignment and decoded it to garbage.
|
|
647
|
+
const units = str.split(/=+/);
|
|
648
|
+
for (let i = 0; i < units.length; i++) {
|
|
649
|
+
this.remainder += units[i];
|
|
650
|
+
// the trailing piece is not followed by padding, so it stays open
|
|
651
|
+
if (i < units.length - 1) {
|
|
652
|
+
this.flushRemainder();
|
|
472
653
|
}
|
|
654
|
+
}
|
|
473
655
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
656
|
+
if (this.remainder.length >= this.maxChunkSize) {
|
|
657
|
+
const alignedLength = Math.floor(this.remainder.length / 4) * 4;
|
|
658
|
+
this.pushChunk(this.remainder.substring(0, alignedLength));
|
|
659
|
+
this.remainder = this.remainder.substring(alignedLength);
|
|
477
660
|
}
|
|
478
661
|
}
|
|
479
662
|
|
|
480
663
|
finalize() {
|
|
481
|
-
|
|
482
|
-
this.chunks.push(decodeBase64(this.remainder));
|
|
483
|
-
}
|
|
664
|
+
this.flushRemainder();
|
|
484
665
|
|
|
485
666
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
486
667
|
}
|
|
487
668
|
}
|
|
488
669
|
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
const QP_SPLIT_REGEX = /(?==[a-f0-9]{2})/i;
|
|
492
|
-
const SOFT_LINE_BREAK_REGEX = /=\r?\n/g;
|
|
493
|
-
const PARTIAL_QP_ENDING_REGEX = /=[a-fA-F0-9]?$/;
|
|
670
|
+
const CHR_EQUALS = 0x3d;
|
|
671
|
+
const CHR_LF = 0x0a;
|
|
494
672
|
|
|
495
673
|
class QPDecoder {
|
|
496
|
-
constructor(
|
|
497
|
-
opts = opts || {};
|
|
498
|
-
|
|
499
|
-
this.decoder = opts.decoder || new TextDecoder();
|
|
500
|
-
|
|
674
|
+
constructor() {
|
|
501
675
|
this.maxChunkSize = 100 * 1024;
|
|
502
676
|
|
|
503
|
-
this.
|
|
677
|
+
this.buffer = new Uint8Array(this.maxChunkSize);
|
|
678
|
+
this.bufferPos = 0;
|
|
504
679
|
|
|
505
680
|
this.chunks = [];
|
|
506
681
|
}
|
|
507
682
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
for (let i = 0, len = encodedBytes.length; i < len; i++) {
|
|
512
|
-
dataView.setUint8(i, parseInt(encodedBytes[i], 16));
|
|
683
|
+
writeByte(byte) {
|
|
684
|
+
if (this.bufferPos >= this.buffer.length) {
|
|
685
|
+
this.flushBuffer();
|
|
513
686
|
}
|
|
514
|
-
|
|
687
|
+
this.buffer[this.bufferPos++] = byte;
|
|
515
688
|
}
|
|
516
689
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
for (let part of list) {
|
|
524
|
-
if (part.charAt(0) !== '=') {
|
|
525
|
-
if (encodedBytes.length) {
|
|
526
|
-
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
527
|
-
encodedBytes = [];
|
|
528
|
-
}
|
|
529
|
-
this.chunks.push(part);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (part.length === 3) {
|
|
534
|
-
// Validate that this is actually a valid QP sequence
|
|
535
|
-
if (VALID_QP_REGEX.test(part)) {
|
|
536
|
-
encodedBytes.push(part.substr(1));
|
|
537
|
-
} else {
|
|
538
|
-
// Not a valid QP sequence, treat as literal text
|
|
539
|
-
if (encodedBytes.length) {
|
|
540
|
-
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
541
|
-
encodedBytes = [];
|
|
542
|
-
}
|
|
543
|
-
this.chunks.push(part);
|
|
544
|
-
}
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (part.length > 3) {
|
|
549
|
-
// First 3 chars should be a valid QP sequence
|
|
550
|
-
const firstThree = part.substr(0, 3);
|
|
551
|
-
if (VALID_QP_REGEX.test(firstThree)) {
|
|
552
|
-
encodedBytes.push(part.substr(1, 2));
|
|
553
|
-
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
554
|
-
encodedBytes = [];
|
|
555
|
-
|
|
556
|
-
part = part.substr(3);
|
|
557
|
-
this.chunks.push(part);
|
|
558
|
-
} else {
|
|
559
|
-
// Not a valid QP sequence, treat entire part as literal
|
|
560
|
-
if (encodedBytes.length) {
|
|
561
|
-
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
562
|
-
encodedBytes = [];
|
|
563
|
-
}
|
|
564
|
-
this.chunks.push(part);
|
|
565
|
-
}
|
|
690
|
+
// Literal text is the bulk of a typical body, so it is copied in runs rather than a
|
|
691
|
+
// byte at a time
|
|
692
|
+
writeBytes(line, start, end) {
|
|
693
|
+
while (start < end) {
|
|
694
|
+
if (this.bufferPos >= this.buffer.length) {
|
|
695
|
+
this.flushBuffer();
|
|
566
696
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
this.
|
|
697
|
+
const count = Math.min(end - start, this.buffer.length - this.bufferPos);
|
|
698
|
+
this.buffer.set(line.subarray(start, start + count), this.bufferPos);
|
|
699
|
+
this.bufferPos += count;
|
|
700
|
+
start += count;
|
|
570
701
|
}
|
|
571
702
|
}
|
|
572
703
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
704
|
+
flushBuffer() {
|
|
705
|
+
if (this.bufferPos) {
|
|
706
|
+
this.chunks.push(this.buffer.slice(0, this.bufferPos));
|
|
707
|
+
this.bufferPos = 0;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
576
710
|
|
|
577
|
-
|
|
711
|
+
// Quoted-printable source is 7 bit by definition, so it is decoded byte by byte and
|
|
712
|
+
// the result is handed on as bytes. Running the body charset over the encoded source
|
|
713
|
+
// instead corrupted every part whose charset was not ASCII compatible: the same
|
|
714
|
+
// content that decoded correctly in base64 came out as mojibake in quoted-printable.
|
|
715
|
+
update(line) {
|
|
716
|
+
let len = line.length;
|
|
578
717
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
718
|
+
// a line ending in '=' is a soft line break, the newline is not part of the content
|
|
719
|
+
const softBreak = len > 0 && line[len - 1] === CHR_EQUALS;
|
|
720
|
+
if (softBreak) {
|
|
721
|
+
len--;
|
|
582
722
|
}
|
|
583
723
|
|
|
584
|
-
|
|
724
|
+
let literalStart = 0;
|
|
725
|
+
for (let i = 0; i < len; i++) {
|
|
726
|
+
if (line[i] !== CHR_EQUALS || i + 2 >= len) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
585
729
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
730
|
+
const high = hexNibble(line[i + 1]);
|
|
731
|
+
const low = hexNibble(line[i + 2]);
|
|
732
|
+
if (high < 0 || low < 0) {
|
|
733
|
+
// not a valid escape sequence, keep it as literal text
|
|
734
|
+
continue;
|
|
591
735
|
}
|
|
592
|
-
|
|
593
|
-
|
|
736
|
+
|
|
737
|
+
this.writeBytes(line, literalStart, i);
|
|
738
|
+
this.writeByte((high << 4) | low);
|
|
739
|
+
i += 2;
|
|
740
|
+
literalStart = i + 1;
|
|
594
741
|
}
|
|
742
|
+
this.writeBytes(line, literalStart, len);
|
|
595
743
|
|
|
596
|
-
|
|
744
|
+
if (!softBreak) {
|
|
745
|
+
this.writeByte(CHR_LF);
|
|
746
|
+
}
|
|
597
747
|
}
|
|
598
748
|
|
|
599
749
|
finalize() {
|
|
600
|
-
|
|
601
|
-
this.decodeChunks(this.remainder);
|
|
602
|
-
this.remainder = '';
|
|
603
|
-
}
|
|
750
|
+
this.flushBuffer();
|
|
604
751
|
|
|
605
752
|
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
606
753
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
607
754
|
}
|
|
608
755
|
}
|
|
609
756
|
|
|
610
|
-
|
|
757
|
+
// Header lines are decoded with ignoreBOM so that a U+FEFF at the start of a line is
|
|
758
|
+
// kept as a character instead of being swallowed. A stripped BOM turns a line a strict
|
|
759
|
+
// parser skips into a genuine header, which is how a second `From:` gets smuggled past
|
|
760
|
+
// anything that inspects the raw message.
|
|
761
|
+
const headerDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
762
|
+
|
|
763
|
+
// Trims only the whitespace RFC 5322 allows around a field name. String.prototype.trim
|
|
764
|
+
// also strips U+00A0, U+FEFF, U+2028 and the rest of the Unicode spaces, which turns a
|
|
765
|
+
// line that a strict parser rejects into a canonical field name: ` From:` became a
|
|
766
|
+
// `from` header, and since the first occurrence of a header wins it outranked the real
|
|
767
|
+
// sender. Leaving the character in the key keeps the line visible without letting it
|
|
768
|
+
// collide with a genuine header.
|
|
769
|
+
const trimWsp = str => str.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
770
|
+
|
|
771
|
+
// Headers that decide how this part's body is read, see processHeaders
|
|
772
|
+
const CONTENT_HEADERS = new Set([
|
|
773
|
+
'content-type',
|
|
774
|
+
'content-transfer-encoding',
|
|
775
|
+
'content-disposition',
|
|
776
|
+
'content-id',
|
|
777
|
+
'content-description'
|
|
778
|
+
]);
|
|
611
779
|
|
|
612
780
|
class MimeNode {
|
|
613
781
|
constructor(options) {
|
|
@@ -615,8 +783,11 @@ class MimeNode {
|
|
|
615
783
|
|
|
616
784
|
this.postalMime = this.options.postalMime;
|
|
617
785
|
|
|
618
|
-
this.root = !!this.options.parentNode;
|
|
619
786
|
this.childNodes = [];
|
|
787
|
+
// Cursor into childNodes for finalizeChildNodes. Every new part of a multipart
|
|
788
|
+
// finalizes its parent's children, so re-walking the whole array each time is
|
|
789
|
+
// quadratic in the number of parts.
|
|
790
|
+
this.finalizedChildCount = 0;
|
|
620
791
|
|
|
621
792
|
if (this.options.parentNode) {
|
|
622
793
|
this.parentNode = this.options.parentNode;
|
|
@@ -634,15 +805,15 @@ class MimeNode {
|
|
|
634
805
|
this.state = 'header';
|
|
635
806
|
|
|
636
807
|
this.headerLines = [];
|
|
637
|
-
this.headerSize = 0;
|
|
638
808
|
|
|
639
809
|
// RFC 2046 Section 5.1.5: multipart/digest defaults to message/rfc822
|
|
640
810
|
const parentMultipartType = this.options.parentMultipartType || null;
|
|
641
811
|
const defaultContentType = parentMultipartType === 'digest' ? 'message/rfc822' : 'text/plain';
|
|
642
812
|
|
|
813
|
+
// Replaced by the first matching header, see the CONTENT_HEADERS pass in
|
|
814
|
+
// processHeaders
|
|
643
815
|
this.contentType = {
|
|
644
|
-
value: defaultContentType
|
|
645
|
-
default: true
|
|
816
|
+
value: defaultContentType
|
|
646
817
|
};
|
|
647
818
|
|
|
648
819
|
this.contentTransferEncoding = {
|
|
@@ -662,7 +833,7 @@ class MimeNode {
|
|
|
662
833
|
if (/base64/i.test(transferEncoding)) {
|
|
663
834
|
this.contentDecoder = new Base64Decoder();
|
|
664
835
|
} else if (/quoted-printable/i.test(transferEncoding)) {
|
|
665
|
-
this.contentDecoder = new QPDecoder(
|
|
836
|
+
this.contentDecoder = new QPDecoder();
|
|
666
837
|
} else {
|
|
667
838
|
this.contentDecoder = new PassThroughDecoder();
|
|
668
839
|
}
|
|
@@ -700,17 +871,32 @@ class MimeNode {
|
|
|
700
871
|
}
|
|
701
872
|
|
|
702
873
|
async finalizeChildNodes() {
|
|
703
|
-
|
|
704
|
-
|
|
874
|
+
// Children are only ever appended, so everything before the cursor is already
|
|
875
|
+
// finished and re-visiting it only costs time.
|
|
876
|
+
while (this.finalizedChildCount < this.childNodes.length) {
|
|
877
|
+
await this.childNodes[this.finalizedChildCount++].finalize();
|
|
705
878
|
}
|
|
706
879
|
}
|
|
707
880
|
|
|
708
|
-
// Strip RFC 822 comments (parenthesized text) from structured header values
|
|
881
|
+
// Strip RFC 822 comments (parenthesized text) from structured header values.
|
|
882
|
+
//
|
|
883
|
+
// Inside an unquoted parameter value a parenthesis that continues the current token is
|
|
884
|
+
// content, because `filename=Invoice(1).pdf` is a filename and not a token followed by
|
|
885
|
+
// a comment, and deleting the parens silently renames the attachment.
|
|
709
886
|
stripComments(str) {
|
|
710
887
|
let result = '';
|
|
711
888
|
let depth = 0;
|
|
712
889
|
let escaped = false;
|
|
713
890
|
let inQuote = false;
|
|
891
|
+
// where the outermost comment opened, for the unbalanced case below
|
|
892
|
+
let commentStart = -1;
|
|
893
|
+
// A parameter value starts at `=` and ends at the `;` that begins the next one
|
|
894
|
+
let inParameterValue = false;
|
|
895
|
+
|
|
896
|
+
// A comment may only appear where linear whitespace is allowed, so inside a
|
|
897
|
+
// parameter value the parenthesis has to follow whitespace to open one. Outside
|
|
898
|
+
// one, eg. after the type itself, anything goes.
|
|
899
|
+
const opensComment = () => !inParameterValue || !result.length || /[ \t]$/.test(result);
|
|
714
900
|
|
|
715
901
|
for (let i = 0; i < str.length; i++) {
|
|
716
902
|
const chr = str.charAt(i);
|
|
@@ -738,7 +924,10 @@ class MimeNode {
|
|
|
738
924
|
}
|
|
739
925
|
|
|
740
926
|
if (!inQuote) {
|
|
741
|
-
if (chr === '(') {
|
|
927
|
+
if (chr === '(' && opensComment()) {
|
|
928
|
+
if (depth === 0) {
|
|
929
|
+
commentStart = i;
|
|
930
|
+
}
|
|
742
931
|
depth++;
|
|
743
932
|
continue;
|
|
744
933
|
}
|
|
@@ -746,6 +935,13 @@ class MimeNode {
|
|
|
746
935
|
depth--;
|
|
747
936
|
continue;
|
|
748
937
|
}
|
|
938
|
+
if (depth === 0) {
|
|
939
|
+
if (chr === '=') {
|
|
940
|
+
inParameterValue = true;
|
|
941
|
+
} else if (chr === ';') {
|
|
942
|
+
inParameterValue = false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
749
945
|
}
|
|
750
946
|
|
|
751
947
|
if (depth === 0) {
|
|
@@ -753,7 +949,14 @@ class MimeNode {
|
|
|
753
949
|
}
|
|
754
950
|
}
|
|
755
951
|
|
|
756
|
-
|
|
952
|
+
if (depth === 0) {
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// An unbalanced `(` is not a comment. Dropping everything after it would take any
|
|
957
|
+
// parameter that follows with it, including the boundary that holds the message
|
|
958
|
+
// together, so the dangling text is only discarded when nothing follows it.
|
|
959
|
+
return str.indexOf(';', commentStart) < 0 ? result : str;
|
|
757
960
|
}
|
|
758
961
|
|
|
759
962
|
parseStructuredHeader(str) {
|
|
@@ -769,62 +972,123 @@ class MimeNode {
|
|
|
769
972
|
let value = '';
|
|
770
973
|
let stage = 'value';
|
|
771
974
|
|
|
975
|
+
// Whitespace seen outside a quoted string is held back until a significant
|
|
976
|
+
// character follows it, so surrounding whitespace can be dropped without
|
|
977
|
+
// trimming spaces the sender quoted on purpose. Trimming the stored value
|
|
978
|
+
// instead loses the trailing space in `filename*0="Annual Report "`, which the
|
|
979
|
+
// next continuation section is meant to be appended to.
|
|
980
|
+
let pendingSpace = '';
|
|
981
|
+
let quoteClosed = false;
|
|
982
|
+
|
|
772
983
|
let quote = false;
|
|
773
984
|
let escaped = false;
|
|
774
985
|
let chr;
|
|
775
986
|
|
|
987
|
+
const addChr = c => {
|
|
988
|
+
if (value.length) {
|
|
989
|
+
value += pendingSpace;
|
|
990
|
+
}
|
|
991
|
+
pendingSpace = '';
|
|
992
|
+
value += c;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const takeValue = () => {
|
|
996
|
+
const result = value;
|
|
997
|
+
value = '';
|
|
998
|
+
pendingSpace = '';
|
|
999
|
+
quoteClosed = false;
|
|
1000
|
+
return result;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// A duplicated parameter resolves to its first occurrence, matching how duplicated
|
|
1004
|
+
// headers are resolved. Letting the last one win means `boundary="b"; boundary="c"`
|
|
1005
|
+
// registers a boundary that no delimiter in the message matches, which drops the
|
|
1006
|
+
// body without an error. hasOwnProperty, because a parameter may be named
|
|
1007
|
+
// `constructor` or `toString`.
|
|
1008
|
+
const storeParam = (name, result) => {
|
|
1009
|
+
if (!Object.prototype.hasOwnProperty.call(response.params, name)) {
|
|
1010
|
+
response.params[name] = result;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const storeValue = () => {
|
|
1015
|
+
const result = takeValue();
|
|
1016
|
+
if (key === false) {
|
|
1017
|
+
response.value = result;
|
|
1018
|
+
} else {
|
|
1019
|
+
storeParam(key, result);
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// A parameter name with no `=` is a valueless parameter, not the start of the
|
|
1024
|
+
// next one. Without this the name would keep growing across the `;` and swallow
|
|
1025
|
+
// whatever followed, which is how `x=1; flag; boundary="AAA"` loses its boundary.
|
|
1026
|
+
const storeEmptyKey = () => {
|
|
1027
|
+
const name = takeValue().trim();
|
|
1028
|
+
if (name) {
|
|
1029
|
+
storeParam(name.toLowerCase(), '');
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
|
|
776
1033
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
777
1034
|
chr = str.charAt(i);
|
|
778
1035
|
switch (stage) {
|
|
779
1036
|
case 'key':
|
|
780
1037
|
if (chr === '=') {
|
|
781
|
-
key =
|
|
1038
|
+
key = takeValue().trim().toLowerCase();
|
|
782
1039
|
stage = 'value';
|
|
783
|
-
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
if (chr === ';') {
|
|
1043
|
+
storeEmptyKey();
|
|
784
1044
|
break;
|
|
785
1045
|
}
|
|
786
1046
|
value += chr;
|
|
787
1047
|
break;
|
|
788
1048
|
case 'value':
|
|
789
1049
|
if (escaped) {
|
|
790
|
-
|
|
791
|
-
} else if (chr === '\\') {
|
|
1050
|
+
addChr(chr);
|
|
1051
|
+
} else if (quote && chr === '\\') {
|
|
1052
|
+
// backslash only escapes inside a quoted string, everywhere else
|
|
1053
|
+
// it is an ordinary character. Treating it as an escape turns
|
|
1054
|
+
// `filename=C:\Users\me\a.txt` into `C:Usersmea.txt`.
|
|
792
1055
|
escaped = true;
|
|
793
1056
|
continue;
|
|
794
1057
|
} else if (quote && chr === quote) {
|
|
795
1058
|
quote = false;
|
|
1059
|
+
quoteClosed = true;
|
|
796
1060
|
} else if (!quote && chr === '"') {
|
|
797
1061
|
quote = chr;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
response.params[key] = value.trim();
|
|
1062
|
+
// whitespace before a quote that opens the value is padding, but
|
|
1063
|
+
// between a token and a quoted string it is content
|
|
1064
|
+
if (value.length) {
|
|
1065
|
+
value += pendingSpace;
|
|
803
1066
|
}
|
|
1067
|
+
pendingSpace = '';
|
|
1068
|
+
} else if (!quote && chr === ';') {
|
|
1069
|
+
storeValue();
|
|
804
1070
|
stage = 'key';
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1071
|
+
} else if (!quote && (chr === ' ' || chr === '\t')) {
|
|
1072
|
+
pendingSpace += chr;
|
|
1073
|
+
} else if (!quoteClosed) {
|
|
1074
|
+
addChr(chr);
|
|
808
1075
|
}
|
|
1076
|
+
// Anything else is trailing junk after a closed quoted string. RFC 2045
|
|
1077
|
+
// says a parameter value is a token or a quoted string, not both, and
|
|
1078
|
+
// appending the junk is how `boundary="AAA" (unterminated comment`
|
|
1079
|
+
// turned into a boundary that no delimiter in the message matches.
|
|
809
1080
|
escaped = false;
|
|
810
1081
|
break;
|
|
811
1082
|
}
|
|
812
1083
|
}
|
|
813
1084
|
|
|
814
1085
|
// finalize remainder
|
|
815
|
-
value = value.trim();
|
|
816
1086
|
if (stage === 'value') {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
response.value = value;
|
|
820
|
-
} else {
|
|
821
|
-
// subkey value
|
|
822
|
-
response.params[key] = value;
|
|
823
|
-
}
|
|
824
|
-
} else if (value) {
|
|
1087
|
+
storeValue();
|
|
1088
|
+
} else {
|
|
825
1089
|
// treat as key without value, see emptykey:
|
|
826
1090
|
// Header-Key: somevalue; key=value; emptykey
|
|
827
|
-
|
|
1091
|
+
storeEmptyKey();
|
|
828
1092
|
}
|
|
829
1093
|
|
|
830
1094
|
if (response.value) {
|
|
@@ -841,6 +1105,11 @@ class MimeNode {
|
|
|
841
1105
|
return (
|
|
842
1106
|
str
|
|
843
1107
|
.split(/\r?\n/)
|
|
1108
|
+
// remove whitespace stuffing before anything else
|
|
1109
|
+
// http://tools.ietf.org/html/rfc3676#section-4.4
|
|
1110
|
+
// doing it after the join leaves the stuffed space of a continuation line
|
|
1111
|
+
// sitting in the middle of the joined paragraph
|
|
1112
|
+
.map(line => (line.charAt(0) === ' ' ? line.slice(1) : line))
|
|
844
1113
|
// remove soft linebreaks
|
|
845
1114
|
// soft linebreaks are added after space symbols
|
|
846
1115
|
.reduce((previousValue, currentValue) => {
|
|
@@ -856,9 +1125,6 @@ class MimeNode {
|
|
|
856
1125
|
return previousValue + '\n' + currentValue;
|
|
857
1126
|
}
|
|
858
1127
|
})
|
|
859
|
-
// remove whitespace stuffing
|
|
860
|
-
// http://tools.ietf.org/html/rfc3676#section-4.4
|
|
861
|
-
.replace(/^ /gm, '')
|
|
862
1128
|
);
|
|
863
1129
|
}
|
|
864
1130
|
|
|
@@ -877,27 +1143,38 @@ class MimeNode {
|
|
|
877
1143
|
}
|
|
878
1144
|
|
|
879
1145
|
processHeaders() {
|
|
880
|
-
// First pass:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1146
|
+
// First pass: group folded continuation lines with the header they belong to.
|
|
1147
|
+
//
|
|
1148
|
+
// Only SP and HTAB continue a header (RFC 5322 3.2.2 WSP). JS `\s` also matches
|
|
1149
|
+
// NBSP, vertical tab, form feed and U+2028, so a line starting with one of those
|
|
1150
|
+
// used to be absorbed into the header above it and disappear from both `headers`
|
|
1151
|
+
// and `headerLines` while a strict parser still sees it as a header of its own.
|
|
1152
|
+
//
|
|
1153
|
+
// Collecting into an array and joining once keeps this linear. Appending onto the
|
|
1154
|
+
// previous string in a backward pass re-scans the joined value on every line,
|
|
1155
|
+
// which is quadratic in the number of folds and lets a message that fits inside
|
|
1156
|
+
// maxHeadersSize burn seconds of CPU.
|
|
1157
|
+
let foldedLines = [];
|
|
1158
|
+
for (let line of this.headerLines) {
|
|
1159
|
+
if (foldedLines.length && /^[ \t]/.test(line)) {
|
|
1160
|
+
foldedLines[foldedLines.length - 1].push(line);
|
|
1161
|
+
} else {
|
|
1162
|
+
foldedLines.push([line]);
|
|
886
1163
|
}
|
|
887
1164
|
}
|
|
888
1165
|
|
|
889
1166
|
// Initialize rawHeaderLines to store unmodified lines
|
|
890
1167
|
this.rawHeaderLines = [];
|
|
891
1168
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
//
|
|
895
|
-
for (let
|
|
896
|
-
let rawLine =
|
|
1169
|
+
let seenContentHeaders = new Set();
|
|
1170
|
+
|
|
1171
|
+
// Second pass: process headers in document order
|
|
1172
|
+
for (let parts of foldedLines) {
|
|
1173
|
+
let rawLine = parts.join('\n');
|
|
897
1174
|
|
|
898
1175
|
// Extract key from raw line for rawHeaderLines
|
|
899
1176
|
let sep = rawLine.indexOf(':');
|
|
900
|
-
let rawKey = sep < 0 ? rawLine
|
|
1177
|
+
let rawKey = trimWsp(sep < 0 ? rawLine : rawLine.substr(0, sep));
|
|
901
1178
|
|
|
902
1179
|
// Store raw line with lowercase key
|
|
903
1180
|
this.rawHeaderLines.push({
|
|
@@ -905,31 +1182,44 @@ class MimeNode {
|
|
|
905
1182
|
line: rawLine
|
|
906
1183
|
});
|
|
907
1184
|
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
let
|
|
1185
|
+
// Unfolding removes the line break and keeps the folding whitespace, so
|
|
1186
|
+
// `Subject: Hello\r\n World` stays `Hello World`. Collapsing every
|
|
1187
|
+
// whitespace run instead also rewrote boundary values and filenames, and it
|
|
1188
|
+
// replaced the non-ASCII spaces that raw UTF-8 headers (RFC 6532) may carry.
|
|
1189
|
+
let unfoldedLine = parts.join('');
|
|
1190
|
+
sep = unfoldedLine.indexOf(':');
|
|
1191
|
+
let key = trimWsp(sep < 0 ? unfoldedLine : unfoldedLine.substr(0, sep));
|
|
1192
|
+
// A bare CR is not legal in a field body. It used to be folded into a space by
|
|
1193
|
+
// the whitespace collapse, and passing it through would hand consumers that
|
|
1194
|
+
// write the value back out a line of their own.
|
|
1195
|
+
let value = sep < 0 ? '' : trimWsp(unfoldedLine.substr(sep + 1).replace(/[\r\n]+/g, ' '));
|
|
913
1196
|
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
914
1197
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1198
|
+
// A header that decides how the body is read must resolve the same way every
|
|
1199
|
+
// time it is duplicated, otherwise a message can present one Content-Type to a
|
|
1200
|
+
// scanner and a different one here. Every one of these takes the first
|
|
1201
|
+
// occurrence and later copies are ignored.
|
|
1202
|
+
const lowerKey = key.toLowerCase();
|
|
1203
|
+
if (CONTENT_HEADERS.has(lowerKey) && !seenContentHeaders.has(lowerKey)) {
|
|
1204
|
+
seenContentHeaders.add(lowerKey);
|
|
1205
|
+
|
|
1206
|
+
switch (lowerKey) {
|
|
1207
|
+
case 'content-type':
|
|
918
1208
|
this.contentType = { value, parsed: {} };
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1209
|
+
break;
|
|
1210
|
+
case 'content-transfer-encoding':
|
|
1211
|
+
this.contentTransferEncoding = { value, parsed: {} };
|
|
1212
|
+
break;
|
|
1213
|
+
case 'content-disposition':
|
|
1214
|
+
this.contentDisposition = { value, parsed: {} };
|
|
1215
|
+
break;
|
|
1216
|
+
case 'content-id':
|
|
1217
|
+
this.contentId = value;
|
|
1218
|
+
break;
|
|
1219
|
+
case 'content-description':
|
|
1220
|
+
this.contentDescription = value;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
933
1223
|
}
|
|
934
1224
|
}
|
|
935
1225
|
|
|
@@ -948,10 +1238,13 @@ class MimeNode {
|
|
|
948
1238
|
|
|
949
1239
|
this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value);
|
|
950
1240
|
|
|
951
|
-
|
|
1241
|
+
// Take the first token rather than splitting on the first non-token character.
|
|
1242
|
+
// `split()` returns an empty string for anything that does not start with a word
|
|
1243
|
+
// character, so `(comment) base64` and `"base64"` used to fall through to the
|
|
1244
|
+
// pass-through decoder and hand the caller undecoded base64 as the message body.
|
|
1245
|
+
this.contentTransferEncoding.encoding = (this.stripComments(this.contentTransferEncoding.value)
|
|
952
1246
|
.toLowerCase()
|
|
953
|
-
.
|
|
954
|
-
.shift();
|
|
1247
|
+
.match(/[\w-]+/) || [''])[0];
|
|
955
1248
|
|
|
956
1249
|
this.setupContentDecoder(this.contentTransferEncoding.encoding);
|
|
957
1250
|
}
|
|
@@ -964,14 +1257,17 @@ class MimeNode {
|
|
|
964
1257
|
return this.processHeaders();
|
|
965
1258
|
}
|
|
966
1259
|
|
|
967
|
-
|
|
1260
|
+
// Counted across the whole message, not per part. A per-node budget lets a
|
|
1261
|
+
// multipart carry the limit again for every part it declares, so a message
|
|
1262
|
+
// many times over the limit still parses.
|
|
1263
|
+
this.postalMime.headerSize += line.length;
|
|
968
1264
|
|
|
969
|
-
if (this.headerSize > this.options.maxHeadersSize) {
|
|
1265
|
+
if (this.postalMime.headerSize > this.options.maxHeadersSize) {
|
|
970
1266
|
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
|
|
971
1267
|
throw error;
|
|
972
1268
|
}
|
|
973
1269
|
|
|
974
|
-
this.headerLines.push(
|
|
1270
|
+
this.headerLines.push(headerDecoder.decode(line));
|
|
975
1271
|
break;
|
|
976
1272
|
case 'body': {
|
|
977
1273
|
// add line to body
|
|
@@ -3311,6 +3607,30 @@ function htmlToText(str) {
|
|
|
3311
3607
|
return str;
|
|
3312
3608
|
}
|
|
3313
3609
|
|
|
3610
|
+
// A message date is not always a date. PostalMime keeps the raw header value when it does
|
|
3611
|
+
// not parse, and Intl.DateTimeFormat throws a RangeError on that, which used to reject the
|
|
3612
|
+
// whole parse of any message carrying a forwarded copy with a broken Date header.
|
|
3613
|
+
function formatDate(date) {
|
|
3614
|
+
if (typeof Intl === 'undefined') {
|
|
3615
|
+
return date;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
const parsed = new Date(date);
|
|
3619
|
+
if (isNaN(parsed.getTime())) {
|
|
3620
|
+
return date;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
return new Intl.DateTimeFormat('default', {
|
|
3624
|
+
year: 'numeric',
|
|
3625
|
+
month: 'numeric',
|
|
3626
|
+
day: 'numeric',
|
|
3627
|
+
hour: 'numeric',
|
|
3628
|
+
minute: 'numeric',
|
|
3629
|
+
second: 'numeric',
|
|
3630
|
+
hour12: false
|
|
3631
|
+
}).format(parsed);
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3314
3634
|
function formatTextAddress(address) {
|
|
3315
3635
|
return []
|
|
3316
3636
|
.concat(address.name || [])
|
|
@@ -3416,7 +3736,9 @@ function formatTextHeader(message) {
|
|
|
3416
3736
|
let rows = [];
|
|
3417
3737
|
|
|
3418
3738
|
if (message.from) {
|
|
3419
|
-
|
|
3739
|
+
// through the plural formatter, because `From:` may hold RFC 5322 group syntax
|
|
3740
|
+
// and a group has no address of its own
|
|
3741
|
+
rows.push({ key: 'From', val: formatTextAddresses([message.from]) });
|
|
3420
3742
|
}
|
|
3421
3743
|
|
|
3422
3744
|
if (message.subject) {
|
|
@@ -3424,22 +3746,7 @@ function formatTextHeader(message) {
|
|
|
3424
3746
|
}
|
|
3425
3747
|
|
|
3426
3748
|
if (message.date) {
|
|
3427
|
-
|
|
3428
|
-
year: 'numeric',
|
|
3429
|
-
month: 'numeric',
|
|
3430
|
-
day: 'numeric',
|
|
3431
|
-
hour: 'numeric',
|
|
3432
|
-
minute: 'numeric',
|
|
3433
|
-
second: 'numeric',
|
|
3434
|
-
hour12: false
|
|
3435
|
-
};
|
|
3436
|
-
|
|
3437
|
-
let dateStr =
|
|
3438
|
-
typeof Intl === 'undefined'
|
|
3439
|
-
? message.date
|
|
3440
|
-
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
|
|
3441
|
-
|
|
3442
|
-
rows.push({ key: 'Date', val: dateStr });
|
|
3749
|
+
rows.push({ key: 'Date', val: formatDate(message.date) });
|
|
3443
3750
|
}
|
|
3444
3751
|
|
|
3445
3752
|
if (message.to && message.to.length) {
|
|
@@ -3506,7 +3813,9 @@ function formatHtmlHeader(message) {
|
|
|
3506
3813
|
|
|
3507
3814
|
if (message.from) {
|
|
3508
3815
|
rows.push(
|
|
3509
|
-
|
|
3816
|
+
// through the plural formatter, because `From:` may hold RFC 5322 group syntax
|
|
3817
|
+
// and a group has no address of its own
|
|
3818
|
+
`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddresses([message.from])}</div>`
|
|
3510
3819
|
);
|
|
3511
3820
|
}
|
|
3512
3821
|
|
|
@@ -3519,25 +3828,10 @@ function formatHtmlHeader(message) {
|
|
|
3519
3828
|
}
|
|
3520
3829
|
|
|
3521
3830
|
if (message.date) {
|
|
3522
|
-
let dateOptions = {
|
|
3523
|
-
year: 'numeric',
|
|
3524
|
-
month: 'numeric',
|
|
3525
|
-
day: 'numeric',
|
|
3526
|
-
hour: 'numeric',
|
|
3527
|
-
minute: 'numeric',
|
|
3528
|
-
second: 'numeric',
|
|
3529
|
-
hour12: false
|
|
3530
|
-
};
|
|
3531
|
-
|
|
3532
|
-
let dateStr =
|
|
3533
|
-
typeof Intl === 'undefined'
|
|
3534
|
-
? message.date
|
|
3535
|
-
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
|
|
3536
|
-
|
|
3537
3831
|
rows.push(
|
|
3538
3832
|
`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
|
|
3539
3833
|
message.date
|
|
3540
|
-
)}">${escapeHtml(
|
|
3834
|
+
)}">${escapeHtml(formatDate(message.date))}</div>`
|
|
3541
3835
|
);
|
|
3542
3836
|
}
|
|
3543
3837
|
|
|
@@ -3566,6 +3860,56 @@ function formatHtmlHeader(message) {
|
|
|
3566
3860
|
return template;
|
|
3567
3861
|
}
|
|
3568
3862
|
|
|
3863
|
+
const WORD_CHAR_REGEX = /\w/;
|
|
3864
|
+
const NON_SPACE_TOKEN_REGEX = /[^\s]+/g;
|
|
3865
|
+
|
|
3866
|
+
/**
|
|
3867
|
+
* Finds the first address looking token in a run of text.
|
|
3868
|
+
*
|
|
3869
|
+
* This replaces a `\s*\b[^@\s]+@[^\s]+\b\s*` scan over the whole string, which backtracks
|
|
3870
|
+
* quadratically: the leading `\s*` makes every position inside a whitespace run a viable
|
|
3871
|
+
* start, and `[^@\s]+` then gives back one character at a time looking for an '@'. A
|
|
3872
|
+
* single header well inside the default size limit could hold a core busy for minutes.
|
|
3873
|
+
*
|
|
3874
|
+
* Scanning whitespace delimited tokens instead is linear and keeps the word boundary
|
|
3875
|
+
* semantics of the regex: the local part has to open on a word character and the domain
|
|
3876
|
+
* has to end on one.
|
|
3877
|
+
*
|
|
3878
|
+
* @param {String} text Text to search
|
|
3879
|
+
* @return {Object|null} `{index, length, value}` of the address, or null if there is none
|
|
3880
|
+
*/
|
|
3881
|
+
function findAddressInText(text) {
|
|
3882
|
+
NON_SPACE_TOKEN_REGEX.lastIndex = 0;
|
|
3883
|
+
|
|
3884
|
+
let match;
|
|
3885
|
+
while ((match = NON_SPACE_TOKEN_REGEX.exec(text))) {
|
|
3886
|
+
const token = match[0];
|
|
3887
|
+
const at = token.indexOf('@');
|
|
3888
|
+
|
|
3889
|
+
// `\b[^@\s]+@` needs at least one character before the '@'
|
|
3890
|
+
let start = 0;
|
|
3891
|
+
while (start < at && !WORD_CHAR_REGEX.test(token.charAt(start))) {
|
|
3892
|
+
start++;
|
|
3893
|
+
}
|
|
3894
|
+
if (start >= at) {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
// `[^\s]+\b` needs at least one character after the '@', ending on a word character
|
|
3899
|
+
let end = token.length;
|
|
3900
|
+
while (end > at + 1 && !WORD_CHAR_REGEX.test(token.charAt(end - 1))) {
|
|
3901
|
+
end--;
|
|
3902
|
+
}
|
|
3903
|
+
if (end <= at + 1) {
|
|
3904
|
+
continue;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
return { index: match.index + start, length: end - start, value: token.substring(start, end) };
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
return null;
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3569
3913
|
/**
|
|
3570
3914
|
* Converts tokens for a single address into an address object
|
|
3571
3915
|
*
|
|
@@ -3684,25 +4028,24 @@ function _handleAddress(tokens, depth) {
|
|
|
3684
4028
|
}
|
|
3685
4029
|
}
|
|
3686
4030
|
|
|
3687
|
-
let _regexHandler = function (address) {
|
|
3688
|
-
if (!data.address.length) {
|
|
3689
|
-
data.address = [address.trim()];
|
|
3690
|
-
return ' ';
|
|
3691
|
-
} else {
|
|
3692
|
-
return address;
|
|
3693
|
-
}
|
|
3694
|
-
};
|
|
3695
|
-
|
|
3696
4031
|
// still no address
|
|
3697
4032
|
if (!data.address.length) {
|
|
3698
4033
|
for (i = data.text.length - 1; i >= 0; i--) {
|
|
3699
4034
|
// Security fix: Do not extract email addresses from quoted strings
|
|
3700
4035
|
if (!data.textWasQuoted[i]) {
|
|
3701
|
-
//
|
|
3702
|
-
|
|
3703
|
-
if (
|
|
4036
|
+
// handles an email address that has more than one @
|
|
4037
|
+
const found = findAddressInText(data.text[i]);
|
|
4038
|
+
if (found) {
|
|
4039
|
+
data.address = [found.value];
|
|
4040
|
+
// the address and the whitespace around it collapse to one space
|
|
4041
|
+
data.text[i] = (
|
|
4042
|
+
data.text[i].substring(0, found.index).replace(/\s+$/, '') +
|
|
4043
|
+
' ' +
|
|
4044
|
+
data.text[i].substring(found.index + found.length).replace(/^\s+/, '')
|
|
4045
|
+
).trim();
|
|
3704
4046
|
break;
|
|
3705
4047
|
}
|
|
4048
|
+
data.text[i] = data.text[i].trim();
|
|
3706
4049
|
}
|
|
3707
4050
|
}
|
|
3708
4051
|
}
|
|
@@ -3723,7 +4066,10 @@ function _handleAddress(tokens, depth) {
|
|
|
3723
4066
|
data.text = data.text.join(' ');
|
|
3724
4067
|
data.address = data.address.join(' ');
|
|
3725
4068
|
|
|
3726
|
-
|
|
4069
|
+
// `^=\?[^=]+?=$` could not match a base64 word whose padding puts an '=' inside it,
|
|
4070
|
+
// so whether a bare encoded word was decoded or left to become the address itself
|
|
4071
|
+
// came down to whether its payload happened to need padding.
|
|
4072
|
+
if (!data.address && isEncodedWordsOnly(data.text.trim())) {
|
|
3727
4073
|
// try to extract words from text content
|
|
3728
4074
|
const decodedText = decodeWords(data.text);
|
|
3729
4075
|
// Security: only re-parse if decoded text contains angle-bracket addresses.
|
|
@@ -4087,6 +4433,9 @@ class PostalMime {
|
|
|
4087
4433
|
});
|
|
4088
4434
|
this.boundaries = [];
|
|
4089
4435
|
|
|
4436
|
+
// Header bytes seen across every part of this message, see MimeNode.feed
|
|
4437
|
+
this.headerSize = 0;
|
|
4438
|
+
|
|
4090
4439
|
this.textContent = {};
|
|
4091
4440
|
this.attachments = [];
|
|
4092
4441
|
|
|
@@ -4316,7 +4665,9 @@ class PostalMime {
|
|
|
4316
4665
|
}
|
|
4317
4666
|
|
|
4318
4667
|
if (node.contentDescription) {
|
|
4319
|
-
|
|
4668
|
+
// decoded like filename, it is an unstructured header that may
|
|
4669
|
+
// carry encoded words
|
|
4670
|
+
attachment.description = decodeWords(node.contentDescription);
|
|
4320
4671
|
}
|
|
4321
4672
|
|
|
4322
4673
|
if (node.contentId) {
|
|
@@ -4536,9 +4887,12 @@ class PostalMime {
|
|
|
4536
4887
|
buf = await blobToArrayBuffer(buf);
|
|
4537
4888
|
}
|
|
4538
4889
|
|
|
4539
|
-
// Cast Node.js Buffer
|
|
4540
|
-
|
|
4541
|
-
|
|
4890
|
+
// Cast a Node.js Buffer, a typed array or a DataView into an ArrayBuffer.
|
|
4891
|
+
// `new Uint8Array(view)` only works for array-likes, so a DataView produced an
|
|
4892
|
+
// empty buffer and the message parsed to nothing without an error. Slicing off
|
|
4893
|
+
// byteOffset also keeps views over a larger buffer from reading their neighbours.
|
|
4894
|
+
if (ArrayBuffer.isView(buf)) {
|
|
4895
|
+
buf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
4542
4896
|
}
|
|
4543
4897
|
|
|
4544
4898
|
this.buf = buf;
|
|
@@ -4555,9 +4909,11 @@ class PostalMime {
|
|
|
4555
4909
|
await this.processNodeTree();
|
|
4556
4910
|
|
|
4557
4911
|
const message = {
|
|
4558
|
-
headers: this.root.headers
|
|
4559
|
-
|
|
4560
|
-
.
|
|
4912
|
+
headers: this.root.headers.map(entry => ({
|
|
4913
|
+
key: entry.key,
|
|
4914
|
+
originalKey: entry.originalKey,
|
|
4915
|
+
value: entry.value
|
|
4916
|
+
}))
|
|
4561
4917
|
};
|
|
4562
4918
|
|
|
4563
4919
|
for (const key of ['from', 'sender']) {
|
|
@@ -4626,8 +4982,8 @@ class PostalMime {
|
|
|
4626
4982
|
|
|
4627
4983
|
message.attachments = this.attachments;
|
|
4628
4984
|
|
|
4629
|
-
// Expose raw header lines
|
|
4630
|
-
message.headerLines = (this.root.rawHeaderLines || []).slice()
|
|
4985
|
+
// Expose raw header lines, in the same order as the headers array
|
|
4986
|
+
message.headerLines = (this.root.rawHeaderLines || []).slice();
|
|
4631
4987
|
|
|
4632
4988
|
switch (this.attachmentEncoding) {
|
|
4633
4989
|
case 'arraybuffer':
|