@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.
Files changed (96) hide show
  1. package/.jsii +4 -4
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/node_modules/@zone-eu/mailsplit/README.md +1 -1
  5. package/node_modules/@zone-eu/mailsplit/lib/headers.js +10 -4
  6. package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +5 -4
  7. package/node_modules/@zone-eu/mailsplit/lib/mime-node.d.ts +4 -1
  8. package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +23 -2
  9. package/node_modules/@zone-eu/mailsplit/lib/types.d.ts +3 -1
  10. package/node_modules/@zone-eu/mailsplit/package.json +4 -4
  11. package/node_modules/libbase64/CHANGELOG.md +20 -0
  12. package/node_modules/libbase64/README.md +40 -13
  13. package/node_modules/libbase64/dist/cjs/libbase64.d.ts +80 -0
  14. package/node_modules/libbase64/dist/cjs/libbase64.js +310 -0
  15. package/node_modules/libbase64/dist/cjs/package.json +3 -0
  16. package/node_modules/libbase64/dist/esm/libbase64.d.ts +80 -0
  17. package/node_modules/libbase64/{lib → dist/esm}/libbase64.js +74 -117
  18. package/node_modules/libbase64/dist/esm/package.json +3 -0
  19. package/node_modules/libbase64/package.json +59 -13
  20. package/node_modules/libmime/CHANGELOG.md +31 -0
  21. package/node_modules/libmime/README.md +55 -26
  22. package/node_modules/libmime/SECURITY.md +152 -0
  23. package/node_modules/libmime/dist/cjs/charset.d.ts +43 -0
  24. package/node_modules/libmime/dist/cjs/charset.js +135 -0
  25. package/node_modules/libmime/dist/cjs/charsets.d.ts +2 -0
  26. package/node_modules/libmime/dist/cjs/charsets.js +213 -0
  27. package/node_modules/libmime/dist/cjs/get-charset-name.d.ts +1 -0
  28. package/node_modules/libmime/dist/cjs/get-charset-name.js +224 -0
  29. package/node_modules/libmime/dist/cjs/libmime.d.ts +218 -0
  30. package/node_modules/libmime/dist/cjs/libmime.js +1013 -0
  31. package/node_modules/libmime/dist/cjs/mimetypes.d.ts +7 -0
  32. package/node_modules/libmime/dist/cjs/mimetypes.js +2072 -0
  33. package/node_modules/libmime/dist/cjs/package.json +3 -0
  34. package/node_modules/libmime/dist/cjs/types.d.ts +59 -0
  35. package/node_modules/libmime/dist/cjs/types.js +4 -0
  36. package/node_modules/libmime/dist/esm/charset.d.ts +43 -0
  37. package/node_modules/libmime/dist/esm/charset.js +125 -0
  38. package/node_modules/libmime/dist/esm/charsets.d.ts +2 -0
  39. package/node_modules/libmime/{lib → dist/esm}/charsets.js +3 -5
  40. package/node_modules/libmime/dist/esm/get-charset-name.d.ts +1 -0
  41. package/node_modules/libmime/{lib → dist/esm}/get-charset-name.js +1 -8
  42. package/node_modules/libmime/dist/esm/libmime.d.ts +219 -0
  43. package/node_modules/libmime/{lib → dist/esm}/libmime.js +256 -329
  44. package/node_modules/libmime/dist/esm/mimetypes.d.ts +7 -0
  45. package/node_modules/libmime/dist/esm/mimetypes.js +2068 -0
  46. package/node_modules/libmime/dist/esm/package.json +3 -0
  47. package/node_modules/libmime/dist/esm/types.d.ts +59 -0
  48. package/node_modules/libmime/dist/esm/types.js +3 -0
  49. package/node_modules/libmime/package.json +58 -15
  50. package/node_modules/libqp/CHANGELOG.md +18 -0
  51. package/node_modules/libqp/README.md +50 -19
  52. package/node_modules/libqp/dist/cjs/libqp.d.ts +74 -0
  53. package/node_modules/libqp/dist/cjs/libqp.js +378 -0
  54. package/node_modules/libqp/dist/cjs/package.json +3 -0
  55. package/node_modules/libqp/dist/esm/libqp.d.ts +74 -0
  56. package/node_modules/libqp/dist/esm/libqp.js +369 -0
  57. package/node_modules/libqp/dist/esm/package.json +3 -0
  58. package/node_modules/libqp/lib/libqp.js +4 -383
  59. package/node_modules/libqp/lib/package.json +3 -0
  60. package/node_modules/libqp/package.json +62 -6
  61. package/node_modules/mailparser/.release-please-manifest.json +1 -1
  62. package/node_modules/mailparser/CHANGELOG.md +42 -0
  63. package/node_modules/mailparser/package.json +5 -5
  64. package/node_modules/nodemailer/CHANGELOG.md +44 -0
  65. package/node_modules/nodemailer/dist/cjs/addressparser/index.js +695 -267
  66. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +4 -1
  67. package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +8 -2
  68. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +48 -16
  69. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  70. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  71. package/node_modules/nodemailer/dist/cjs/shared/url.js +22 -5
  72. package/node_modules/nodemailer/dist/esm/addressparser/index.js +695 -267
  73. package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +4 -1
  74. package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +8 -2
  75. package/node_modules/nodemailer/dist/esm/mime-node/index.js +48 -16
  76. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  77. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  78. package/node_modules/nodemailer/dist/esm/shared/url.js +22 -5
  79. package/node_modules/nodemailer/package.json +1 -1
  80. package/package.json +3 -3
  81. package/node_modules/libbase64/.eslintrc +0 -11
  82. package/node_modules/libbase64/.github/FUNDING.yml +0 -4
  83. package/node_modules/libbase64/.github/VULNERABILITY_REPORT.yml +0 -75
  84. package/node_modules/libbase64/.github/workflows/release.yaml +0 -39
  85. package/node_modules/libbase64/.github/workflows/test.yml +0 -21
  86. package/node_modules/libbase64/.ncurc.js +0 -13
  87. package/node_modules/libbase64/.prettierrc.js +0 -8
  88. package/node_modules/libmime/lib/charset.js +0 -131
  89. package/node_modules/libmime/lib/mimetypes.js +0 -2049
  90. package/node_modules/libqp/.eslintrc +0 -15
  91. package/node_modules/libqp/.github/FUNDING.yml +0 -4
  92. package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +0 -75
  93. package/node_modules/libqp/.github/workflows/release.yaml +0 -39
  94. package/node_modules/libqp/.github/workflows/test.yml +0 -21
  95. package/node_modules/libqp/.prettierrc.js +0 -8
  96. package/node_modules/libqp/test/libqp-test.js +0 -366
