imapflow 1.2.8 → 1.2.10
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/.ncurc.js +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +36 -58
- package/eslint.config.js +18 -16
- package/lib/charsets.js +15 -0
- package/lib/commands/append.js +62 -54
- package/lib/commands/authenticate.js +99 -52
- package/lib/commands/capability.js +12 -2
- package/lib/commands/close.js +11 -1
- package/lib/commands/compress.js +10 -1
- package/lib/commands/copy.js +18 -1
- package/lib/commands/create.js +15 -2
- package/lib/commands/delete.js +10 -1
- package/lib/commands/enable.js +12 -1
- package/lib/commands/expunge.js +18 -2
- package/lib/commands/fetch.js +39 -4
- package/lib/commands/id.js +22 -3
- package/lib/commands/idle.js +39 -4
- package/lib/commands/list.js +86 -48
- package/lib/commands/login.js +12 -1
- package/lib/commands/logout.js +11 -2
- package/lib/commands/move.js +17 -1
- package/lib/commands/namespace.js +32 -2
- package/lib/commands/noop.js +6 -1
- package/lib/commands/quota.js +33 -14
- package/lib/commands/rename.js +13 -1
- package/lib/commands/search.js +16 -1
- package/lib/commands/select.js +76 -33
- package/lib/commands/starttls.js +6 -1
- package/lib/commands/status.js +64 -52
- package/lib/commands/store.js +27 -4
- package/lib/commands/subscribe.js +7 -1
- package/lib/commands/unsubscribe.js +7 -1
- package/lib/handler/imap-compiler.js +44 -2
- package/lib/handler/imap-formal-syntax.js +51 -3
- package/lib/handler/imap-handler.js +8 -0
- package/lib/handler/imap-parser.js +23 -2
- package/lib/handler/imap-stream.js +84 -31
- package/lib/handler/parser-instance.js +61 -1
- package/lib/handler/token-parser.js +66 -9
- package/lib/imap-commands.js +11 -0
- package/lib/imap-flow.d.ts +6 -0
- package/lib/imap-flow.js +175 -46
- package/lib/jp-decoder.js +10 -0
- package/lib/limited-passthrough.js +12 -5
- package/lib/proxy-connection.js +18 -12
- package/lib/search-compiler.js +3 -11
- package/lib/special-use.js +23 -16
- package/lib/tools.js +218 -13
- package/package.json +5 -17
- package/test/commands-integration-test.js +33 -0
- package/test/connection-edge-cases-test.js +105 -0
- package/test/special-use-test.js +32 -0
- package/.babelrc +0 -6
- package/.eslintrc +0 -16
- package/assets/favicon.ico +0 -0
- package/jsdoc.json +0 -28
package/lib/tools.js
CHANGED
|
@@ -11,11 +11,21 @@ const iconv = require('iconv-lite');
|
|
|
11
11
|
|
|
12
12
|
const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Error subclass thrown when IMAP authentication fails.
|
|
16
|
+
*/
|
|
14
17
|
class AuthenticationFailure extends Error {
|
|
15
18
|
authenticationFailed = true;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
const tools = {
|
|
22
|
+
/**
|
|
23
|
+
* Encodes a mailbox path to modified UTF-7 if the server does not support UTF8=ACCEPT.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} connection - IMAP connection instance
|
|
26
|
+
* @param {String} path - Mailbox path to encode
|
|
27
|
+
* @returns {String} Encoded mailbox path
|
|
28
|
+
*/
|
|
19
29
|
encodePath(connection, path) {
|
|
20
30
|
path = (path || '').toString();
|
|
21
31
|
if (!connection.enabled.has('UTF8=ACCEPT') && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
|
|
@@ -28,6 +38,13 @@ const tools = {
|
|
|
28
38
|
return path;
|
|
29
39
|
},
|
|
30
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Decodes a mailbox path from modified UTF-7 if the server does not support UTF8=ACCEPT.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} connection - IMAP connection instance
|
|
45
|
+
* @param {String} path - Mailbox path to decode
|
|
46
|
+
* @returns {String} Decoded mailbox path
|
|
47
|
+
*/
|
|
31
48
|
decodePath(connection, path) {
|
|
32
49
|
path = (path || '').toString();
|
|
33
50
|
if (!connection.enabled.has('UTF8=ACCEPT') && /[&]/.test(path)) {
|
|
@@ -40,6 +57,15 @@ const tools = {
|
|
|
40
57
|
return path;
|
|
41
58
|
},
|
|
42
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Normalizes a mailbox path by joining array segments with the namespace delimiter,
|
|
62
|
+
* uppercasing INBOX, and prepending the namespace prefix if needed.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} connection - IMAP connection instance
|
|
65
|
+
* @param {String|String[]} path - Mailbox path or array of path segments
|
|
66
|
+
* @param {Boolean} [skipNamespace] - If true, skips prepending the namespace prefix
|
|
67
|
+
* @returns {String} Normalized mailbox path
|
|
68
|
+
*/
|
|
43
69
|
normalizePath(connection, path, skipNamespace) {
|
|
44
70
|
if (Array.isArray(path)) {
|
|
45
71
|
path = path.join((connection.namespace && connection.namespace.delimiter) || '');
|
|
@@ -58,6 +84,14 @@ const tools = {
|
|
|
58
84
|
return path;
|
|
59
85
|
},
|
|
60
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Compares two mailbox paths for equality after normalization.
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} connection - IMAP connection instance
|
|
91
|
+
* @param {String} a - First mailbox path
|
|
92
|
+
* @param {String} b - Second mailbox path
|
|
93
|
+
* @returns {Boolean} True if the paths are equal after normalization
|
|
94
|
+
*/
|
|
61
95
|
comparePaths(connection, a, b) {
|
|
62
96
|
if (!a || !b) {
|
|
63
97
|
return false;
|
|
@@ -65,6 +99,12 @@ const tools = {
|
|
|
65
99
|
return tools.normalizePath(connection, a) === tools.normalizePath(connection, b);
|
|
66
100
|
},
|
|
67
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Parses a capability response list into a Map of capability names to values.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array} list - Array of capability objects from IMAP response
|
|
106
|
+
* @returns {Map<string, boolean|number>} Map of capability names to `true` or numeric values
|
|
107
|
+
*/
|
|
68
108
|
updateCapabilities(list) {
|
|
69
109
|
let map = new Map();
|
|
70
110
|
|
|
@@ -96,6 +136,13 @@ const tools = {
|
|
|
96
136
|
|
|
97
137
|
AuthenticationFailure,
|
|
98
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Extracts the IMAP response status code (e.g. AUTHENTICATIONFAILED, NONEXISTENT)
|
|
141
|
+
* from a parsed server response.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} response - Parsed IMAP server response
|
|
144
|
+
* @returns {String|false} Uppercase status code string, or false if not found
|
|
145
|
+
*/
|
|
99
146
|
getStatusCode(response) {
|
|
100
147
|
return response &&
|
|
101
148
|
response.attributes &&
|
|
@@ -107,6 +154,12 @@ const tools = {
|
|
|
107
154
|
: false;
|
|
108
155
|
},
|
|
109
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Compiles an IMAP response object back into a human-readable string.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} response - Parsed IMAP server response
|
|
161
|
+
* @returns {Promise<String|false>} Compiled response text, or false if no response
|
|
162
|
+
*/
|
|
110
163
|
async getErrorText(response) {
|
|
111
164
|
if (!response) {
|
|
112
165
|
return false;
|
|
@@ -115,6 +168,12 @@ const tools = {
|
|
|
115
168
|
return (await compiler(response)).toString();
|
|
116
169
|
},
|
|
117
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Enhances an IMAP command error with the server response code and text.
|
|
173
|
+
*
|
|
174
|
+
* @param {Error} err - Error object with a `response` property
|
|
175
|
+
* @returns {Promise<Error>} The enhanced error with `serverResponseCode` and string `response`
|
|
176
|
+
*/
|
|
118
177
|
async enhanceCommandError(err) {
|
|
119
178
|
let errorCode = tools.getStatusCode(err.response);
|
|
120
179
|
if (errorCode) {
|
|
@@ -124,6 +183,12 @@ const tools = {
|
|
|
124
183
|
return err;
|
|
125
184
|
},
|
|
126
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Converts a flat list of mailbox folders into a tree structure.
|
|
188
|
+
*
|
|
189
|
+
* @param {Object[]} folders - Array of folder objects from LIST/LSUB response
|
|
190
|
+
* @returns {Object} Tree structure with a `root` flag and nested `folders` arrays
|
|
191
|
+
*/
|
|
127
192
|
getFolderTree(folders) {
|
|
128
193
|
let tree = {
|
|
129
194
|
root: true,
|
|
@@ -212,11 +277,21 @@ const tools = {
|
|
|
212
277
|
return tree;
|
|
213
278
|
},
|
|
214
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Derives a flag color name from a message's flags Set using Apple Mail color flag rules.
|
|
282
|
+
*
|
|
283
|
+
* @param {Set<string>} flags - Message flags Set
|
|
284
|
+
* @returns {String|null} Color name (e.g. 'red', 'orange') or null if not flagged
|
|
285
|
+
*/
|
|
215
286
|
getFlagColor(flags) {
|
|
216
287
|
if (!flags.has('\\Flagged')) {
|
|
217
288
|
return null;
|
|
218
289
|
}
|
|
219
290
|
|
|
291
|
+
// Apple Mail encodes flag colors as a 3-bit value using $MailFlagBit0/1/2 keywords.
|
|
292
|
+
// Bit 0 = 1, Bit 1 = 2, Bit 2 = 4. The resulting integer (0-6) indexes into FLAG_COLORS:
|
|
293
|
+
// 0=red, 1=orange, 2=yellow, 3=green, 4=blue, 5=purple, 6=grey.
|
|
294
|
+
// Value 7 (all bits set) is unused; defaults to red.
|
|
220
295
|
const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
|
|
221
296
|
const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
|
|
222
297
|
const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
|
|
@@ -226,19 +301,30 @@ const tools = {
|
|
|
226
301
|
return FLAG_COLORS[color] || 'red'; // default to red for the unused \b111
|
|
227
302
|
},
|
|
228
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Converts a color name to the corresponding flag add/remove operations for Apple Mail color flags.
|
|
306
|
+
*
|
|
307
|
+
* @param {String} color - Color name (e.g. 'red', 'orange', 'yellow')
|
|
308
|
+
* @returns {Object|null} Object with `add` and `remove` arrays of flag strings, or null if invalid color
|
|
309
|
+
*/
|
|
229
310
|
getColorFlags(color) {
|
|
311
|
+
// Reverse mapping from a color name to the Apple Mail $MailFlagBit0/1/2 flags.
|
|
312
|
+
// Returns an object with 'add' and 'remove' arrays so the caller can STORE +FLAGS/-FLAGS.
|
|
230
313
|
const colorCode = color ? FLAG_COLORS.indexOf((color || '').toString().toLowerCase().trim()) : null;
|
|
231
314
|
if (colorCode < 0 && colorCode !== null) {
|
|
232
315
|
return null;
|
|
233
316
|
}
|
|
234
317
|
|
|
318
|
+
// Decompose color index back into its 3-bit representation
|
|
235
319
|
const bits = [];
|
|
236
320
|
bits[0] = colorCode & 1; // eslint-disable-line no-bitwise
|
|
237
321
|
bits[1] = colorCode & 2; // eslint-disable-line no-bitwise
|
|
238
322
|
bits[2] = colorCode & 4; // eslint-disable-line no-bitwise
|
|
239
323
|
|
|
324
|
+
// If colorCode is truthy (non-zero), add \Flagged; if zero/null, remove \Flagged
|
|
240
325
|
let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };
|
|
241
326
|
|
|
327
|
+
// For each bit, add the corresponding $MailFlagBitN if set, remove it if unset
|
|
242
328
|
for (let i = 0; i < bits.length; i++) {
|
|
243
329
|
if (bits[i]) {
|
|
244
330
|
result.add.push(`$MailFlagBit${i}`);
|
|
@@ -249,6 +335,13 @@ const tools = {
|
|
|
249
335
|
return result;
|
|
250
336
|
},
|
|
251
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Formats a raw untagged FETCH response into a structured message object.
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} untagged - Parsed untagged IMAP response
|
|
342
|
+
* @param {Object} mailbox - Current mailbox state object
|
|
343
|
+
* @returns {Promise<Object>} Formatted message object with properties like seq, uid, flags, envelope, etc.
|
|
344
|
+
*/
|
|
252
345
|
async formatMessageResponse(untagged, mailbox) {
|
|
253
346
|
let map = {};
|
|
254
347
|
|
|
@@ -309,25 +402,33 @@ const tools = {
|
|
|
309
402
|
|
|
310
403
|
case 'uid':
|
|
311
404
|
map.uid = Number(getString(attribute));
|
|
405
|
+
// If the UID we just saw is >= the mailbox's uidNext, bump uidNext.
|
|
406
|
+
// This keeps the local uidNext estimate current without requiring a
|
|
407
|
+
// separate STATUS command, handling cases where new messages arrived
|
|
408
|
+
// since the last SELECT/EXAMINE.
|
|
312
409
|
if (map.uid && (!mailbox.uidNext || mailbox.uidNext <= map.uid)) {
|
|
313
|
-
// current uidNext seems to be outdated, bump it
|
|
314
410
|
mailbox.uidNext = map.uid + 1;
|
|
315
411
|
}
|
|
316
412
|
break;
|
|
317
413
|
|
|
318
414
|
case 'modseq':
|
|
319
415
|
map.modseq = BigInt(getArray(attribute)[0]);
|
|
416
|
+
// Similarly, keep the local highestModseq estimate up to date.
|
|
417
|
+
// This is critical for CONDSTORE/QRESYNC delta syncing.
|
|
320
418
|
if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
|
|
321
|
-
// current highestModseq seems to be outdated, bump it
|
|
322
419
|
mailbox.highestModseq = map.modseq;
|
|
323
420
|
}
|
|
324
421
|
break;
|
|
325
422
|
|
|
326
423
|
case 'emailid':
|
|
424
|
+
// OBJECTID extension (RFC 8474): server-assigned stable email identifier
|
|
327
425
|
map.emailId = getArray(attribute)[0];
|
|
328
426
|
break;
|
|
329
427
|
|
|
330
428
|
case 'x-gm-msgid':
|
|
429
|
+
// Gmail extension: X-GM-MSGID is Gmail's unique message ID.
|
|
430
|
+
// Mapped to the same emailId field as OBJECTID for a unified API,
|
|
431
|
+
// but this is a Gmail-specific numeric string, not an RFC 8474 ObjectID.
|
|
331
432
|
map.emailId = getString(attribute);
|
|
332
433
|
break;
|
|
333
434
|
|
|
@@ -423,6 +524,12 @@ const tools = {
|
|
|
423
524
|
return map;
|
|
424
525
|
},
|
|
425
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Strips surrounding double quotes from a name string.
|
|
529
|
+
*
|
|
530
|
+
* @param {String} name - Raw name string potentially wrapped in quotes
|
|
531
|
+
* @returns {String} Name with surrounding quotes removed
|
|
532
|
+
*/
|
|
426
533
|
processName(name) {
|
|
427
534
|
name = (name || '').toString();
|
|
428
535
|
if (name.length > 2 && name.at(0) === '"' && name.at(-1) === '"') {
|
|
@@ -431,6 +538,12 @@ const tools = {
|
|
|
431
538
|
return name;
|
|
432
539
|
},
|
|
433
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Parses a raw IMAP ENVELOPE response into a structured envelope object.
|
|
543
|
+
*
|
|
544
|
+
* @param {Array} entry - Raw envelope data array from IMAP response
|
|
545
|
+
* @returns {Object} Parsed envelope with date, subject, from, to, cc, bcc, messageId, etc.
|
|
546
|
+
*/
|
|
434
547
|
parseEnvelope(entry) {
|
|
435
548
|
let getStrValue = obj => {
|
|
436
549
|
if (!obj) {
|
|
@@ -510,11 +623,19 @@ const tools = {
|
|
|
510
623
|
return envelope;
|
|
511
624
|
},
|
|
512
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Parses structured MIME parameter arrays (including RFC 2231 continuations)
|
|
628
|
+
* into a flat key-value object.
|
|
629
|
+
*
|
|
630
|
+
* @param {Array} arr - Raw parameter array from BODYSTRUCTURE response
|
|
631
|
+
* @returns {Object} Key-value object of decoded parameters
|
|
632
|
+
*/
|
|
513
633
|
getStructuredParams(arr) {
|
|
514
634
|
let key;
|
|
515
635
|
|
|
516
636
|
let params = {};
|
|
517
637
|
|
|
638
|
+
// BODYSTRUCTURE parameters come as flat key/value pairs: [key1, val1, key2, val2, ...]
|
|
518
639
|
[].concat(arr || []).forEach((val, j) => {
|
|
519
640
|
if (j % 2) {
|
|
520
641
|
params[key] = libmime.decodeWords(((val && val.value) || '').toString());
|
|
@@ -523,6 +644,8 @@ const tools = {
|
|
|
523
644
|
}
|
|
524
645
|
});
|
|
525
646
|
|
|
647
|
+
// Detect RFC 2231 encoded filenames that were placed in the plain 'filename' param
|
|
648
|
+
// instead of 'filename*'. The pattern charset'language'encoded_value indicates encoding.
|
|
526
649
|
if (params.filename && !params['filename*'] && /^[a-z\-_0-9]+'[a-z]*'[^'\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]+/.test(params.filename)) {
|
|
527
650
|
// seems like encoded value
|
|
528
651
|
let [encoding, , encodedValue] = params.filename.split("'");
|
|
@@ -531,12 +654,15 @@ const tools = {
|
|
|
531
654
|
}
|
|
532
655
|
}
|
|
533
656
|
|
|
534
|
-
//
|
|
657
|
+
// RFC 2231 parameter continuations: parameters like filename*0, filename*1, etc.
|
|
658
|
+
// are split parts of a single value. Parameters ending with '*' contain charset info.
|
|
659
|
+
// This pass collects continuation parts and groups them by their base key name.
|
|
535
660
|
Object.keys(params).forEach(key => {
|
|
536
661
|
let actualKey;
|
|
537
662
|
let nr;
|
|
538
663
|
let value;
|
|
539
664
|
|
|
665
|
+
// Match keys ending with *N or *N* (where N is the continuation index)
|
|
540
666
|
let match = key.match(/\*((\d+)\*?)?$/);
|
|
541
667
|
|
|
542
668
|
if (!match) {
|
|
@@ -556,6 +682,7 @@ const tools = {
|
|
|
556
682
|
|
|
557
683
|
value = params[key];
|
|
558
684
|
|
|
685
|
+
// The first segment (*0*) may contain charset and language: charset'language'value
|
|
559
686
|
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
560
687
|
params[actualKey].charset = match[1] || 'utf-8';
|
|
561
688
|
value = match[2];
|
|
@@ -567,7 +694,9 @@ const tools = {
|
|
|
567
694
|
delete params[key];
|
|
568
695
|
});
|
|
569
696
|
|
|
570
|
-
//
|
|
697
|
+
// Reassemble split RFC 2231 strings by sorting continuation parts and joining them.
|
|
698
|
+
// For charset-encoded values, convert URL-encoded (%XX) sequences to MIME quoted-printable
|
|
699
|
+
// format (=?charset?Q?...?=) so libmime.decodeWords can decode them to Unicode.
|
|
571
700
|
Object.keys(params).forEach(key => {
|
|
572
701
|
let value;
|
|
573
702
|
if (params[key] && Array.isArray(params[key].values)) {
|
|
@@ -577,7 +706,10 @@ const tools = {
|
|
|
577
706
|
.join('');
|
|
578
707
|
|
|
579
708
|
if (params[key].charset) {
|
|
580
|
-
//
|
|
709
|
+
// Convert URL encoding (%AB) to MIME quoted-printable (=AB) by:
|
|
710
|
+
// 1. Escaping QP-special chars (=, ?, _, space) as %XX
|
|
711
|
+
// 2. Replacing all '%' with '=' to switch from URL encoding to QP encoding
|
|
712
|
+
// 3. Wrapping in =?charset?Q?...?= for libmime to decode
|
|
581
713
|
params[key] = libmime.decodeWords(
|
|
582
714
|
'=?' +
|
|
583
715
|
params[key].charset +
|
|
@@ -605,7 +737,16 @@ const tools = {
|
|
|
605
737
|
return params;
|
|
606
738
|
},
|
|
607
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Parses a raw IMAP BODYSTRUCTURE response into a structured tree of body parts.
|
|
742
|
+
*
|
|
743
|
+
* @param {Array} entry - Raw BODYSTRUCTURE data array from IMAP response
|
|
744
|
+
* @returns {Object} Parsed body structure tree with part numbers, types, parameters, and child nodes
|
|
745
|
+
*/
|
|
608
746
|
parseBodystructure(entry) {
|
|
747
|
+
// Recursively walks the BODYSTRUCTURE tree, building MIME part numbers.
|
|
748
|
+
// Part numbers follow the IMAP dot-notation: "1", "1.1", "2.3", etc.
|
|
749
|
+
// The root multipart has no part number; its children start at 1.
|
|
609
750
|
let walk = (node, path) => {
|
|
610
751
|
path = path || [];
|
|
611
752
|
|
|
@@ -613,13 +754,15 @@ const tools = {
|
|
|
613
754
|
i = 0,
|
|
614
755
|
part = 0;
|
|
615
756
|
|
|
757
|
+
// Build the dot-separated part number from the path array (e.g., [1,2] -> "1.2")
|
|
616
758
|
if (path.length) {
|
|
617
759
|
curNode.part = path.join('.');
|
|
618
760
|
}
|
|
619
761
|
|
|
620
|
-
// multipart
|
|
762
|
+
// multipart: first elements are arrays (child body parts), followed by the subtype string
|
|
621
763
|
if (Array.isArray(node[0])) {
|
|
622
764
|
curNode.childNodes = [];
|
|
765
|
+
// Each child array is a nested body part; increment part counter for each
|
|
623
766
|
while (Array.isArray(node[i])) {
|
|
624
767
|
curNode.childNodes.push(walk(node[i], path.concat(++part)));
|
|
625
768
|
i++;
|
|
@@ -672,9 +815,11 @@ const tools = {
|
|
|
672
815
|
i++;
|
|
673
816
|
|
|
674
817
|
if (curNode.type === 'message/rfc822') {
|
|
675
|
-
// message/
|
|
818
|
+
// message/rfc822 is special in IMAP BODYSTRUCTURE: after the standard
|
|
819
|
+
// 7 fields, it includes an embedded envelope, a nested bodystructure,
|
|
820
|
+
// and a line count for the encapsulated message.
|
|
676
821
|
|
|
677
|
-
// envelope
|
|
822
|
+
// envelope of the encapsulated message
|
|
678
823
|
if (node[i]) {
|
|
679
824
|
curNode.envelope = tools.parseEnvelope([].concat(node[i] || []));
|
|
680
825
|
}
|
|
@@ -682,9 +827,10 @@ const tools = {
|
|
|
682
827
|
|
|
683
828
|
if (node[i]) {
|
|
684
829
|
curNode.childNodes = [
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// path.
|
|
830
|
+
// The nested bodystructure reuses the same path (not path+1) because
|
|
831
|
+
// the encapsulated message shares the part number with its wrapper.
|
|
832
|
+
// Distinction is via suffixes: path.MIME = wrapper headers,
|
|
833
|
+
// path.HEADER = encapsulated message headers.
|
|
688
834
|
walk(node[i], path)
|
|
689
835
|
];
|
|
690
836
|
}
|
|
@@ -698,12 +844,13 @@ const tools = {
|
|
|
698
844
|
}
|
|
699
845
|
|
|
700
846
|
if (/^text\//.test(curNode.type)) {
|
|
701
|
-
// text/*
|
|
847
|
+
// Per RFC 3501, text/* parts include an additional line count field after size.
|
|
848
|
+
// However, some servers omit this field, producing 11 elements instead of 12+.
|
|
702
849
|
|
|
703
850
|
// NB! some less known servers do not include the line count value
|
|
704
851
|
// length should be 12+
|
|
705
852
|
if (node.length === 11 && Array.isArray(node[i + 1]) && !Array.isArray(node[i + 2])) {
|
|
706
|
-
// invalid structure, disposition params are shifted
|
|
853
|
+
// invalid structure, disposition params are shifted -- skip the line count
|
|
707
854
|
} else {
|
|
708
855
|
// correct structure, line count number is provided
|
|
709
856
|
if (node[i]) {
|
|
@@ -763,10 +910,22 @@ const tools = {
|
|
|
763
910
|
return walk(entry);
|
|
764
911
|
},
|
|
765
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Checks if a value is a Date object.
|
|
915
|
+
*
|
|
916
|
+
* @param {*} obj - Value to check
|
|
917
|
+
* @returns {Boolean} True if the value is a Date object
|
|
918
|
+
*/
|
|
766
919
|
isDate(obj) {
|
|
767
920
|
return Object.prototype.toString.call(obj) === '[object Date]';
|
|
768
921
|
},
|
|
769
922
|
|
|
923
|
+
/**
|
|
924
|
+
* Converts a value to a valid Date object, or returns null.
|
|
925
|
+
*
|
|
926
|
+
* @param {*} value - Date object or date string to convert
|
|
927
|
+
* @returns {Date|null} Valid Date object, or null if conversion fails
|
|
928
|
+
*/
|
|
770
929
|
toValidDate(value) {
|
|
771
930
|
if (!value) {
|
|
772
931
|
return null;
|
|
@@ -780,6 +939,12 @@ const tools = {
|
|
|
780
939
|
return value;
|
|
781
940
|
},
|
|
782
941
|
|
|
942
|
+
/**
|
|
943
|
+
* Formats a date value into IMAP date format (DD-Mon-YYYY).
|
|
944
|
+
*
|
|
945
|
+
* @param {Date|String} value - Date to format
|
|
946
|
+
* @returns {String|undefined} Formatted date string, or undefined if invalid
|
|
947
|
+
*/
|
|
783
948
|
formatDate(value) {
|
|
784
949
|
value = tools.toValidDate(value);
|
|
785
950
|
if (!value) {
|
|
@@ -795,6 +960,12 @@ const tools = {
|
|
|
795
960
|
return dateParts.join('-');
|
|
796
961
|
},
|
|
797
962
|
|
|
963
|
+
/**
|
|
964
|
+
* Formats a date value into IMAP date-time format (DD-Mon-YYYY HH:MM:SS +0000).
|
|
965
|
+
*
|
|
966
|
+
* @param {Date|String} value - Date to format
|
|
967
|
+
* @returns {String|undefined} Formatted date-time string, or undefined if invalid
|
|
968
|
+
*/
|
|
798
969
|
formatDateTime(value) {
|
|
799
970
|
value = tools.toValidDate(value);
|
|
800
971
|
if (!value) {
|
|
@@ -807,6 +978,13 @@ const tools = {
|
|
|
807
978
|
return `${dateStr} ${timeStr} +0000`;
|
|
808
979
|
},
|
|
809
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
|
|
983
|
+
* and capitalizes system flags properly.
|
|
984
|
+
*
|
|
985
|
+
* @param {String} flag - Flag string to normalize
|
|
986
|
+
* @returns {String|false} Normalized flag string, or false if the flag cannot be set
|
|
987
|
+
*/
|
|
810
988
|
formatFlag(flag) {
|
|
811
989
|
switch (flag.toLowerCase()) {
|
|
812
990
|
case '\\recent':
|
|
@@ -823,10 +1001,23 @@ const tools = {
|
|
|
823
1001
|
return flag;
|
|
824
1002
|
},
|
|
825
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* Checks if a flag can be used in the given mailbox based on permanent flags.
|
|
1006
|
+
*
|
|
1007
|
+
* @param {Object} mailbox - Mailbox object with permanentFlags
|
|
1008
|
+
* @param {String} flag - Flag to check
|
|
1009
|
+
* @returns {Boolean} True if the flag is allowed
|
|
1010
|
+
*/
|
|
826
1011
|
canUseFlag(mailbox, flag) {
|
|
827
1012
|
return !mailbox || !mailbox.permanentFlags || mailbox.permanentFlags.has('\\*') || mailbox.permanentFlags.has(flag);
|
|
828
1013
|
},
|
|
829
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {String} range - IMAP sequence range string
|
|
1019
|
+
* @returns {Number[]} Array of expanded sequence numbers
|
|
1020
|
+
*/
|
|
830
1021
|
expandRange(range) {
|
|
831
1022
|
return range.split(',').flatMap(entry => {
|
|
832
1023
|
entry = entry.trim();
|
|
@@ -853,6 +1044,13 @@ const tools = {
|
|
|
853
1044
|
});
|
|
854
1045
|
},
|
|
855
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
1049
|
+
* charset decoder for JIS/ISO-2022-JP, otherwise delegates to iconv-lite.
|
|
1050
|
+
*
|
|
1051
|
+
* @param {String} [charset='ascii'] - Character set name
|
|
1052
|
+
* @returns {Object} A stream decoder (Transform stream) for the charset
|
|
1053
|
+
*/
|
|
856
1054
|
getDecoder(charset) {
|
|
857
1055
|
charset = (charset || 'ascii').toString().trim().toLowerCase();
|
|
858
1056
|
if (/^jis|^iso-?2022-?jp|^EUCJP/i.test(charset)) {
|
|
@@ -863,6 +1061,13 @@ const tools = {
|
|
|
863
1061
|
return iconv.decodeStream(charset);
|
|
864
1062
|
},
|
|
865
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Packs an array of message sequence numbers into a compact IMAP range string
|
|
1066
|
+
* (e.g. [1,2,3,5,7,8] becomes "1:3,5,7:8").
|
|
1067
|
+
*
|
|
1068
|
+
* @param {Number|Number[]} list - Sequence number or array of sequence numbers
|
|
1069
|
+
* @returns {String} Packed IMAP sequence range string
|
|
1070
|
+
*/
|
|
866
1071
|
packMessageRange(list) {
|
|
867
1072
|
if (!Array.isArray(list)) {
|
|
868
1073
|
list = [].concat(list || []);
|
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "grunt",
|
|
9
9
|
"coverage": "c8 --reporter=text --reporter=html npx nodeunit test/*-test.js",
|
|
10
|
-
"prepare": "npm run build",
|
|
11
|
-
"docs": "rm -rf docs && mkdir -p docs && jsdoc lib/imap-flow.js -c jsdoc.json -R README.md --destination docs/ && cp assets/favicon.ico docs",
|
|
12
|
-
"build": "npm run docs",
|
|
13
|
-
"st": "npm run docs && st -d docs -i index.html",
|
|
14
10
|
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
|
|
15
11
|
"format": "prettier --write \"**/*.{js,json,md,yml,yaml}\" --ignore-path .prettierignore",
|
|
16
12
|
"lint": "eslint ."
|
|
@@ -31,13 +27,8 @@
|
|
|
31
27
|
},
|
|
32
28
|
"homepage": "https://imapflow.com/",
|
|
33
29
|
"devDependencies": {
|
|
34
|
-
"@babel/eslint-parser": "7.28.6",
|
|
35
|
-
"@babel/eslint-plugin": "7.27.1",
|
|
36
|
-
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
37
|
-
"@babel/preset-env": "7.28.6",
|
|
38
|
-
"@eslint/eslintrc": "3.3.3",
|
|
39
30
|
"@eslint/js": "9.39.2",
|
|
40
|
-
"@types/node": "25.
|
|
31
|
+
"@types/node": "25.3.0",
|
|
41
32
|
"c8": "10.1.3",
|
|
42
33
|
"eslint": "9.39.2",
|
|
43
34
|
"eslint-config-nodemailer": "1.2.0",
|
|
@@ -45,12 +36,9 @@
|
|
|
45
36
|
"grunt": "1.6.1",
|
|
46
37
|
"grunt-cli": "1.5.0",
|
|
47
38
|
"grunt-contrib-nodeunit": "5.0.0",
|
|
48
|
-
"grunt-eslint": "
|
|
49
|
-
"imapflow-jsdoc-template": "3.4.0-imapflow.3",
|
|
50
|
-
"jsdoc": "4.0.4",
|
|
39
|
+
"grunt-eslint": "26.0.0",
|
|
51
40
|
"prettier": "3.8.1",
|
|
52
41
|
"proxyquire": "^2.1.3",
|
|
53
|
-
"st": "3.0.3",
|
|
54
42
|
"typescript": "5.9.3"
|
|
55
43
|
},
|
|
56
44
|
"dependencies": {
|
|
@@ -60,8 +48,8 @@
|
|
|
60
48
|
"libbase64": "1.3.0",
|
|
61
49
|
"libmime": "5.3.7",
|
|
62
50
|
"libqp": "2.1.1",
|
|
63
|
-
"nodemailer": "
|
|
64
|
-
"pino": "10.3.
|
|
51
|
+
"nodemailer": "8.0.1",
|
|
52
|
+
"pino": "10.3.1",
|
|
65
53
|
"socks": "2.8.7"
|
|
66
54
|
}
|
|
67
55
|
}
|
|
@@ -3858,6 +3858,39 @@ module.exports['Commands: list sort fallback path comparison'] = async test => {
|
|
|
3858
3858
|
test.done();
|
|
3859
3859
|
};
|
|
3860
3860
|
|
|
3861
|
+
module.exports['Commands: list STATUS handles unknown key in response'] = async test => {
|
|
3862
|
+
const connection = createMockConnection({
|
|
3863
|
+
state: 3,
|
|
3864
|
+
capabilities: new Map([
|
|
3865
|
+
['SPECIAL-USE', true],
|
|
3866
|
+
['LIST-STATUS', true]
|
|
3867
|
+
]),
|
|
3868
|
+
exec: async (cmd, attrs, opts) => {
|
|
3869
|
+
if (cmd === 'LIST' && opts && opts.untagged) {
|
|
3870
|
+
if (opts.untagged.LIST) {
|
|
3871
|
+
await opts.untagged.LIST({
|
|
3872
|
+
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'TestFolder' }]
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
if (opts.untagged.STATUS) {
|
|
3876
|
+
await opts.untagged.STATUS({
|
|
3877
|
+
attributes: [{ value: 'TestFolder' }, [{ value: 'XUNKNOWN' }, { value: '999' }, { value: 'MESSAGES' }, { value: '10' }]]
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return { next: () => {} };
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
|
|
3885
|
+
const result = await listCommand(connection, '', '*', { statusQuery: { messages: true } });
|
|
3886
|
+
const folder = result.find(e => e.path === 'TestFolder');
|
|
3887
|
+
test.ok(folder);
|
|
3888
|
+
test.ok(folder.status);
|
|
3889
|
+
test.equal(folder.status.messages, 10);
|
|
3890
|
+
test.equal(folder.status.XUNKNOWN, undefined); // Unknown keys silently ignored
|
|
3891
|
+
test.done();
|
|
3892
|
+
};
|
|
3893
|
+
|
|
3861
3894
|
// ============================================
|
|
3862
3895
|
// SELECT Command Tests
|
|
3863
3896
|
// ============================================
|