@mavogel/awscdk-rootmail 0.1.23 → 0.1.25
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 +4 -4
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/node_modules/@zone-eu/mailsplit/README.md +1 -1
- package/node_modules/@zone-eu/mailsplit/lib/headers.js +10 -4
- package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +5 -4
- package/node_modules/@zone-eu/mailsplit/lib/mime-node.d.ts +4 -1
- package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +23 -2
- package/node_modules/@zone-eu/mailsplit/lib/types.d.ts +3 -1
- package/node_modules/@zone-eu/mailsplit/package.json +4 -4
- package/node_modules/libbase64/CHANGELOG.md +20 -0
- package/node_modules/libbase64/README.md +40 -13
- package/node_modules/libbase64/dist/cjs/libbase64.d.ts +80 -0
- package/node_modules/libbase64/dist/cjs/libbase64.js +310 -0
- package/node_modules/libbase64/dist/cjs/package.json +3 -0
- package/node_modules/libbase64/dist/esm/libbase64.d.ts +80 -0
- package/node_modules/libbase64/{lib → dist/esm}/libbase64.js +74 -117
- package/node_modules/libbase64/dist/esm/package.json +3 -0
- package/node_modules/libbase64/package.json +59 -13
- package/node_modules/libmime/CHANGELOG.md +31 -0
- package/node_modules/libmime/README.md +55 -26
- package/node_modules/libmime/SECURITY.md +152 -0
- package/node_modules/libmime/dist/cjs/charset.d.ts +43 -0
- package/node_modules/libmime/dist/cjs/charset.js +135 -0
- package/node_modules/libmime/dist/cjs/charsets.d.ts +2 -0
- package/node_modules/libmime/dist/cjs/charsets.js +213 -0
- package/node_modules/libmime/dist/cjs/get-charset-name.d.ts +1 -0
- package/node_modules/libmime/dist/cjs/get-charset-name.js +224 -0
- package/node_modules/libmime/dist/cjs/libmime.d.ts +218 -0
- package/node_modules/libmime/dist/cjs/libmime.js +1013 -0
- package/node_modules/libmime/dist/cjs/mimetypes.d.ts +7 -0
- package/node_modules/libmime/dist/cjs/mimetypes.js +2072 -0
- package/node_modules/libmime/dist/cjs/package.json +3 -0
- package/node_modules/libmime/dist/cjs/types.d.ts +59 -0
- package/node_modules/libmime/dist/cjs/types.js +4 -0
- package/node_modules/libmime/dist/esm/charset.d.ts +43 -0
- package/node_modules/libmime/dist/esm/charset.js +125 -0
- package/node_modules/libmime/dist/esm/charsets.d.ts +2 -0
- package/node_modules/libmime/{lib → dist/esm}/charsets.js +3 -5
- package/node_modules/libmime/dist/esm/get-charset-name.d.ts +1 -0
- package/node_modules/libmime/{lib → dist/esm}/get-charset-name.js +1 -8
- package/node_modules/libmime/dist/esm/libmime.d.ts +219 -0
- package/node_modules/libmime/{lib → dist/esm}/libmime.js +256 -329
- package/node_modules/libmime/dist/esm/mimetypes.d.ts +7 -0
- package/node_modules/libmime/dist/esm/mimetypes.js +2068 -0
- package/node_modules/libmime/dist/esm/package.json +3 -0
- package/node_modules/libmime/dist/esm/types.d.ts +59 -0
- package/node_modules/libmime/dist/esm/types.js +3 -0
- package/node_modules/libmime/package.json +58 -15
- package/node_modules/libqp/CHANGELOG.md +18 -0
- package/node_modules/libqp/README.md +50 -19
- package/node_modules/libqp/dist/cjs/libqp.d.ts +74 -0
- package/node_modules/libqp/dist/cjs/libqp.js +378 -0
- package/node_modules/libqp/dist/cjs/package.json +3 -0
- package/node_modules/libqp/dist/esm/libqp.d.ts +74 -0
- package/node_modules/libqp/dist/esm/libqp.js +369 -0
- package/node_modules/libqp/dist/esm/package.json +3 -0
- package/node_modules/libqp/lib/libqp.js +4 -383
- package/node_modules/libqp/lib/package.json +3 -0
- package/node_modules/libqp/package.json +62 -6
- package/node_modules/mailparser/.release-please-manifest.json +1 -1
- package/node_modules/mailparser/CHANGELOG.md +42 -0
- package/node_modules/mailparser/package.json +5 -5
- package/node_modules/nodemailer/CHANGELOG.md +44 -0
- package/node_modules/nodemailer/dist/cjs/addressparser/index.js +695 -267
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +4 -1
- package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +8 -2
- package/node_modules/nodemailer/dist/cjs/mime-node/index.js +48 -16
- 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/shared/url.js +22 -5
- package/node_modules/nodemailer/dist/esm/addressparser/index.js +695 -267
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +4 -1
- package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +8 -2
- package/node_modules/nodemailer/dist/esm/mime-node/index.js +48 -16
- 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/shared/url.js +22 -5
- package/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/libbase64/.eslintrc +0 -11
- package/node_modules/libbase64/.github/FUNDING.yml +0 -4
- package/node_modules/libbase64/.github/VULNERABILITY_REPORT.yml +0 -75
- package/node_modules/libbase64/.github/workflows/release.yaml +0 -39
- package/node_modules/libbase64/.github/workflows/test.yml +0 -21
- package/node_modules/libbase64/.ncurc.js +0 -13
- package/node_modules/libbase64/.prettierrc.js +0 -8
- package/node_modules/libmime/lib/charset.js +0 -131
- package/node_modules/libmime/lib/mimetypes.js +0 -2049
- package/node_modules/libqp/.eslintrc +0 -15
- package/node_modules/libqp/.github/FUNDING.yml +0 -4
- package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +0 -75
- package/node_modules/libqp/.github/workflows/release.yaml +0 -39
- package/node_modules/libqp/.github/workflows/test.yml +0 -21
- package/node_modules/libqp/.prettierrc.js +0 -8
- package/node_modules/libqp/test/libqp-test.js +0 -366
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
/* eslint no-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const libqp = require('libqp');
|
|
8
|
-
const mimetypes = require('./mimetypes');
|
|
9
|
-
|
|
1
|
+
/* eslint no-div-regex: 0 */
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import libbase64 from 'libbase64';
|
|
4
|
+
import libqp from 'libqp';
|
|
5
|
+
import * as libcharset from './charset.js';
|
|
6
|
+
import * as mimetypes from './mimetypes.js';
|
|
10
7
|
const STAGE_KEY = 0x1001;
|
|
11
8
|
const STAGE_VALUE = 0x1002;
|
|
12
|
-
|
|
13
9
|
// Sets an own property on a plain object. A regular assignment with a
|
|
14
10
|
// '__proto__' key would silently modify the object's prototype instead of
|
|
15
11
|
// creating an own property, so it needs special handling.
|
|
@@ -21,43 +17,46 @@ const setOwnProperty = (obj, key, value) => {
|
|
|
21
17
|
enumerable: true,
|
|
22
18
|
configurable: true
|
|
23
19
|
});
|
|
24
|
-
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
25
22
|
obj[key] = value;
|
|
26
23
|
}
|
|
27
24
|
};
|
|
28
|
-
|
|
29
|
-
// Checks own properties only, as attacker controlled keys like '__proto__'
|
|
30
|
-
// or 'toString' would otherwise resolve to inherited Object.prototype members.
|
|
31
|
-
// NB! Not named hasOwnProperty, that would shadow the global builtin
|
|
32
|
-
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
33
|
-
|
|
34
25
|
// Whitespace that separates the tokens of a header value. Kept as a char comparison,
|
|
35
26
|
// a regex test would be run for every single char of every parsed header
|
|
36
|
-
const isWSP = chr => chr === ' ' || chr === '\t' || chr === '\r' || chr === '\n' || chr === '\f' || chr === '\v';
|
|
37
|
-
|
|
38
|
-
class Libmime {
|
|
27
|
+
const isWSP = (chr) => chr === ' ' || chr === '\t' || chr === '\r' || chr === '\n' || chr === '\f' || chr === '\v';
|
|
28
|
+
export class Libmime {
|
|
39
29
|
constructor(config) {
|
|
40
30
|
this.config = config || {};
|
|
41
31
|
}
|
|
42
|
-
|
|
43
32
|
/**
|
|
44
33
|
* Checks if a value is plaintext string (uses only printable 7bit chars)
|
|
45
34
|
*
|
|
35
|
+
* When isParam is set the value is destined for a header parameter, so HT, CR and LF
|
|
36
|
+
* are not plaintext either: a header parameter has no way to carry them. HT is a valid
|
|
37
|
+
* fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
|
|
38
|
+
* cannot appear in a header value at all. DEL is neither a token character nor qtext,
|
|
39
|
+
* so it can not be carried bare or quoted. Such values have to go through the rfc2231
|
|
40
|
+
* parameter continuation encoding instead, the same way non-ASCII text does.
|
|
41
|
+
*
|
|
46
42
|
* @param {String} value String to be tested
|
|
43
|
+
* @param {Boolean} [isParam] Set to true if the value is a header parameter value
|
|
47
44
|
* @returns {Boolean} true if it is a plaintext string
|
|
48
45
|
*/
|
|
49
|
-
isPlainText(value) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
isPlainText(value, isParam) {
|
|
47
|
+
let re = isParam ? /[\x00-\x1f\x7f\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
|
48
|
+
return typeof value === 'string' && !re.test(value);
|
|
49
|
+
}
|
|
50
|
+
// A parameter value goes out as is only when it is plain text that does not look like an encoded
|
|
51
|
+
// word. Receivers decode encoded words found in parameter values (and libmime itself does so for
|
|
52
|
+
// unencoded continuations), so as plain text it would come back as something else
|
|
53
|
+
isPlainParamValue(value) {
|
|
54
|
+
return this.isPlainText(value, true) && !value.includes('=?');
|
|
55
55
|
}
|
|
56
|
-
|
|
57
56
|
/**
|
|
58
57
|
* Checks if a multi line string containes lines longer than the selected value.
|
|
59
58
|
*
|
|
60
|
-
* Useful when detecting if a mail message needs any processing at all
|
|
59
|
+
* Useful when detecting if a mail message needs any processing at all:
|
|
61
60
|
* if only plaintext characters are used and lines are short, then there is
|
|
62
61
|
* no need to encode the values in any way. If the value is plaintext but has
|
|
63
62
|
* longer lines then allowed, then use format=flowed
|
|
@@ -69,7 +68,6 @@ class Libmime {
|
|
|
69
68
|
hasLongerLines(str, lineLength) {
|
|
70
69
|
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
|
|
71
70
|
}
|
|
72
|
-
|
|
73
71
|
/**
|
|
74
72
|
* Decodes a string from a format=flowed soft wrapping.
|
|
75
73
|
*
|
|
@@ -77,38 +75,29 @@ class Libmime {
|
|
|
77
75
|
* @param {Boolean} [delSp] If true, delete leading spaces (delsp=yes)
|
|
78
76
|
* @return {String} Mime decoded string
|
|
79
77
|
*/
|
|
80
|
-
decodeFlowed(
|
|
81
|
-
str = (
|
|
82
|
-
|
|
78
|
+
decodeFlowed(input, delSp) {
|
|
79
|
+
let str = (input || '').toString();
|
|
83
80
|
let lines = str.split(/\r?\n/);
|
|
84
|
-
|
|
85
81
|
let result = [];
|
|
86
|
-
|
|
87
82
|
// The current paragraph is kept as a list of non-empty pieces and joined once when it
|
|
88
83
|
// ends. Appending to one string and testing it with a regex on every line re-scans (and
|
|
89
84
|
// flattens) the whole paragraph each time, which is quadratic on long soft-wrapped bodies.
|
|
90
|
-
let parts =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let pushPart = part => {
|
|
85
|
+
let parts = [], partsLength = 0;
|
|
86
|
+
let pushPart = (part) => {
|
|
94
87
|
if (part) {
|
|
95
88
|
parts.push(part);
|
|
96
89
|
partsLength += part.length;
|
|
97
90
|
}
|
|
98
91
|
};
|
|
99
|
-
|
|
100
92
|
// remove soft linebreaks
|
|
101
93
|
// soft linebreaks are added after space symbols
|
|
102
94
|
for (let i = 0; i < lines.length; i++) {
|
|
103
95
|
let line = lines[i];
|
|
104
|
-
|
|
105
96
|
// the last piece is never empty, so it holds the last character of the paragraph
|
|
106
|
-
let last = parts
|
|
107
|
-
|
|
97
|
+
let last = parts.length ? parts[parts.length - 1] : '';
|
|
108
98
|
// Lines never contain a line break, so a signature separator can only be the whole
|
|
109
99
|
// paragraph being exactly "-- "
|
|
110
100
|
let isSoftBreak = last.endsWith(' ') && !(partsLength === 3 && parts.join('') === '-- ');
|
|
111
|
-
|
|
112
101
|
if (isSoftBreak) {
|
|
113
102
|
if (delSp) {
|
|
114
103
|
// delsp adds space to text to be able to fold it
|
|
@@ -116,28 +105,31 @@ class Libmime {
|
|
|
116
105
|
parts.pop();
|
|
117
106
|
partsLength -= last.length;
|
|
118
107
|
pushPart(last.slice(0, -1));
|
|
108
|
+
pushPart(line);
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
else {
|
|
111
|
+
// remove space stuffing of the continuation line, the stuffing of a paragraph's first
|
|
112
|
+
// line is removed below. Without delsp the text is only wrapped after spaces, so a
|
|
113
|
+
// leading space can only be stuffing (http://tools.ietf.org/html/rfc3676#section-4.4)
|
|
114
|
+
pushPart(line.replace(/^ /, ''));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (i > 0) {
|
|
123
119
|
result.push(parts.join(''));
|
|
124
120
|
}
|
|
125
|
-
|
|
126
121
|
parts = [];
|
|
127
122
|
partsLength = 0;
|
|
128
123
|
pushPart(line);
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
|
-
|
|
132
|
-
if (parts !== null && partsLength) {
|
|
126
|
+
if (partsLength) {
|
|
133
127
|
result.push(parts.join(''));
|
|
134
128
|
}
|
|
135
|
-
|
|
136
129
|
// remove whitespace stuffing
|
|
137
130
|
// http://tools.ietf.org/html/rfc3676#section-4.4
|
|
138
131
|
return result.join('\n').replace(/^ /gm, '');
|
|
139
132
|
}
|
|
140
|
-
|
|
141
133
|
/**
|
|
142
134
|
* Adds soft line breaks to content marked with format=flowed to
|
|
143
135
|
* ensure that no line in the message is never longer than lineLength
|
|
@@ -148,22 +140,26 @@ class Libmime {
|
|
|
148
140
|
*/
|
|
149
141
|
encodeFlowed(str, lineLength) {
|
|
150
142
|
lineLength = lineLength || 76;
|
|
151
|
-
|
|
143
|
+
// space stuffing http://tools.ietf.org/html/rfc3676#section-4.2
|
|
144
|
+
let stuff = (line) => line.replace(/^( |From|>)/i, ' $1');
|
|
152
145
|
let flowed = [];
|
|
153
146
|
str.split(/\r?\n/).forEach(line => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
// A space in front of a hard line break would turn it into a soft one and the decoder would
|
|
148
|
+
// join the next line to this one, so trailing spaces are trimmed (rfc3676 section 4.2). The
|
|
149
|
+
// signature separator "-- " is the one line that keeps its trailing space
|
|
150
|
+
if (line !== '-- ') {
|
|
151
|
+
line = line.replace(/ +$/, '');
|
|
152
|
+
}
|
|
153
|
+
let folded = this.foldLines(stuff(line), lineLength, true).split('\r\n');
|
|
154
|
+
// the lines that folding starts need stuffing as much as the first one does,
|
|
155
|
+
// a continuation line that starts with ">" would be read as a quote
|
|
156
|
+
for (let i = 1; i < folded.length; i++) {
|
|
157
|
+
folded[i] = stuff(folded[i]);
|
|
158
|
+
}
|
|
159
|
+
flowed.push(folded.join('\r\n'));
|
|
163
160
|
});
|
|
164
161
|
return flowed.join('\r\n');
|
|
165
162
|
}
|
|
166
|
-
|
|
167
163
|
/**
|
|
168
164
|
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
|
|
169
165
|
*
|
|
@@ -175,49 +171,47 @@ class Libmime {
|
|
|
175
171
|
encodeWord(data, mimeWordEncoding, maxLength) {
|
|
176
172
|
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
|
|
177
173
|
maxLength = maxLength || 0;
|
|
178
|
-
|
|
179
|
-
let encodedStr;
|
|
174
|
+
let encodedStr = '';
|
|
180
175
|
let toCharset = 'UTF-8';
|
|
181
|
-
|
|
182
176
|
if (maxLength && maxLength > 7 + toCharset.length) {
|
|
183
177
|
maxLength -= 7 + toCharset.length;
|
|
184
178
|
}
|
|
185
|
-
|
|
186
179
|
if (mimeWordEncoding === 'Q') {
|
|
187
180
|
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
|
|
188
181
|
encodedStr = libqp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
|
|
189
182
|
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
|
|
190
183
|
if (chr === ' ') {
|
|
191
184
|
return '_';
|
|
192
|
-
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
193
187
|
return '=' + (ord.length === 1 ? '0' + ord : ord);
|
|
194
188
|
}
|
|
195
189
|
});
|
|
196
|
-
}
|
|
197
|
-
|
|
190
|
+
}
|
|
191
|
+
else if (mimeWordEncoding === 'B') {
|
|
192
|
+
// the chunking loop below splits raw text and base64 encodes each part, so a
|
|
193
|
+
// Buffer goes in as its UTF-8 string: handing it the base64 of the whole input
|
|
194
|
+
// would encode the encoding itself and decode back to base64 text
|
|
195
|
+
encodedStr = typeof data === 'string' ? data : data.toString('utf-8');
|
|
198
196
|
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
|
199
197
|
}
|
|
200
|
-
|
|
201
198
|
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : libbase64.encode(data)).length > maxLength) {
|
|
202
199
|
if (mimeWordEncoding === 'Q') {
|
|
203
200
|
encodedStr = this.splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
|
204
|
-
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
205
203
|
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
|
|
206
204
|
let parts = [];
|
|
207
205
|
let lpart = '';
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
// iterating a string yields whole code points, a surrogate pair stays together and an
|
|
207
|
+
// unpaired surrogate comes on its own without swallowing the unit that follows it
|
|
208
|
+
for (let chr of encodedStr) {
|
|
210
209
|
// check if we can add this character to the existing string
|
|
211
210
|
// without breaking byte length limit
|
|
212
|
-
|
|
213
|
-
if (/[\ud83c\ud83d\ud83e]/.test(chr) && i < len - 1) {
|
|
214
|
-
// composite emoji byte, so add the next byte as well
|
|
215
|
-
chr += encodedStr.charAt(++i);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
|
|
211
|
+
if (Buffer.byteLength(lpart + chr) <= maxLength || !lpart) {
|
|
219
212
|
lpart += chr;
|
|
220
|
-
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
221
215
|
// we hit the length limit, so push the existing string and start over
|
|
222
216
|
parts.push(libbase64.encode(lpart));
|
|
223
217
|
lpart = chr;
|
|
@@ -226,20 +220,19 @@ class Libmime {
|
|
|
226
220
|
if (lpart) {
|
|
227
221
|
parts.push(libbase64.encode(lpart));
|
|
228
222
|
}
|
|
229
|
-
|
|
230
223
|
if (parts.length > 1) {
|
|
231
224
|
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
|
232
|
-
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
233
227
|
encodedStr = parts.join('');
|
|
234
228
|
}
|
|
235
229
|
}
|
|
236
|
-
}
|
|
230
|
+
}
|
|
231
|
+
else if (mimeWordEncoding === 'B') {
|
|
237
232
|
encodedStr = libbase64.encode(data);
|
|
238
233
|
}
|
|
239
|
-
|
|
240
|
-
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
|
|
234
|
+
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.slice(-2) === '?=' ? '' : '?=');
|
|
241
235
|
}
|
|
242
|
-
|
|
243
236
|
/**
|
|
244
237
|
* Normalizes a charset name to its canonical form, resolving common aliases and
|
|
245
238
|
* invalid names. For example `iso-8859-8-i` and `iso-8859-8-e` become `ISO-8859-8`
|
|
@@ -252,7 +245,6 @@ class Libmime {
|
|
|
252
245
|
normalizeCharset(charset) {
|
|
253
246
|
return libcharset.normalizeCharset(charset);
|
|
254
247
|
}
|
|
255
|
-
|
|
256
248
|
/**
|
|
257
249
|
* Decodes the inner payload of a single mime encoded-word into a unicode string.
|
|
258
250
|
* Expects the three components of `=?charset?encoding?text?=` already separated.
|
|
@@ -268,19 +260,17 @@ class Libmime {
|
|
|
268
260
|
// this implementation silently ignores this tag
|
|
269
261
|
let splitPos = charset.indexOf('*');
|
|
270
262
|
if (splitPos >= 0) {
|
|
271
|
-
charset = charset.
|
|
263
|
+
charset = charset.substring(0, splitPos);
|
|
272
264
|
}
|
|
273
265
|
charset = libcharset.normalizeCharset(charset);
|
|
274
|
-
|
|
275
266
|
encoding = encoding.toUpperCase();
|
|
276
|
-
|
|
267
|
+
let decoded;
|
|
277
268
|
if (encoding === 'Q') {
|
|
278
269
|
str = str
|
|
279
270
|
// remove spaces between = and hex char, this might indicate invalidly applied line splitting
|
|
280
271
|
.replace(/=\s+([0-9a-fA-F])/g, '=$1')
|
|
281
272
|
// convert all underscores to spaces
|
|
282
273
|
.replace(/[_\s]/g, ' ');
|
|
283
|
-
|
|
284
274
|
let buf = Buffer.from(str);
|
|
285
275
|
let bytes = [];
|
|
286
276
|
for (let i = 0, len = buf.length; i < len; i++) {
|
|
@@ -297,18 +287,18 @@ class Libmime {
|
|
|
297
287
|
}
|
|
298
288
|
bytes.push(c);
|
|
299
289
|
}
|
|
300
|
-
|
|
301
|
-
}
|
|
290
|
+
decoded = Buffer.from(bytes);
|
|
291
|
+
}
|
|
292
|
+
else if (encoding === 'B') {
|
|
302
293
|
// libbase64 decodes input made of several padded segments segment by segment
|
|
303
|
-
|
|
304
|
-
}
|
|
294
|
+
decoded = libbase64.decode(str);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
305
297
|
// keep as is, convert Buffer to unicode string, assume utf8
|
|
306
|
-
|
|
298
|
+
decoded = Buffer.from(str);
|
|
307
299
|
}
|
|
308
|
-
|
|
309
|
-
return libcharset.decode(str, charset);
|
|
300
|
+
return libcharset.decode(decoded, charset);
|
|
310
301
|
}
|
|
311
|
-
|
|
312
302
|
/**
|
|
313
303
|
* Finds word sequences with non ascii text and converts these to mime words
|
|
314
304
|
*
|
|
@@ -323,24 +313,17 @@ class Libmime {
|
|
|
323
313
|
fromCharset = maxLength;
|
|
324
314
|
maxLength = undefined;
|
|
325
315
|
}
|
|
326
|
-
|
|
327
|
-
maxLength = maxLength || 0;
|
|
328
|
-
|
|
316
|
+
let maxLineLength = Number(maxLength) || 0;
|
|
329
317
|
let decodedValue = libcharset.decode(libcharset.convert(data || '', fromCharset));
|
|
330
318
|
let encodedValue;
|
|
331
|
-
|
|
332
319
|
let firstMatch = decodedValue.match(/(?:^|\s)([^\s]*[\u0080-\uFFFF])/);
|
|
333
320
|
if (!firstMatch) {
|
|
334
321
|
return decodedValue;
|
|
335
322
|
}
|
|
336
|
-
let startIndex =
|
|
337
|
-
firstMatch.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
index: 0
|
|
341
|
-
}
|
|
342
|
-
).index;
|
|
343
|
-
|
|
323
|
+
let startIndex = firstMatch.index +
|
|
324
|
+
(firstMatch[0].match(/[^\s]/) || {
|
|
325
|
+
index: 0
|
|
326
|
+
}).index;
|
|
344
327
|
// The encoded part ends with the word that holds the last non ascii char. Found with a scan,
|
|
345
328
|
// the regex that used to do this rescanned the rest of the word from every non ascii char in
|
|
346
329
|
// it, which made a long non ascii word quadratic
|
|
@@ -350,15 +333,12 @@ class Libmime {
|
|
|
350
333
|
}
|
|
351
334
|
let wordEnd = decodedValue.slice(lastIndex + 1).search(/\s/);
|
|
352
335
|
let endIndex = wordEnd < 0 ? decodedValue.length : lastIndex + 1 + wordEnd;
|
|
353
|
-
|
|
354
336
|
encodedValue =
|
|
355
|
-
(startIndex ? decodedValue.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
337
|
+
(startIndex ? decodedValue.substring(0, startIndex) : '') +
|
|
338
|
+
this.encodeWord(decodedValue.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLineLength) +
|
|
339
|
+
(endIndex < decodedValue.length ? decodedValue.slice(endIndex) : '');
|
|
359
340
|
return encodedValue;
|
|
360
341
|
}
|
|
361
|
-
|
|
362
342
|
/**
|
|
363
343
|
* Decode a string that might include one or several mime words
|
|
364
344
|
*
|
|
@@ -366,43 +346,40 @@ class Libmime {
|
|
|
366
346
|
* @return {String} Decoded unicode string
|
|
367
347
|
*/
|
|
368
348
|
decodeWords(str) {
|
|
369
|
-
return (
|
|
370
|
-
(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (m, charset, encoding, text) => this.decodeWord(charset, encoding, text))
|
|
396
|
-
);
|
|
349
|
+
return ((str || '')
|
|
350
|
+
.toString()
|
|
351
|
+
// find base64 words that can be joined
|
|
352
|
+
.replace(/(=\?([^?]+)\?[Bb]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
|
|
353
|
+
// only mark b64 chunks to be joined if charsets match
|
|
354
|
+
if (libcharset.normalizeCharset(chLeft || '') === libcharset.normalizeCharset(chRight || '')) {
|
|
355
|
+
// set a joiner marker
|
|
356
|
+
return left + '__\x00JOIN\x00__';
|
|
357
|
+
}
|
|
358
|
+
return match;
|
|
359
|
+
})
|
|
360
|
+
// find QP words that can be joined
|
|
361
|
+
.replace(/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
|
|
362
|
+
// only mark QP chunks to be joined if charsets match
|
|
363
|
+
if (libcharset.normalizeCharset(chLeft || '') === libcharset.normalizeCharset(chRight || '')) {
|
|
364
|
+
// set a joiner marker
|
|
365
|
+
return left + '__\x00JOIN\x00__';
|
|
366
|
+
}
|
|
367
|
+
return match;
|
|
368
|
+
})
|
|
369
|
+
// join base64 encoded words
|
|
370
|
+
.replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, '')
|
|
371
|
+
// remove spaces between mime encoded words
|
|
372
|
+
.replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, '$1')
|
|
373
|
+
// decode words
|
|
374
|
+
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (_m, charset, encoding, text) => this.decodeWord(charset, encoding, text)));
|
|
397
375
|
}
|
|
398
|
-
|
|
399
376
|
getHex(c) {
|
|
400
|
-
if (
|
|
377
|
+
if (c !== undefined &&
|
|
378
|
+
((c >= 0x30 /* 0 */ && c <= 0x39) /* 9 */ || (c >= 0x61 /* a */ && c <= 0x66) /* f */ || (c >= 0x41 /* A */ && c <= 0x46)) /* F */) {
|
|
401
379
|
return String.fromCharCode(c);
|
|
402
380
|
}
|
|
403
381
|
return false;
|
|
404
382
|
}
|
|
405
|
-
|
|
406
383
|
/**
|
|
407
384
|
* Splits a string by :
|
|
408
385
|
* The result is not mime word decoded, you need to do your own decoding based
|
|
@@ -413,19 +390,14 @@ class Libmime {
|
|
|
413
390
|
*/
|
|
414
391
|
decodeHeader(headerLine) {
|
|
415
392
|
let line = (headerLine || '')
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
match = line.match(/^\s*([^:]+):(.*)$/),
|
|
420
|
-
key = ((match && match[1]) || '').trim().toLowerCase(),
|
|
421
|
-
value = ((match && match[2]) || '').trim();
|
|
422
|
-
|
|
393
|
+
.toString()
|
|
394
|
+
.replace(/(?:\r?\n|\r)[ \t]*/g, ' ')
|
|
395
|
+
.trim(), match = line.match(/^\s*([^:]+):(.*)$/), key = ((match && match[1]) || '').trim().toLowerCase(), value = ((match && match[2]) || '').trim();
|
|
423
396
|
return {
|
|
424
397
|
key,
|
|
425
398
|
value
|
|
426
399
|
};
|
|
427
400
|
}
|
|
428
|
-
|
|
429
401
|
/**
|
|
430
402
|
* Parses a block of header lines. Does not decode mime words as every
|
|
431
403
|
* header might have its own rules (eg. formatted email addresses and such)
|
|
@@ -434,48 +406,42 @@ class Libmime {
|
|
|
434
406
|
* @return {Object} An object of headers, where header keys are object keys and every value is an Array of the values for that key
|
|
435
407
|
*/
|
|
436
408
|
decodeHeaders(headers) {
|
|
437
|
-
let lines = headers.split(/\r?\n|\r/),
|
|
438
|
-
headersObj = {},
|
|
439
|
-
header,
|
|
440
|
-
i,
|
|
441
|
-
len;
|
|
442
|
-
|
|
409
|
+
let lines = headers.split(/\r?\n|\r/), headersObj = {}, header, i, len;
|
|
443
410
|
// Empty lines in front of the block are not part of any header, a caller might
|
|
444
411
|
// include the line break that separates a mime part boundary from its headers
|
|
445
412
|
let headersPos = 0;
|
|
446
413
|
while (headersPos < lines.length && lines[headersPos] === '') {
|
|
447
414
|
headersPos++;
|
|
448
415
|
}
|
|
449
|
-
|
|
450
416
|
// An empty line terminates the header block, anything after it is the message body.
|
|
451
417
|
// Without this a body line that starts with whitespace would be folded into the last header
|
|
452
418
|
let bodyPos = lines.indexOf('', headersPos);
|
|
453
419
|
lines = lines.slice(headersPos, bodyPos >= 0 ? bodyPos : lines.length);
|
|
454
|
-
|
|
455
420
|
// unfold folded lines, a continuation line always starts with a space or a tab
|
|
456
421
|
// Built in one forward pass, removing continuation lines with splice() is quadratic
|
|
457
422
|
let unfolded = [];
|
|
458
423
|
for (i = 0, len = lines.length; i < len; i++) {
|
|
459
424
|
if (unfolded.length && /^[ \t]/.test(lines[i])) {
|
|
460
425
|
unfolded[unfolded.length - 1] += '\r\n' + lines[i];
|
|
461
|
-
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
462
428
|
unfolded.push(lines[i]);
|
|
463
429
|
}
|
|
464
430
|
}
|
|
465
431
|
lines = unfolded;
|
|
466
|
-
|
|
467
432
|
for (i = 0, len = lines.length; i < len; i++) {
|
|
468
433
|
header = this.decodeHeader(lines[i]);
|
|
469
|
-
|
|
434
|
+
// own properties only, an attacker controlled key such as '__proto__' or 'toString' would
|
|
435
|
+
// otherwise resolve to an inherited Object.prototype member
|
|
436
|
+
if (!Object.hasOwn(headersObj, header.key)) {
|
|
470
437
|
setOwnProperty(headersObj, header.key, [header.value]);
|
|
471
|
-
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
472
440
|
headersObj[header.key].push(header.value);
|
|
473
441
|
}
|
|
474
442
|
}
|
|
475
|
-
|
|
476
443
|
return headersObj;
|
|
477
444
|
}
|
|
478
|
-
|
|
479
445
|
/**
|
|
480
446
|
* Joins parsed header value together as 'value; param1=value1; param2=value2'
|
|
481
447
|
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
|
|
@@ -485,28 +451,37 @@ class Libmime {
|
|
|
485
451
|
*/
|
|
486
452
|
buildHeaderValue(structured) {
|
|
487
453
|
let paramsArray = [];
|
|
488
|
-
|
|
489
|
-
Object.keys(
|
|
454
|
+
let params = structured.params || {};
|
|
455
|
+
Object.keys(params).forEach(key => {
|
|
490
456
|
// filename might include unicode characters so it is a special case
|
|
491
|
-
let value =
|
|
492
|
-
|
|
457
|
+
let value = params[key];
|
|
458
|
+
// a parameter name is a token too and it is emitted without any quoting around it
|
|
459
|
+
let param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
|
460
|
+
if (!this.isPlainParamValue(value) || value.length >= 75) {
|
|
493
461
|
this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
|
494
|
-
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.
|
|
462
|
+
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.endsWith('*')) {
|
|
495
463
|
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
|
496
|
-
}
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
497
466
|
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
|
|
498
467
|
}
|
|
499
468
|
});
|
|
500
|
-
}
|
|
469
|
+
}
|
|
470
|
+
else if (!value.length || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
|
|
471
|
+
// a parameter value is a token or a quoted-string and a token is never empty, so
|
|
472
|
+
// a valueless parameter such as the 'flag' of 'multipart/mixed; flag; boundary=b'
|
|
473
|
+
// goes out as 'flag=""' rather than as the 'flag=' that parses as neither
|
|
501
474
|
paramsArray.push(param + '=' + JSON.stringify(value));
|
|
502
|
-
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
503
477
|
paramsArray.push(param + '=' + value);
|
|
504
478
|
}
|
|
505
479
|
});
|
|
506
|
-
|
|
507
|
-
|
|
480
|
+
// the value ahead of the parameters is a token, it has no way to carry a control
|
|
481
|
+
// char or DEL and there is no quoting construct around it to escape one into
|
|
482
|
+
let value = typeof structured.value === 'string' ? structured.value.replace(/[\x00-\x1f\x7f]/g, '') : structured.value;
|
|
483
|
+
return value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
|
|
508
484
|
}
|
|
509
|
-
|
|
510
485
|
/**
|
|
511
486
|
* Parses a header value with key=value arguments into a structured
|
|
512
487
|
* object.
|
|
@@ -528,10 +503,9 @@ class Libmime {
|
|
|
528
503
|
*/
|
|
529
504
|
parseHeaderValue(str) {
|
|
530
505
|
let response = {
|
|
531
|
-
value:
|
|
506
|
+
value: '',
|
|
532
507
|
params: {}
|
|
533
508
|
};
|
|
534
|
-
|
|
535
509
|
// names of the params assembled from rfc2231 segments, see the continuation handling below.
|
|
536
510
|
// Non-enumerable, so the {value, params} shape callers serialize and compare stays as it is
|
|
537
511
|
Object.defineProperty(response, 'decodedParams', {
|
|
@@ -540,40 +514,37 @@ class Libmime {
|
|
|
540
514
|
enumerable: false,
|
|
541
515
|
configurable: true
|
|
542
516
|
});
|
|
543
|
-
|
|
544
517
|
let key = false;
|
|
545
518
|
let value = '';
|
|
546
519
|
// Length of the significant part of `value`. Whitespace outside of quotes is
|
|
547
520
|
// not part of the value, while whitespace inside quotes is (rfc2045 section 5.1)
|
|
548
521
|
let valueEnd = 0;
|
|
549
522
|
let stage = STAGE_VALUE;
|
|
550
|
-
|
|
551
523
|
let quote = false;
|
|
552
524
|
let escaped = false;
|
|
553
525
|
let chr;
|
|
554
|
-
|
|
555
526
|
// Stores the collected value, either as the default value, as the value for the
|
|
556
527
|
// current key, or as a key that has no value at all. Runs once per parameter
|
|
557
528
|
let commit = () => {
|
|
558
529
|
let collected = value.substring(0, valueEnd);
|
|
559
530
|
value = '';
|
|
560
531
|
valueEnd = 0;
|
|
561
|
-
|
|
562
532
|
if (stage === STAGE_KEY) {
|
|
563
533
|
// key without a value, see emptykey:
|
|
564
534
|
// Header-Key: somevalue; key=value; emptykey
|
|
565
535
|
if (collected) {
|
|
566
536
|
setOwnProperty(response.params, collected.toLowerCase(), '');
|
|
567
537
|
}
|
|
568
|
-
}
|
|
538
|
+
}
|
|
539
|
+
else if (key === false) {
|
|
569
540
|
// default value
|
|
570
541
|
response.value = collected;
|
|
571
|
-
}
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
572
544
|
// subkey value
|
|
573
545
|
setOwnProperty(response.params, key, collected);
|
|
574
546
|
}
|
|
575
547
|
};
|
|
576
|
-
|
|
577
548
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
578
549
|
chr = str.charAt(i);
|
|
579
550
|
switch (stage) {
|
|
@@ -583,14 +554,17 @@ class Libmime {
|
|
|
583
554
|
value = '';
|
|
584
555
|
valueEnd = 0;
|
|
585
556
|
stage = STAGE_VALUE;
|
|
586
|
-
}
|
|
557
|
+
}
|
|
558
|
+
else if (chr === ';') {
|
|
587
559
|
// key without a value, eg. the "inline" in "attachment; inline; filename=a.txt"
|
|
588
560
|
commit();
|
|
589
|
-
}
|
|
561
|
+
}
|
|
562
|
+
else if (isWSP(chr)) {
|
|
590
563
|
if (value.length) {
|
|
591
564
|
value += chr;
|
|
592
565
|
}
|
|
593
|
-
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
594
568
|
value += chr;
|
|
595
569
|
valueEnd = value.length;
|
|
596
570
|
}
|
|
@@ -599,20 +573,25 @@ class Libmime {
|
|
|
599
573
|
if (escaped) {
|
|
600
574
|
value += chr;
|
|
601
575
|
valueEnd = value.length;
|
|
602
|
-
}
|
|
576
|
+
}
|
|
577
|
+
else if (chr === '\\') {
|
|
603
578
|
escaped = true;
|
|
604
579
|
continue;
|
|
605
|
-
}
|
|
580
|
+
}
|
|
581
|
+
else if (chr === '"') {
|
|
606
582
|
// every char between the quotes is significant, including whitespace
|
|
607
583
|
quote = !quote;
|
|
608
|
-
}
|
|
584
|
+
}
|
|
585
|
+
else if (!quote && chr === ';') {
|
|
609
586
|
commit();
|
|
610
587
|
stage = STAGE_KEY;
|
|
611
|
-
}
|
|
588
|
+
}
|
|
589
|
+
else if (!quote && isWSP(chr)) {
|
|
612
590
|
if (value.length) {
|
|
613
591
|
value += chr;
|
|
614
592
|
}
|
|
615
|
-
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
616
595
|
value += chr;
|
|
617
596
|
valueEnd = value.length;
|
|
618
597
|
}
|
|
@@ -620,32 +599,24 @@ class Libmime {
|
|
|
620
599
|
break;
|
|
621
600
|
}
|
|
622
601
|
}
|
|
623
|
-
|
|
624
602
|
// finalize remainder
|
|
625
603
|
commit();
|
|
626
|
-
|
|
627
604
|
// handle parameter value continuations
|
|
628
605
|
// https://tools.ietf.org/html/rfc2231#section-3
|
|
629
|
-
|
|
630
606
|
// Collect the segments of every continuation parameter. These are kept out of
|
|
631
607
|
// response.params so that a half assembled value can not leak into the result
|
|
632
608
|
let continuations = new Map();
|
|
633
|
-
|
|
634
609
|
for (let key of Object.keys(response.params)) {
|
|
635
610
|
let match = key.match(/\*((\d+)\*?)?$/);
|
|
636
|
-
|
|
637
611
|
if (!match) {
|
|
638
612
|
// nothing to do here, does not seem like a continuation param
|
|
639
613
|
continue;
|
|
640
614
|
}
|
|
641
|
-
|
|
642
|
-
let actualKey = key.substr(0, match.index).toLowerCase();
|
|
615
|
+
let actualKey = key.substring(0, match.index).toLowerCase();
|
|
643
616
|
let nr = Number(match[2]) || 0;
|
|
644
617
|
let value = response.params[key];
|
|
645
|
-
|
|
646
618
|
// remove the old reference
|
|
647
619
|
delete response.params[key];
|
|
648
|
-
|
|
649
620
|
let continuation = continuations.get(actualKey);
|
|
650
621
|
if (!continuation) {
|
|
651
622
|
continuation = {
|
|
@@ -654,15 +625,12 @@ class Libmime {
|
|
|
654
625
|
};
|
|
655
626
|
continuations.set(actualKey, continuation);
|
|
656
627
|
}
|
|
657
|
-
|
|
658
628
|
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
659
629
|
continuation.charset = match[1] || 'utf-8';
|
|
660
630
|
value = match[2];
|
|
661
631
|
}
|
|
662
|
-
|
|
663
632
|
continuation.values.push({ nr, value });
|
|
664
633
|
}
|
|
665
|
-
|
|
666
634
|
// concatenate the split rfc2231 strings and decode the encoded ones
|
|
667
635
|
for (let [key, continuation] of continuations) {
|
|
668
636
|
// NB! An assembled continuation always overrides a plain parameter with the same
|
|
@@ -673,39 +641,33 @@ class Libmime {
|
|
|
673
641
|
.sort((a, b) => a.nr - b.nr)
|
|
674
642
|
.map(val => val.value)
|
|
675
643
|
.join('');
|
|
676
|
-
|
|
677
644
|
// Both branches below store a decoded value, so a caller that runs decodeWords() over the
|
|
678
645
|
// params has to leave this one out. A value that percent-encodes an encoded word would
|
|
679
646
|
// otherwise be decoded twice: filename*=utf-8''invoice%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D
|
|
680
647
|
// names the file "invoice=?utf-8?Q?=2Eexe?=", not "invoice.exe"
|
|
681
648
|
response.decodedParams.push(key);
|
|
682
|
-
|
|
683
649
|
if (!continuation.charset) {
|
|
684
650
|
setOwnProperty(response.params, key, this.decodeWords(value));
|
|
685
651
|
continue;
|
|
686
652
|
}
|
|
687
|
-
|
|
688
653
|
// convert "%AB" to the quoted printable "=AB" and decode it as the charset of the parameter.
|
|
689
654
|
// NB! The charset is passed in as an argument instead of building a "=?charset?Q?=AB?=" string,
|
|
690
655
|
// a charset that contains "?" would otherwise inject an encoded word of its own
|
|
691
656
|
let qpValue = value
|
|
692
657
|
// fix invalidly encoded chars
|
|
693
658
|
.replace(/[=_\s]/g, s => {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
659
|
+
if (s === ' ') {
|
|
660
|
+
return '_';
|
|
661
|
+
}
|
|
662
|
+
let c = s.charCodeAt(0).toString(16);
|
|
663
|
+
return '%' + (c.length < 2 ? '0' : '') + c;
|
|
664
|
+
})
|
|
700
665
|
// change from urlencoding to percent encoding
|
|
701
666
|
.replace(/%/g, '=');
|
|
702
|
-
|
|
703
667
|
setOwnProperty(response.params, key, this.decodeWord(continuation.charset, 'Q', qpValue));
|
|
704
668
|
}
|
|
705
|
-
|
|
706
669
|
return response;
|
|
707
670
|
}
|
|
708
|
-
|
|
709
671
|
/**
|
|
710
672
|
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
|
|
711
673
|
* Useful for splitting long parameter values.
|
|
@@ -724,24 +686,19 @@ class Libmime {
|
|
|
724
686
|
*/
|
|
725
687
|
buildHeaderParam(key, data, maxLength, fromCharset) {
|
|
726
688
|
let list = [];
|
|
727
|
-
|
|
728
689
|
if (typeof data !== 'string' && !Buffer.isBuffer(data)) {
|
|
729
690
|
// documented input is a string or a Buffer, normalize anything else
|
|
730
691
|
data = data === null || data === undefined ? '' : data.toString();
|
|
731
692
|
}
|
|
732
|
-
|
|
733
693
|
let encodedStr = typeof data === 'string' ? data : libcharset.decode(data, fromCharset);
|
|
734
|
-
let
|
|
735
|
-
let chr, ord;
|
|
694
|
+
let chr;
|
|
736
695
|
let line;
|
|
737
696
|
let startPos = 0;
|
|
738
697
|
let isEncoded = false;
|
|
739
698
|
let i, len;
|
|
740
|
-
|
|
741
699
|
maxLength = maxLength || 50;
|
|
742
|
-
|
|
743
700
|
// process ascii only text
|
|
744
|
-
if (this.
|
|
701
|
+
if (this.isPlainParamValue(data)) {
|
|
745
702
|
// check if conversion is even needed
|
|
746
703
|
if (encodedStr.length <= maxLength) {
|
|
747
704
|
return [
|
|
@@ -751,50 +708,36 @@ class Libmime {
|
|
|
751
708
|
}
|
|
752
709
|
];
|
|
753
710
|
}
|
|
754
|
-
|
|
755
711
|
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
|
|
756
712
|
list.push({
|
|
757
713
|
line: str
|
|
758
714
|
});
|
|
759
715
|
return '';
|
|
760
716
|
});
|
|
761
|
-
|
|
762
717
|
if (encodedStr) {
|
|
763
718
|
list.push({
|
|
764
719
|
line: encodedStr
|
|
765
720
|
});
|
|
766
721
|
}
|
|
767
|
-
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
768
724
|
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
|
|
769
|
-
// string
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
chr = encodedStr.charAt(i);
|
|
773
|
-
ord = chr.charCodeAt(0);
|
|
774
|
-
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
|
|
775
|
-
chr += encodedStr.charAt(i + 1);
|
|
776
|
-
encodedStrArr.push(chr);
|
|
777
|
-
i++;
|
|
778
|
-
} else {
|
|
779
|
-
encodedStrArr.push(chr);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
encodedStr = encodedStrArr;
|
|
725
|
+
// string contains surrogate pairs, so split it into code points. An unpaired
|
|
726
|
+
// surrogate comes on its own and does not tear apart a valid pair following it
|
|
727
|
+
encodedStr = Array.from(encodedStr);
|
|
783
728
|
}
|
|
784
|
-
|
|
785
729
|
// first line includes the charset and language info and needs to be encoded
|
|
786
730
|
// even if it does not contain any unicode characters
|
|
787
731
|
line = "utf-8''";
|
|
788
732
|
isEncoded = true;
|
|
789
733
|
startPos = 0;
|
|
790
|
-
|
|
791
734
|
// process text with unicode or special chars
|
|
792
735
|
for (i = 0, len = encodedStr.length; i < len; i++) {
|
|
793
736
|
chr = encodedStr[i];
|
|
794
|
-
|
|
795
737
|
if (isEncoded) {
|
|
796
738
|
chr = this.safeEncodeURIComponent(chr);
|
|
797
|
-
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
798
741
|
// try to urlencode current char
|
|
799
742
|
chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr);
|
|
800
743
|
// By default it is not required to encode a line, the need
|
|
@@ -809,9 +752,13 @@ class Libmime {
|
|
|
809
752
|
line,
|
|
810
753
|
encoded: isEncoded
|
|
811
754
|
});
|
|
755
|
+
// the line we start here holds an encoded char, so it has to be
|
|
756
|
+
// flagged as one. Otherwise it gets an unstarred continuation key
|
|
757
|
+
// and a receiver reads the percent escapes as literal text
|
|
812
758
|
line = '';
|
|
813
|
-
|
|
814
|
-
}
|
|
759
|
+
isEncoded = true;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
815
762
|
isEncoded = true;
|
|
816
763
|
i = startPos;
|
|
817
764
|
line = '';
|
|
@@ -819,7 +766,6 @@ class Libmime {
|
|
|
819
766
|
}
|
|
820
767
|
}
|
|
821
768
|
}
|
|
822
|
-
|
|
823
769
|
// if the line is already too long, push it to the list and start a new one
|
|
824
770
|
if ((line + chr).length >= maxLength) {
|
|
825
771
|
list.push({
|
|
@@ -830,14 +776,15 @@ class Libmime {
|
|
|
830
776
|
if (chr === encodedStr[i]) {
|
|
831
777
|
isEncoded = false;
|
|
832
778
|
startPos = i - 1;
|
|
833
|
-
}
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
834
781
|
isEncoded = true;
|
|
835
782
|
}
|
|
836
|
-
}
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
837
785
|
line += chr;
|
|
838
786
|
}
|
|
839
787
|
}
|
|
840
|
-
|
|
841
788
|
if (line) {
|
|
842
789
|
list.push({
|
|
843
790
|
line,
|
|
@@ -845,7 +792,6 @@ class Libmime {
|
|
|
845
792
|
});
|
|
846
793
|
}
|
|
847
794
|
}
|
|
848
|
-
|
|
849
795
|
return list.map((item, i) => ({
|
|
850
796
|
// encoded lines: {name}*{part}*
|
|
851
797
|
// unencoded lines: {name}*{part}
|
|
@@ -854,7 +800,6 @@ class Libmime {
|
|
|
854
800
|
value: item.line
|
|
855
801
|
}));
|
|
856
802
|
}
|
|
857
|
-
|
|
858
803
|
/**
|
|
859
804
|
* Returns file extension for a content type string. If no suitable extensions
|
|
860
805
|
* are found, 'bin' is used as the default extension
|
|
@@ -862,29 +807,24 @@ class Libmime {
|
|
|
862
807
|
* @param {String} mimeType Content type to be checked for
|
|
863
808
|
* @return {String} File extension
|
|
864
809
|
*/
|
|
865
|
-
detectExtension(
|
|
866
|
-
mimeType = (
|
|
867
|
-
if (!hasOwn(mimetypes.list, mimeType)) {
|
|
810
|
+
detectExtension(input) {
|
|
811
|
+
let mimeType = (input || '').toString().toLowerCase().replace(/\s/g, '');
|
|
812
|
+
if (!Object.hasOwn(mimetypes.list, mimeType)) {
|
|
868
813
|
return 'bin';
|
|
869
814
|
}
|
|
870
|
-
|
|
871
815
|
if (typeof mimetypes.list[mimeType] === 'string') {
|
|
872
816
|
return mimetypes.list[mimeType];
|
|
873
817
|
}
|
|
874
|
-
|
|
875
818
|
let mimeParts = mimeType.split('/');
|
|
876
|
-
|
|
877
819
|
// search for name match
|
|
878
820
|
for (let i = 0, len = mimetypes.list[mimeType].length; i < len; i++) {
|
|
879
821
|
if (mimeParts[1] === mimetypes.list[mimeType][i]) {
|
|
880
822
|
return mimetypes.list[mimeType][i];
|
|
881
823
|
}
|
|
882
824
|
}
|
|
883
|
-
|
|
884
825
|
// use the first one
|
|
885
826
|
return mimetypes.list[mimeType][0] !== '*' ? mimetypes.list[mimeType][0] : 'bin';
|
|
886
827
|
}
|
|
887
|
-
|
|
888
828
|
/**
|
|
889
829
|
* Returns content type for a file extension. If no suitable content types
|
|
890
830
|
* are found, 'application/octet-stream' is used as the default content type
|
|
@@ -892,19 +832,15 @@ class Libmime {
|
|
|
892
832
|
* @param {String} extension Extension (or filename) to be checked for
|
|
893
833
|
* @return {String} Content type
|
|
894
834
|
*/
|
|
895
|
-
detectMimeType(
|
|
896
|
-
extension = (
|
|
897
|
-
|
|
898
|
-
if (!hasOwn(mimetypes.extensions, extension)) {
|
|
835
|
+
detectMimeType(input) {
|
|
836
|
+
let extension = (input || '').toString().toLowerCase().replace(/\s/g, '').replace(/^\./g, '').split('.').pop() || '';
|
|
837
|
+
if (!Object.hasOwn(mimetypes.extensions, extension)) {
|
|
899
838
|
return 'application/octet-stream';
|
|
900
839
|
}
|
|
901
|
-
|
|
902
840
|
if (typeof mimetypes.extensions[extension] === 'string') {
|
|
903
841
|
return mimetypes.extensions[extension];
|
|
904
842
|
}
|
|
905
|
-
|
|
906
843
|
let mimeParts;
|
|
907
|
-
|
|
908
844
|
// search for name match
|
|
909
845
|
for (let i = 0, len = mimetypes.extensions[extension].length; i < len; i++) {
|
|
910
846
|
mimeParts = mimetypes.extensions[extension][i].split('/');
|
|
@@ -912,11 +848,9 @@ class Libmime {
|
|
|
912
848
|
return mimetypes.extensions[extension][i];
|
|
913
849
|
}
|
|
914
850
|
}
|
|
915
|
-
|
|
916
851
|
// use the first one
|
|
917
852
|
return mimetypes.extensions[extension][0];
|
|
918
853
|
}
|
|
919
|
-
|
|
920
854
|
/**
|
|
921
855
|
* Folds long lines, useful for folding header lines (afterSpace=false) and
|
|
922
856
|
* flowed text (afterSpace=true)
|
|
@@ -926,18 +860,12 @@ class Libmime {
|
|
|
926
860
|
* @param {Boolean} afterSpace If true, leave a space in the end of a line
|
|
927
861
|
* @return {String} String with folded lines
|
|
928
862
|
*/
|
|
929
|
-
foldLines(
|
|
930
|
-
str = (
|
|
863
|
+
foldLines(input, lineLength, afterSpace) {
|
|
864
|
+
let str = (input || '').toString();
|
|
931
865
|
lineLength = lineLength || 76;
|
|
932
|
-
|
|
933
|
-
let pos = 0,
|
|
934
|
-
len = str.length,
|
|
935
|
-
result = '',
|
|
936
|
-
line,
|
|
937
|
-
match;
|
|
938
|
-
|
|
866
|
+
let pos = 0, len = str.length, result = '', line, match;
|
|
939
867
|
while (pos < len) {
|
|
940
|
-
line = str.
|
|
868
|
+
line = str.substring(pos, pos + Math.max(lineLength, 0));
|
|
941
869
|
if (line.length < lineLength) {
|
|
942
870
|
result += line;
|
|
943
871
|
break;
|
|
@@ -947,22 +875,21 @@ class Libmime {
|
|
|
947
875
|
result += line;
|
|
948
876
|
pos += line.length;
|
|
949
877
|
continue;
|
|
950
|
-
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
|
|
951
|
-
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
|
|
952
|
-
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
|
953
|
-
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
|
|
954
878
|
}
|
|
955
|
-
|
|
879
|
+
else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
|
|
880
|
+
line = line.substring(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
|
|
881
|
+
}
|
|
882
|
+
else if ((match = str.slice(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
|
883
|
+
line = line + match[0].substring(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
|
|
884
|
+
}
|
|
956
885
|
result += line;
|
|
957
886
|
pos += line.length;
|
|
958
887
|
if (pos < len) {
|
|
959
888
|
result += '\r\n';
|
|
960
889
|
}
|
|
961
890
|
}
|
|
962
|
-
|
|
963
891
|
return result;
|
|
964
892
|
}
|
|
965
|
-
|
|
966
893
|
/**
|
|
967
894
|
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
|
|
968
895
|
*
|
|
@@ -971,74 +898,74 @@ class Libmime {
|
|
|
971
898
|
* @return {Array} Split string
|
|
972
899
|
*/
|
|
973
900
|
splitMimeEncodedString(str, maxlen) {
|
|
974
|
-
let curLine,
|
|
975
|
-
match,
|
|
976
|
-
chr,
|
|
977
|
-
done,
|
|
978
|
-
lines = [];
|
|
979
|
-
|
|
901
|
+
let curLine, fallbackLine, match, chr, done, lines = [];
|
|
980
902
|
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
|
|
981
903
|
maxlen = Math.max(maxlen || 0, 12);
|
|
982
|
-
|
|
983
904
|
while (str.length) {
|
|
984
|
-
curLine = str.
|
|
985
|
-
|
|
905
|
+
curLine = str.substring(0, maxlen);
|
|
986
906
|
// move incomplete escaped char back to main
|
|
987
907
|
if ((match = curLine.match(/[=][0-9A-F]?$/i))) {
|
|
988
|
-
curLine = curLine.
|
|
908
|
+
curLine = curLine.substring(0, match.index);
|
|
989
909
|
}
|
|
990
|
-
|
|
910
|
+
// Malformed input (a run of stray UTF-8 continuation bytes, or a dangling "=" at the
|
|
911
|
+
// end) has no split point that keeps a character sequence whole, so the loop below
|
|
912
|
+
// walks back to an empty line looking for one. An empty line never consumes anything
|
|
913
|
+
// and would loop forever, so keep the widest chunk that at least does not cut a "=XX"
|
|
914
|
+
// escape in half, or the rest of the string when there is not even that
|
|
915
|
+
fallbackLine = curLine.length ? curLine : str.substring(0, maxlen);
|
|
991
916
|
done = false;
|
|
992
|
-
while (!done) {
|
|
917
|
+
while (!done && curLine.length) {
|
|
993
918
|
done = true;
|
|
994
919
|
// check if not middle of a unicode char sequence
|
|
995
|
-
if ((match = str.
|
|
920
|
+
if ((match = str.slice(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
|
|
996
921
|
chr = parseInt(match[1], 16);
|
|
997
|
-
// invalid sequence, move one char back
|
|
998
|
-
|
|
999
|
-
|
|
922
|
+
// invalid sequence, move one escaped char back and recheck. Only over a whole
|
|
923
|
+
// "=XX" escape, a plain char in front of a stray continuation byte ends the walk
|
|
924
|
+
if (chr < 0xc2 && chr > 0x7f && /=[0-9A-F]{2}$/i.test(curLine)) {
|
|
925
|
+
curLine = curLine.substring(0, curLine.length - 3);
|
|
1000
926
|
done = false;
|
|
1001
927
|
}
|
|
1002
928
|
}
|
|
1003
929
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
lines.push(curLine);
|
|
930
|
+
if (!curLine.length) {
|
|
931
|
+
curLine = fallbackLine;
|
|
1007
932
|
}
|
|
1008
|
-
|
|
933
|
+
lines.push(curLine);
|
|
934
|
+
str = str.slice(curLine.length);
|
|
1009
935
|
}
|
|
1010
|
-
|
|
1011
936
|
return lines;
|
|
1012
937
|
}
|
|
1013
|
-
|
|
1014
938
|
encodeURICharComponent(chr) {
|
|
1015
939
|
let res = '';
|
|
1016
940
|
// percent encoding is defined over bytes, so encode the char as UTF-8 first
|
|
1017
941
|
let buf = Buffer.from(chr, 'utf-8');
|
|
1018
|
-
|
|
1019
942
|
for (let i = 0, len = buf.length; i < len; i++) {
|
|
1020
943
|
let ord = buf[i].toString(16).toUpperCase();
|
|
1021
944
|
res += '%' + (ord.length < 2 ? '0' : '') + ord;
|
|
1022
945
|
}
|
|
1023
|
-
|
|
1024
946
|
return res;
|
|
1025
947
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
str = (str || '').toString();
|
|
1029
|
-
|
|
948
|
+
safeEncodeURIComponent(input) {
|
|
949
|
+
let str = (input || '').toString();
|
|
1030
950
|
try {
|
|
1031
951
|
// might throw if we try to encode invalid sequences, eg. partial emoji
|
|
1032
952
|
str = encodeURIComponent(str);
|
|
1033
|
-
} catch (E) {
|
|
1034
|
-
// should never run
|
|
1035
|
-
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
|
|
1036
953
|
}
|
|
1037
|
-
|
|
954
|
+
catch (_err) {
|
|
955
|
+
// an unpaired surrogate has no utf-8 representation, so run the value through a
|
|
956
|
+
// utf-8 roundtrip to get the same U+FFFD every other encoder here produces and
|
|
957
|
+
// retry. The value must never come back unencoded, it goes into a header parameter
|
|
958
|
+
// where a bare quote or semicolon would break it out into a parameter of its own
|
|
959
|
+
str = encodeURIComponent(Buffer.from(str, 'utf-8').toString('utf-8'));
|
|
960
|
+
}
|
|
1038
961
|
// ensure chars that are not handled by encodeURICompent are converted as well
|
|
1039
962
|
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr));
|
|
1040
963
|
}
|
|
1041
964
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
965
|
+
/**
|
|
966
|
+
* The shared instance, the default export of the package. The class is attached to it as `Libmime`,
|
|
967
|
+
* so `require('libmime').Libmime` keeps working
|
|
968
|
+
*/
|
|
969
|
+
const libmime = new Libmime();
|
|
970
|
+
libmime.Libmime = Libmime;
|
|
971
|
+
export default libmime;
|