@@ -1,15 +1,11 @@
1
- /* eslint no-control-regex: 0, no-div-regex: 0, quotes: 0 */
2
- 'use strict';
3
-
4
- const { Buffer } = require('node:buffer');
5
- const libcharset = require('./charset');
6
- const libbase64 = require('libbase64');
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
- } else {
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
- if (typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) {
51
- return false;
52
- } else {
53
- return true;
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(str, delSp) {
81
- str = (str || '').toString();
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 = null,
91
- partsLength = 0;
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 !== null && parts.length ? parts[parts.length - 1] : '';
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
- pushPart(line);
121
- } else {
122
- if (parts !== null) {
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
- flowed.push(
155
- this.foldLines(
156
- line
157
- // space stuffing http://tools.ietf.org/html/rfc3676#section-4.2
158
- .replace(/^( |From|>)/gim, ' $1'),
159
- lineLength,
160
- true
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
- } else {
185
+ }
186
+ else {
193
187
  return '=' + (ord.length === 1 ? '0' + ord : ord);
194
188
  }
195
189
  });
196
- } else if (mimeWordEncoding === 'B') {
197
- encodedStr = typeof data === 'string' ? data : libbase64.encode(data);
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
- } else {
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
- for (let i = 0, len = encodedStr.length; i < len; i++) {
209
- let chr = encodedStr.charAt(i);
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
- } else {
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
- } else {
225
+ }
226
+ else {
233
227
  encodedStr = parts.join('');
234
228
  }
235
229
  }
236
- } else if (mimeWordEncoding === 'B') {
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.substr(0, splitPos);
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
- str = Buffer.from(bytes);
301
- } else if (encoding === 'B') {
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
- str = libbase64.decode(str);
304
- } else {
294
+ decoded = libbase64.decode(str);
295
+ }
296
+ else {
305
297
  // keep as is, convert Buffer to unicode string, assume utf8
306
- str = Buffer.from(str);
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.index +
338
- (
339
- firstMatch[0].match(/[^\s]/) || {
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.substr(0, startIndex) : '') +
356
- this.encodeWord(decodedValue.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
357
- (endIndex < decodedValue.length ? decodedValue.substr(endIndex) : '');
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
- (str || '')
371
- .toString()
372
- // find base64 words that can be joined
373
- .replace(/(=\?([^?]+)\?[Bb]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
374
- // only mark b64 chunks to be joined if charsets match
375
- if (libcharset.normalizeCharset(chLeft || '') === libcharset.normalizeCharset(chRight || '')) {
376
- // set a joiner marker
377
- return left + '__\x00JOIN\x00__';
378
- }
379
- return match;
380
- })
381
- // find QP words that can be joined
382
- .replace(/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
383
- // only mark QP chunks to be joined if charsets match
384
- if (libcharset.normalizeCharset(chLeft || '') === libcharset.normalizeCharset(chRight || '')) {
385
- // set a joiner marker
386
- return left + '__\x00JOIN\x00__';
387
- }
388
- return match;
389
- })
390
- // join base64 encoded words
391
- .replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, '')
392
- // remove spaces between mime encoded words
393
- .replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, '$1')
394
- // decode words
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 ((c >= 0x30 /* 0 */ && c <= 0x39) /* 9 */ || (c >= 0x61 /* a */ && c <= 0x66) /* f */ || (c >= 0x41 /* A */ && c <= 0x46) /* F */) {
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
- .toString()
417
- .replace(/(?:\r?\n|\r)[ \t]*/g, ' ')
418
- .trim(),
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
- } else {
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
- if (!hasOwn(headersObj, header.key)) {
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
- } else {
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(structured.params || {}).forEach(param => {
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 = structured.params[param];
492
- if (!this.isPlainText(value) || value.length >= 75) {
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.substr(-1) === '*') {
462
+ if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.endsWith('*')) {
495
463
  paramsArray.push(encodedParam.key + '=' + encodedParam.value);
496
- } else {
464
+ }
465
+ else {
497
466
  paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
498
467
  }
499
468
  });
500
- } else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
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
- } else {
475
+ }
476
+ else {
503
477
  paramsArray.push(param + '=' + value);
504
478
  }
505
479
  });
