imapkit 0.0.0-stage → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +608 -2
  3. package/bin/help.txt +98 -0
  4. package/bin/imapkit.js +108 -0
  5. package/cert/server.crt +20 -0
  6. package/cert/server.key +28 -0
  7. package/lib/addressparser.js +283 -0
  8. package/lib/arguments.js +112 -0
  9. package/lib/bodystructure.js +149 -0
  10. package/lib/command-states.js +109 -0
  11. package/lib/commands/append.js +313 -0
  12. package/lib/commands/capability.js +47 -0
  13. package/lib/commands/check.js +21 -0
  14. package/lib/commands/close.js +30 -0
  15. package/lib/commands/copy.js +115 -0
  16. package/lib/commands/create.js +52 -0
  17. package/lib/commands/delete.js +64 -0
  18. package/lib/commands/examine.js +7 -0
  19. package/lib/commands/expunge.js +27 -0
  20. package/lib/commands/fetch.js +229 -0
  21. package/lib/commands/handlers/fetch.js +209 -0
  22. package/lib/commands/handlers/flags.js +42 -0
  23. package/lib/commands/handlers/search.js +519 -0
  24. package/lib/commands/handlers/status.js +85 -0
  25. package/lib/commands/handlers/store.js +127 -0
  26. package/lib/commands/list.js +100 -0
  27. package/lib/commands/login.js +67 -0
  28. package/lib/commands/logout.js +41 -0
  29. package/lib/commands/lsub.js +87 -0
  30. package/lib/commands/noop.js +21 -0
  31. package/lib/commands/rename.js +102 -0
  32. package/lib/commands/search.js +76 -0
  33. package/lib/commands/select.js +289 -0
  34. package/lib/commands/status.js +63 -0
  35. package/lib/commands/store.js +151 -0
  36. package/lib/commands/subscribe.js +53 -0
  37. package/lib/commands/uid copy.js +7 -0
  38. package/lib/commands/uid fetch.js +5 -0
  39. package/lib/commands/uid search.js +5 -0
  40. package/lib/commands/uid store.js +5 -0
  41. package/lib/commands/unsubscribe.js +50 -0
  42. package/lib/dates.js +123 -0
  43. package/lib/deflate-layer.js +232 -0
  44. package/lib/envelope.js +82 -0
  45. package/lib/esearch.js +208 -0
  46. package/lib/framing.js +102 -0
  47. package/lib/list-extensions.js +36 -0
  48. package/lib/load-plugins.js +109 -0
  49. package/lib/mailbox-name.js +133 -0
  50. package/lib/mimeparser.js +778 -0
  51. package/lib/mock-client.js +233 -0
  52. package/lib/numbers.js +52 -0
  53. package/lib/plugins/acl.js +964 -0
  54. package/lib/plugins/appendlimit.js +83 -0
  55. package/lib/plugins/auth-plain.js +94 -0
  56. package/lib/plugins/binary.js +256 -0
  57. package/lib/plugins/catenate.js +253 -0
  58. package/lib/plugins/compress.js +76 -0
  59. package/lib/plugins/condstore.js +563 -0
  60. package/lib/plugins/context-search.js +321 -0
  61. package/lib/plugins/context-sort.js +19 -0
  62. package/lib/plugins/create-special-use.js +108 -0
  63. package/lib/plugins/enable.js +155 -0
  64. package/lib/plugins/esearch.js +156 -0
  65. package/lib/plugins/esort.js +60 -0
  66. package/lib/plugins/id.js +138 -0
  67. package/lib/plugins/idle.js +105 -0
  68. package/lib/plugins/imap4rev2.js +202 -0
  69. package/lib/plugins/list-extended.js +258 -0
  70. package/lib/plugins/list-status.js +31 -0
  71. package/lib/plugins/literalminus.js +20 -0
  72. package/lib/plugins/literalplus.js +18 -0
  73. package/lib/plugins/logindisabled.js +50 -0
  74. package/lib/plugins/messagelimit.js +234 -0
  75. package/lib/plugins/metadata-server.js +13 -0
  76. package/lib/plugins/metadata.js +475 -0
  77. package/lib/plugins/move.js +110 -0
  78. package/lib/plugins/multiappend.js +26 -0
  79. package/lib/plugins/multisearch.js +269 -0
  80. package/lib/plugins/namespace.js +67 -0
  81. package/lib/plugins/notify.js +654 -0
  82. package/lib/plugins/oauthbearer.js +217 -0
  83. package/lib/plugins/objectid.js +243 -0
  84. package/lib/plugins/partial.js +68 -0
  85. package/lib/plugins/preview.js +400 -0
  86. package/lib/plugins/qresync.js +525 -0
  87. package/lib/plugins/quota.js +285 -0
  88. package/lib/plugins/replace.js +145 -0
  89. package/lib/plugins/sasl-ir.js +12 -0
  90. package/lib/plugins/savedate.js +59 -0
  91. package/lib/plugins/savelimit.js +18 -0
  92. package/lib/plugins/searchres.js +82 -0
  93. package/lib/plugins/sort-display.js +23 -0
  94. package/lib/plugins/sort.js +132 -0
  95. package/lib/plugins/special-use.js +95 -0
  96. package/lib/plugins/starttls.js +57 -0
  97. package/lib/plugins/status-size.js +19 -0
  98. package/lib/plugins/thread-orderedsubject.js +16 -0
  99. package/lib/plugins/thread-references.js +16 -0
  100. package/lib/plugins/uidonly.js +135 -0
  101. package/lib/plugins/uidplus.js +124 -0
  102. package/lib/plugins/unauthenticate.js +28 -0
  103. package/lib/plugins/unselect.js +36 -0
  104. package/lib/plugins/utf8-accept.js +68 -0
  105. package/lib/plugins/x-gm-ext-1.js +456 -0
  106. package/lib/plugins/xoauth2.js +188 -0
  107. package/lib/plugins/xtoybird.js +282 -0
  108. package/lib/server.js +2880 -0
  109. package/lib/smtp-listener.js +51 -0
  110. package/lib/sorting.js +373 -0
  111. package/lib/threading.js +357 -0
  112. package/lib/utf8-session.js +123 -0
  113. package/lib/vanished.js +57 -0
  114. package/package.json +61 -5
@@ -0,0 +1,778 @@
1
+ 'use strict';
2
+
3
+ // Parses a message into a MIME tree. Ported from WildDuck (imap-core/lib/indexer/parse-mime-tree.js and
4
+ // tree-walker.js), adapted to ImapKit, where message sources are binary strings (one char per octet).
5
+ //
6
+ // The tree describes the message exactly, so every section a client can FETCH is rendered from it:
7
+ //
8
+ // entity = header-lines [ CRLF body ] ; "CRLF body" present unless hasBody is false
9
+ // body = bytes | preamble *( delimiter entity CRLF ) close-delimiter epilogue
10
+ // delimiter = "--" boundary pad CRLF
11
+ //
12
+ // The CRLF after an entity belongs to the delimiter that follows it (RFC 2046 5.1.1), a part body never
13
+ // includes it.
14
+
15
+ const addressparser = require('./addressparser');
16
+
17
+ // header field and parameter names that are kept in the parsed header, anything else is only available
18
+ // through the raw header lines
19
+ const FIELD_NAME = /^[a-zA-Z0-9\-*]{1,99}$/;
20
+
21
+ // content transfer encodings that leave the data as it is (RFC 2045 section 6.2)
22
+ const IDENTITY_ENCODINGS = ['7bit', '8bit', 'binary'];
23
+
24
+ // message/rfc822 parts nested deeper than this are not parsed, they are described as plain parts
25
+ const MAX_MESSAGE_DEPTH = 32;
26
+
27
+ /**
28
+ * Converts every line break of a message to CRLF. IMAP serves messages with CRLF line breaks, so sizes and
29
+ * contents are computed from this form
30
+ *
31
+ * @param {String} raw Message source as a binary string
32
+ * @return {String} Message source with CRLF line breaks
33
+ */
34
+ function normalizeLineBreaks(raw) {
35
+ const str = (raw || '').toString('binary');
36
+ // most messages already use CRLF, so only replace if there is a bare LF
37
+ return /(?:^|[^\r])\n/.test(str) ? str.replace(/\r?\n/g, '\r\n') : str;
38
+ }
39
+
40
+ /**
41
+ * Splits a structured header value (RFC 2045 5.1 Content-Type and friends) at a separator character,
42
+ * leaving quoted strings intact and dropping RFC 822 comments outside of them. With no separator the
43
+ * whole value comes back as one part with its comments removed.
44
+ *
45
+ * @param {String} value Header value
46
+ * @param {String} [separator] Character to split at, outside quotes and comments
47
+ * @returns {Array} Parts
48
+ */
49
+ function splitStructuredValue(value, separator) {
50
+ const parts = [];
51
+ let current = '';
52
+ let quoted = false;
53
+ let depth = 0;
54
+
55
+ for (let i = 0; i < value.length; i++) {
56
+ const chr = value.charAt(i);
57
+
58
+ if (quoted) {
59
+ current += chr;
60
+ if (chr === '\\' && i + 1 < value.length) {
61
+ // quoted-pair, keep the escaped character for the value parser
62
+ current += value.charAt(++i);
63
+ } else if (chr === '"') {
64
+ quoted = false;
65
+ }
66
+ continue;
67
+ }
68
+
69
+ if (depth) {
70
+ // inside a comment, which may nest and may hold quoted-pairs
71
+ if (chr === '\\') {
72
+ i++;
73
+ } else if (chr === '(') {
74
+ depth++;
75
+ } else if (chr === ')') {
76
+ depth--;
77
+ }
78
+ continue;
79
+ }
80
+
81
+ if (chr === '"') {
82
+ quoted = true;
83
+ current += chr;
84
+ } else if (chr === '(') {
85
+ depth = 1;
86
+ } else if (separator && chr === separator) {
87
+ parts.push(current);
88
+ current = '';
89
+ } else {
90
+ current += chr;
91
+ }
92
+ }
93
+
94
+ parts.push(current);
95
+ return parts;
96
+ }
97
+
98
+ /**
99
+ * Decodes an RFC 2231 extended parameter value (percent encoded octets in a charset). UTF-8 and US-ASCII
100
+ * values become the UTF-8 octets, ISO-8859-1 values are converted to UTF-8, other charsets are kept as
101
+ * an RFC 2047 encoded word
102
+ *
103
+ * @param {String} charset Charset name from the first segment
104
+ * @param {String} value Percent encoded octets
105
+ * @return {String} Decoded value as a binary string
106
+ */
107
+ function decodeExtendedValue(charset, value) {
108
+ const octets = value.replace(/%([0-9a-fA-F]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
109
+ switch ((charset || '').trim().toLowerCase()) {
110
+ case '':
111
+ case 'utf-8':
112
+ case 'utf8':
113
+ case 'us-ascii':
114
+ case 'ascii':
115
+ return octets;
116
+ case 'iso-8859-1':
117
+ case 'latin1':
118
+ // every char of the binary string is an ISO-8859-1 character
119
+ return Buffer.from(octets, 'utf-8').toString('binary');
120
+ default:
121
+ return '=?' + charset.toUpperCase() + '?Q?' + value.replace(/%/g, '=') + '?=';
122
+ }
123
+ }
124
+
125
+ class MIMEParser {
126
+ /**
127
+ * @param {String} rfc822 Message source as a binary string with CRLF line breaks
128
+ * @param {Number} [depth] How many message/rfc822 levels this message is nested in
129
+ */
130
+ constructor(rfc822, depth) {
131
+ this.rfc822 = rfc822 || '';
132
+ this.depth = depth || 0;
133
+
134
+ // the line break that ended the last line read, false once the input is used up
135
+ this._br = '';
136
+ this._pos = 0;
137
+
138
+ this.tree = {
139
+ childNodes: []
140
+ };
141
+ this._node = this.createNode(this.tree);
142
+ }
143
+
144
+ /**
145
+ * Parses the message, line by line
146
+ */
147
+ parse() {
148
+ let line;
149
+ let prevBr = '';
150
+
151
+ // keep parsing until the last linebreak is not a string (no linebreaks anymore)
152
+ while (typeof this._br === 'string') {
153
+ line = this.readLine();
154
+
155
+ const delimiter = this.matchDelimiter(line);
156
+ if (delimiter) {
157
+ this.endPart(delimiter, prevBr);
158
+ } else {
159
+ switch (this._node.state) {
160
+ case 'header':
161
+ if (!line) {
162
+ // the blank line that separates the header from the body
163
+ this.endHeader();
164
+ } else {
165
+ this._node.header.push(line);
166
+ }
167
+ break;
168
+
169
+ case 'body':
170
+ // push the line with previous linebreak value, joined together the lines
171
+ // give the original body
172
+ this._node.body.push((this._node.body.length ? prevBr : '') + line);
173
+ break;
174
+
175
+ case 'epilogue':
176
+ // RFC 2046 5.1.1: everything after the close delimiter up to the next delimiter
177
+ // of the enclosing multipart (or the end of the message) is the epilogue. Every
178
+ // epilogue line keeps the line break that precedes it, the first one being the
179
+ // line break that ends the close delimiter line
180
+ if (!this._node.epilogue) {
181
+ this._node.epilogue = [];
182
+ }
183
+ this._node.epilogue.push(prevBr + line);
184
+ break;
185
+
186
+ default:
187
+ // never should be reached
188
+ throw new Error('Unexpected state');
189
+ }
190
+ }
191
+
192
+ // store the linebreak for later usage
193
+ prevBr = this._br;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Reads a line from the message
199
+ *
200
+ * @return {String} The line, without its line break
201
+ */
202
+ readLine() {
203
+ const end = this.rfc822.indexOf('\n', this._pos);
204
+ if (end < 0) {
205
+ // the remainder, which is empty when the input ended with a line break
206
+ const line = this.rfc822.slice(this._pos);
207
+ this._pos = this.rfc822.length;
208
+ this._br = false;
209
+ return line;
210
+ }
211
+
212
+ let lineEnd = end;
213
+ if (lineEnd > this._pos && this.rfc822.charCodeAt(lineEnd - 1) === 0x0d) {
214
+ lineEnd--;
215
+ }
216
+
217
+ const line = this.rfc822.slice(this._pos, lineEnd);
218
+ this._br = this.rfc822.slice(lineEnd, end + 1);
219
+ this._pos = end + 1;
220
+ return line;
221
+ }
222
+
223
+ /**
224
+ * Ends the header of the current node: its blank line was read, or a delimiter took its place
225
+ */
226
+ endHeader() {
227
+ this.processNodeHeader();
228
+ this.processContentType();
229
+ this._node.state = 'body';
230
+ }
231
+
232
+ /**
233
+ * Checks whether a line is a delimiter of an open multipart: the current node while it collects its
234
+ * preamble or epilogue, the multipart the current node belongs to, or any multipart above that (a
235
+ * lost close delimiter leaves the inner multipart open, the enclosing delimiter still ends it).
236
+ * RFC 2046 5.1.1: `--boundary` or `--boundary--`, optionally followed by transport padding (spaces
237
+ * and tabs) that receivers must accept. After the close delimiter, lines that look like the own
238
+ * delimiter are epilogue text.
239
+ *
240
+ * @param {String} line Line of the message
241
+ * @return {Object|Boolean} `{ multipart, close, pad }`, or false when the line is not a delimiter
242
+ */
243
+ matchDelimiter(line) {
244
+ if (!line.startsWith('--')) {
245
+ return false;
246
+ }
247
+
248
+ const node = this._node;
249
+ // a boundary is only known once the header has ended
250
+ const innermost = node.boundary ? node : node.parentNode;
251
+ for (let multipart = innermost; multipart.boundary; multipart = multipart.parentNode) {
252
+ const delimiter = '--' + multipart.boundary;
253
+ if (!line.startsWith(delimiter)) {
254
+ continue;
255
+ }
256
+
257
+ let rest = line.slice(delimiter.length);
258
+ const close = rest.startsWith('--');
259
+ if (close) {
260
+ rest = rest.slice(2);
261
+ }
262
+
263
+ if (/[^ \t]/.test(rest)) {
264
+ // something other than padding after the boundary, so a different boundary or text
265
+ continue;
266
+ }
267
+
268
+ if (multipart.state === 'epilogue') {
269
+ return false;
270
+ }
271
+
272
+ return { multipart, close, pad: rest };
273
+ }
274
+
275
+ return false;
276
+ }
277
+
278
+ /**
279
+ * Ends whatever the current node was collecting at a delimiter line and moves on to the next
280
+ * part of that multipart, or to its epilogue
281
+ *
282
+ * @param {Object} delimiter Result of matchDelimiter()
283
+ * @param {String} prevBr The line break that ended the line before the delimiter
284
+ */
285
+ endPart(delimiter, prevBr) {
286
+ const node = this._node;
287
+ const multipart = delimiter.multipart;
288
+
289
+ if (node !== multipart) {
290
+ // a part ends here
291
+ if (node.state === 'header') {
292
+ // a delimiter right after the header lines, with no blank line and no line break of
293
+ // its own (RFC 2046 5.1.1 wants one before every delimiter): the line break that ended
294
+ // the last header line, or the previous delimiter line, is the only one there is
295
+ this.endHeader();
296
+ // the multiparts a lost close delimiter leaves open between this part and the one the
297
+ // delimiter belongs to end with this part, so they end bare too
298
+ for (let open = node; open !== multipart; open = open.parentNode) {
299
+ open.bare = true;
300
+ }
301
+ }
302
+ } else if (node.body.length) {
303
+ // the preamble ends here. The line break between the preamble and the delimiter belongs to
304
+ // the delimiter, but the preamble lines end with their own
305
+ node.body[node.body.length - 1] += prevBr;
306
+ }
307
+
308
+ if (!delimiter.close) {
309
+ const next = this.createNode(multipart);
310
+ if (delimiter.pad) {
311
+ next.pad = delimiter.pad;
312
+ }
313
+ this._node = next;
314
+ } else {
315
+ if (delimiter.pad) {
316
+ multipart.closePad = delimiter.pad;
317
+ }
318
+ delete multipart.unterminated;
319
+ multipart.state = 'epilogue';
320
+ this._node = multipart;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Parses the body of a message/rfc822 node into `node.message`. RFC 2046 5.2.1 only allows the
326
+ * identity encodings for such a body, RFC 2045 5.1 and 6.1 make the type and encoding names case
327
+ * insensitive. A message/global body (RFC 6532 section 3.5) goes to `node.globalMessage`, as only
328
+ * IMAP4rev2 treats it like message/rfc822 (RFC 9051 section 7.5.2), see embeddedMessage()
329
+ */
330
+ parseEmbeddedMessage(node) {
331
+ const contentType = node.parsedHeader['content-type'];
332
+ const type = ((contentType && contentType.value) || '').toLowerCase();
333
+ if ((type !== 'message/rfc822' && type !== 'message/global') || this.depth >= MAX_MESSAGE_DEPTH) {
334
+ return;
335
+ }
336
+ const encoding = (node.parsedHeader['content-transfer-encoding'] || '').toString().trim().toLowerCase();
337
+ if (encoding && !IDENTITY_ENCODINGS.includes(encoding)) {
338
+ return;
339
+ }
340
+ node[type === 'message/rfc822' ? 'message' : 'globalMessage'] = parseTree(node.body, this.depth + 1);
341
+ }
342
+
343
+ /**
344
+ * Joins body arrays into strings. Removes unnecessary fields from the tree
345
+ */
346
+ finalizeTree() {
347
+ if (this._node.state === 'header') {
348
+ this.endHeader();
349
+ if (this._node.header.length && !this.rfc822.endsWith('\n')) {
350
+ // the input ended within the last header line, there is no line break to render
351
+ this._node.headerUnterminated = true;
352
+ }
353
+ }
354
+
355
+ // iterative, so that deeply nested multiparts do not exhaust the stack
356
+ const stack = [].concat(this.tree.childNodes);
357
+ while (stack.length) {
358
+ const node = stack.pop();
359
+
360
+ // a body section holds lines, parts, or at least a close delimiter. A blank line followed by
361
+ // the end of the input leaves the trailing empty line in the body, one followed directly by
362
+ // a delimiter leaves nothing
363
+ const closed = node.boundary && !node.unterminated;
364
+ if (!(node.body.length || node.childNodes.length || closed)) {
365
+ node.hasBody = false;
366
+ }
367
+
368
+ // RFC 3501 7.4.2 body-fld-lines, counted like Dovecot does: the line breaks in the body
369
+ node.lineCount = node.body.length ? node.body.length - 1 : 0;
370
+ node.body = node.body.join('');
371
+ node.size = node.body.length;
372
+
373
+ // a message/rfc822 entity carries its parsed message
374
+ this.parseEmbeddedMessage(node);
375
+
376
+ if (node.epilogue) {
377
+ node.epilogue = node.epilogue.join('');
378
+ }
379
+
380
+ node.childNodes.forEach(child => stack.push(child));
381
+
382
+ // remove unneeded properties
383
+ delete node.parentNode;
384
+ delete node.state;
385
+ if (!node.childNodes.length) {
386
+ delete node.childNodes;
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Creates a new node with default values for the parse tree
393
+ */
394
+ createNode(parentNode) {
395
+ const node = {
396
+ state: 'header',
397
+ childNodes: [],
398
+ header: [],
399
+ parsedHeader: Object.create(null),
400
+ body: [],
401
+ multipart: false,
402
+ boundary: false,
403
+ parentNode
404
+ };
405
+ parentNode.childNodes.push(node);
406
+ return node;
407
+ }
408
+
409
+ /**
410
+ * Processes header lines. Splits lines to key-value pairs
411
+ * and processes special values
412
+ */
413
+ processNodeHeader() {
414
+ const node = this._node;
415
+
416
+ // RFC 5322 2.2.3: a line that starts with whitespace continues the previous header field
417
+ const header = [];
418
+ for (const line of node.header) {
419
+ if (header.length && /^\s/.test(line)) {
420
+ header[header.length - 1] += '\r\n' + line;
421
+ } else {
422
+ header.push(line);
423
+ }
424
+ }
425
+ node.header = header;
426
+
427
+ for (const line of header) {
428
+ let value = line.split(':');
429
+ const key = (value.shift() || '').trim().toLowerCase();
430
+ value = value.join(':').trim();
431
+
432
+ // Do not touch headers that have strange looking keys, keep these
433
+ // only in the unparsed array
434
+ if (!FIELD_NAME.test(key)) {
435
+ continue;
436
+ }
437
+
438
+ if (key in node.parsedHeader) {
439
+ if (Array.isArray(node.parsedHeader[key])) {
440
+ node.parsedHeader[key].push(value);
441
+ } else {
442
+ node.parsedHeader[key] = [node.parsedHeader[key], value];
443
+ }
444
+ } else {
445
+ node.parsedHeader[key] = value.replace(/\s*\r?\n\s*/g, ' ');
446
+ }
447
+ }
448
+
449
+ // always ensure the presence of Content-Type. RFC 2046 5.1.5: inside a digest the default
450
+ // is message/rfc822 instead of text/plain
451
+ if (!node.parsedHeader['content-type']) {
452
+ const parentSubtype = ((node.parentNode && node.parentNode.multipart) || '').toString().toLowerCase();
453
+ node.parsedHeader['content-type'] = parentSubtype === 'digest' ? 'message/rfc822' : 'text/plain';
454
+ }
455
+
456
+ // parse additional params for Content-Type and Content-Disposition, the last header wins
457
+ ['content-type', 'content-disposition'].forEach(key => {
458
+ if (node.parsedHeader[key]) {
459
+ node.parsedHeader[key] = this.parseValueParams([].concat(node.parsedHeader[key]).pop());
460
+ }
461
+ });
462
+
463
+ // ensure single value for selected fields, the last header wins
464
+ [
465
+ 'subject',
466
+ 'date',
467
+ 'in-reply-to',
468
+ 'message-id',
469
+ 'content-transfer-encoding',
470
+ 'content-id',
471
+ 'content-description',
472
+ 'content-language',
473
+ 'content-md5',
474
+ 'content-location'
475
+ ].forEach(key => {
476
+ if (Array.isArray(node.parsedHeader[key])) {
477
+ node.parsedHeader[key] = node.parsedHeader[key].pop().replace(/\s*\r?\n\s*/g, ' ');
478
+ }
479
+ });
480
+
481
+ if (node.parsedHeader['content-transfer-encoding']) {
482
+ // RFC 2045 6.1: the mechanism token may be followed by a comment, which is not part of it
483
+ node.parsedHeader['content-transfer-encoding'] = splitStructuredValue(node.parsedHeader['content-transfer-encoding'])[0].trim();
484
+ }
485
+
486
+ // Parse address fields (join several fields with same key)
487
+ ['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].forEach(key => {
488
+ if (node.parsedHeader[key]) {
489
+ node.parsedHeader[key] = [].concat(node.parsedHeader[key]).flatMap(value => (value && addressparser(value.replace(/\s*\r?\n\s*/g, ' '))) || []);
490
+ }
491
+ });
492
+ }
493
+
494
+ /**
495
+ * Splits a value to an object.
496
+ * eg. 'text/plain; charset=utf-8' -> {value: 'text/plain', params:{charset: 'utf-8'}}
497
+ *
498
+ * @param {String} headerValue A string value for a header key
499
+ * @return {Object} Parsed value
500
+ */
501
+ parseValueParams(headerValue) {
502
+ const data = {
503
+ value: '',
504
+ type: '',
505
+ subtype: '',
506
+ params: Object.create(null)
507
+ };
508
+
509
+ // RFC 2231 continuations and extended values, by parameter name
510
+ const continuations = Object.create(null);
511
+
512
+ splitStructuredValue(headerValue || '', ';').forEach((part, i) => {
513
+ if (!i) {
514
+ data.value = part.trim();
515
+ const subtype = data.value.split('/');
516
+ data.type = (subtype.shift() || '').toLowerCase();
517
+ data.subtype = subtype.join('/');
518
+ return;
519
+ }
520
+
521
+ let value = part.split('=');
522
+ const key = (value.shift() || '').trim().toLowerCase();
523
+ value = value.join('=').trim();
524
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
525
+ // RFC 2045 5.1 quoted-string: the quotes are not part of the value and a backslash
526
+ // quotes the character after it (RFC 822 3.3)
527
+ value = value.slice(1, -1).replace(/\\([\s\S])/g, '$1');
528
+ } else {
529
+ value = value.replace(/^['"\s]*|['"\s]*$/g, '');
530
+ }
531
+
532
+ // Do not touch parameters that have strange looking keys
533
+ if (!FIELD_NAME.test(key)) {
534
+ return;
535
+ }
536
+
537
+ // RFC 2231 3 and 4: `name*N` is segment N of a continued value, a trailing asterisk marks
538
+ // a segment as extended (percent encoded, the first one prefixed with charset'language')
539
+ const match = key.match(/^([^*]+)(?:\*(\d{1,3}))?(\*)?$/);
540
+ if (match && (match[2] !== undefined || match[3])) {
541
+ const name = match[1];
542
+ if (!continuations[name]) {
543
+ continuations[name] = [];
544
+ }
545
+ continuations[name][Number(match[2]) || 0] = { value, extended: !!match[3] };
546
+ } else {
547
+ data.params[key] = value;
548
+ }
549
+ data.hasParams = true;
550
+ });
551
+
552
+ Object.keys(continuations).forEach(key => {
553
+ let charset = '';
554
+ let encoded = '';
555
+ let extended = false;
556
+
557
+ continuations[key].forEach((segment, i) => {
558
+ if (!segment) {
559
+ return;
560
+ }
561
+ let value = segment.value;
562
+ if (segment.extended && !i) {
563
+ const parts = value.split("'");
564
+ if (parts.length >= 3) {
565
+ charset = parts.shift();
566
+ parts.shift(); // language, ignored
567
+ value = parts.join("'");
568
+ }
569
+ }
570
+ if (segment.extended) {
571
+ extended = true;
572
+ encoded += value;
573
+ } else {
574
+ // a segment that is not extended is plain text, encode it like the rest
575
+ encoded += value.replace(/[^\x21-\x24\x26-\x7e]/g, chr => '%' + ('0' + chr.charCodeAt(0).toString(16)).substr(-2));
576
+ }
577
+ });
578
+
579
+ data.params[key] = extended ? decodeExtendedValue(charset, encoded) : decodeExtendedValue('', encoded);
580
+ });
581
+
582
+ return data;
583
+ }
584
+
585
+ /**
586
+ * Checks Content-Type value for the current tree node.
587
+ */
588
+ processContentType() {
589
+ const node = this._node;
590
+ // processNodeHeader() always sets a Content-Type
591
+ const contentType = node.parsedHeader['content-type'];
592
+
593
+ if (contentType.type === 'multipart' && contentType.params.boundary) {
594
+ node.multipart = contentType.subtype;
595
+ node.boundary = contentType.params.boundary;
596
+ // until the close delimiter is seen
597
+ node.unterminated = true;
598
+ }
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Parses a message into a MIME tree
604
+ *
605
+ * @param {String} rfc822 Message source as a binary string with CRLF line breaks
606
+ * @param {Number} [depth] Nesting level of message/rfc822 parts
607
+ * @return {Object} Root node of the tree
608
+ */
609
+ function parseTree(rfc822, depth) {
610
+ const parser = new MIMEParser(rfc822, depth);
611
+ parser.parse();
612
+ parser.finalizeTree();
613
+ return parser.tree.childNodes[0];
614
+ }
615
+
616
+ // RFC 3501 9: body-type-mpart = 1*body SP media-subtype. A multipart whose boundary never appeared has
617
+ // no parts, so this empty text part stands in for them
618
+ const PLACEHOLDER_PART = parseTree('Content-Type: text/plain\r\n\r\n');
619
+
620
+ /**
621
+ * The parts of a node as IMAP numbers them: the parts of a multipart (the placeholder when it has none),
622
+ * undefined for anything else
623
+ *
624
+ * @param {Object} node A tree node
625
+ * @returns {Array|undefined} Part nodes
626
+ */
627
+ function partsOf(node) {
628
+ if (node.childNodes) {
629
+ return node.childNodes;
630
+ }
631
+ return node.boundary ? [PLACEHOLDER_PART] : undefined;
632
+ }
633
+
634
+ /**
635
+ * The message encapsulated in a node: the message of a message/rfc822 part, and with `global` also of a
636
+ * message/global part (RFC 9051 sections 6.4.5.1 and 7.5.2, IMAP4rev1 treats message/global as a basic part)
637
+ *
638
+ * @param {Object} node A tree node
639
+ * @param {Boolean} [global] Treat message/global like message/rfc822
640
+ * @returns {Object|undefined} Root node of the encapsulated message
641
+ */
642
+ function embeddedMessage(node, global) {
643
+ return node.message || (global ? node.globalMessage : undefined);
644
+ }
645
+
646
+ /**
647
+ * Resolves a numeric part path to a node. RFC 3501 6.4.5: the parts of a multipart are numbered from 1,
648
+ * a non-multipart message has one part which is the message itself, and the parts of a message/rfc822
649
+ * part are numbered under it
650
+ *
651
+ * @param {Object} tree Root node
652
+ * @param {String} path Dot-separated numeric path
653
+ * @param {Boolean} [global] Number the parts of message/global parts too, like IMAP4rev2 does
654
+ * @return {Object|Boolean} Node, or false when there is no such part
655
+ */
656
+ function resolveNode(tree, path, global) {
657
+ // the message whose parts the next number counts
658
+ let scope = tree;
659
+ let node = tree;
660
+
661
+ for (const number of (path || '').toString().split('.')) {
662
+ const index = Number(number) - 1;
663
+ if (!scope || !(index >= 0)) {
664
+ return false;
665
+ }
666
+
667
+ const parts = partsOf(scope);
668
+ node = parts ? parts[index] : index === 0 ? scope : undefined;
669
+ if (!node) {
670
+ return false;
671
+ }
672
+
673
+ scope = embeddedMessage(node, global) || (partsOf(node) ? node : false);
674
+ }
675
+
676
+ return node;
677
+ }
678
+
679
+ /**
680
+ * The header of a node as the HEADER and MIME sections return it, with the blank line that ends it
681
+ *
682
+ * @param {Object} node Tree node
683
+ * @return {String} Header section
684
+ */
685
+ function headerSection(node) {
686
+ const header = node.header || [];
687
+ if (node.hasBody === false) {
688
+ // RFC 3501 7.4.2: the blank line is part of the header, except for a message with no body and no blank line
689
+ return header.length ? header.join('\r\n') + (node.headerUnterminated ? '' : '\r\n') : '';
690
+ }
691
+ return header.length ? header.join('\r\n') + '\r\n\r\n' : '\r\n';
692
+ }
693
+
694
+ /**
695
+ * Renders a node of the tree back to the octets it was parsed from
696
+ *
697
+ * @param {Object} node Tree node
698
+ * @param {Boolean} [textOnly] Leave out the header of the node (BODY[TEXT], BODY[n])
699
+ * @return {String} Binary string
700
+ */
701
+ function render(node, textOnly) {
702
+ const output = [];
703
+
704
+ const walk = (node, withHeader) => {
705
+ if (withHeader) {
706
+ const header = node.header || [];
707
+ if (header.length) {
708
+ output.push(header.join('\r\n') + (node.headerUnterminated ? '' : '\r\n'));
709
+ }
710
+ }
711
+
712
+ if (node.hasBody === false) {
713
+ return;
714
+ }
715
+
716
+ if (withHeader) {
717
+ // the blank line between header and body
718
+ output.push('\r\n');
719
+ }
720
+
721
+ output.push(node.body || '');
722
+
723
+ if (!node.boundary) {
724
+ return;
725
+ }
726
+
727
+ const children = node.childNodes || [];
728
+ for (let i = 0; i < children.length; i++) {
729
+ const child = children[i];
730
+ output.push('--' + node.boundary + (child.pad || '') + '\r\n');
731
+ walk(child, true);
732
+ if ((!node.unterminated || i < children.length - 1) && !child.bare) {
733
+ // the line break that belongs to the next delimiter
734
+ output.push('\r\n');
735
+ }
736
+ }
737
+
738
+ if (!node.unterminated) {
739
+ output.push('--' + node.boundary + '--' + (node.closePad || ''));
740
+ output.push(node.epilogue || '');
741
+ }
742
+ };
743
+
744
+ walk(node, !textOnly);
745
+ return output.join('');
746
+ }
747
+
748
+ // Parsed data of messages, so a message is parsed only once and not again until its source changes
749
+ const cache = new WeakMap();
750
+
751
+ /**
752
+ * Returns the source with CRLF line breaks and the MIME tree of a message
753
+ *
754
+ * @param {Object} message Message object with a `raw` property
755
+ * @return {Object} `{ raw, tree }`
756
+ */
757
+ function getMessageData(message) {
758
+ let cached = cache.get(message);
759
+ if (!cached || cached.source !== message.raw) {
760
+ const raw = normalizeLineBreaks(message.raw);
761
+ cached = { source: message.raw, raw, tree: parseTree(raw) };
762
+ cache.set(message, cached);
763
+ }
764
+ return cached;
765
+ }
766
+
767
+ module.exports = function (rfc822) {
768
+ return parseTree(normalizeLineBreaks(rfc822));
769
+ };
770
+ module.exports.parseTree = parseTree;
771
+ module.exports.IDENTITY_ENCODINGS = IDENTITY_ENCODINGS;
772
+ module.exports.getMessageData = getMessageData;
773
+ module.exports.normalizeLineBreaks = normalizeLineBreaks;
774
+ module.exports.partsOf = partsOf;
775
+ module.exports.resolveNode = resolveNode;
776
+ module.exports.embeddedMessage = embeddedMessage;
777
+ module.exports.headerSection = headerSection;
778
+ module.exports.render = render;