506
-
507
- return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
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: false,
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
- } else if (key === false) {
538
+ }
539
+ else if (key === false) {
569
540
  // default value
570
541
  response.value = collected;
571
- } else {
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
- } else if (chr === ';') {
557
+ }
558
+ else if (chr === ';') {
587
559
  // key without a value, eg. the "inline" in "attachment; inline; filename=a.txt"
588
560
  commit();
589
- } else if (isWSP(chr)) {
561
+ }
562
+ else if (isWSP(chr)) {
590
563
  if (value.length) {
591
564
  value += chr;
592
565
  }
593
- } else {
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
- } else if (chr === '\\') {
576
+ }
577
+ else if (chr === '\\') {
603
578
  escaped = true;
604
579
  continue;
605
- } else if (chr === '"') {
580
+ }
581
+ else if (chr === '"') {
606
582
  // every char between the quotes is significant, including whitespace
607
583
  quote = !quote;
608
- } else if (!quote && chr === ';') {
584
+ }
585
+ else if (!quote && chr === ';') {
609
586
  commit();
610
587
  stage = STAGE_KEY;
611
- } else if (!quote && isWSP(chr)) {
588
+ }
589
+ else if (!quote && isWSP(chr)) {
612
590
  if (value.length) {
613
591
  value += chr;
614
592
  }
615
- } else {
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
- if (s === ' ') {
695
- return '_';
696
- }
697
- let c = s.charCodeAt(0).toString(16);
698
- return '%' + (c.length < 2 ? '0' : '') + c;
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 encodedStrArr;
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.isPlainText(data)) {
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
- } else {
722
+ }
723
+ else {
768
724
  if (/[\uD800-\uDBFF]/.test(encodedStr)) {
769
- // string containts surrogate pairs, so normalize it to an array of bytes
770
- encodedStrArr = [];
771
- for (i = 0, len = encodedStr.length; i < len; i++) {
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
- } else {
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
- startPos = i - 1;
814
- } else {
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
- } else {
779
+ }
780
+ else {
834
781
  isEncoded = true;
835
782
  }
836
- } else {
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(mimeType) {
866
- mimeType = (mimeType || '').toString().toLowerCase().replace(/\s/g, '');
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(extension) {
896
- extension = (extension || '').toString().toLowerCase().replace(/\s/g, '').replace(/^\./g, '').split('.').pop();
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(str, lineLength, afterSpace) {
930
- str = (str || '').toString();
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.substr(pos, lineLength);
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.substr(0, maxlen);
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.substr(0, match.index);
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.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
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 anc recheck
998
- if (chr < 0xc2 && chr > 0x7f) {
999
- curLine = curLine.substr(0, curLine.length - 3);
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
- if (curLine.length) {
1006
- lines.push(curLine);
930
+ if (!curLine.length) {
931
+ curLine = fallbackLine;
1007
932
  }
1008
- str = str.substr(curLine.length);
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
- safeEncodeURIComponent(str) {
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
- module.exports = new Libmime();
1044
- module.exports.Libmime = Libmime;
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;