imapflow 2.2.6 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +42 -22
- package/dist/cjs/commands/close.d.ts +12 -1
- package/dist/cjs/commands/close.js +4 -2
- package/dist/cjs/commands/delete.js +2 -1
- package/dist/cjs/commands/enable.js +6 -0
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +57 -14
- package/dist/cjs/commands/id.js +8 -1
- package/dist/cjs/commands/idle.js +15 -5
- package/dist/cjs/commands/list.js +10 -1
- package/dist/cjs/commands/login.js +5 -1
- package/dist/cjs/commands/logout.js +7 -0
- package/dist/cjs/commands/namespace.js +7 -3
- package/dist/cjs/commands/quota.js +3 -1
- package/dist/cjs/commands/rename.js +2 -1
- package/dist/cjs/commands/select.js +5 -0
- package/dist/cjs/commands/status.js +6 -1
- package/dist/cjs/commands/store.d.ts +1 -1
- package/dist/cjs/commands/store.js +8 -8
- package/dist/cjs/download.js +220 -95
- package/dist/cjs/handler/imap-compiler.js +19 -10
- package/dist/cjs/handler/limits.d.ts +11 -0
- package/dist/cjs/handler/limits.js +16 -1
- package/dist/cjs/handler/parser-instance.d.ts +10 -0
- package/dist/cjs/handler/parser-instance.js +25 -10
- package/dist/cjs/handler/token-parser.js +36 -28
- package/dist/cjs/imap-flow.d.ts +2 -2
- package/dist/cjs/imap-flow.js +256 -95
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +7 -7
- package/dist/cjs/search-compiler.js +33 -13
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +14 -4
- package/dist/cjs/tools.js +69 -9
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +43 -23
- package/dist/esm/commands/close.d.ts +12 -1
- package/dist/esm/commands/close.js +5 -3
- package/dist/esm/commands/delete.js +2 -1
- package/dist/esm/commands/enable.js +6 -0
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +57 -14
- package/dist/esm/commands/id.js +9 -2
- package/dist/esm/commands/idle.js +16 -6
- package/dist/esm/commands/list.js +10 -1
- package/dist/esm/commands/login.js +6 -2
- package/dist/esm/commands/logout.js +7 -0
- package/dist/esm/commands/namespace.js +7 -3
- package/dist/esm/commands/quota.js +3 -1
- package/dist/esm/commands/rename.js +2 -1
- package/dist/esm/commands/select.js +5 -0
- package/dist/esm/commands/status.js +7 -2
- package/dist/esm/commands/store.d.ts +1 -1
- package/dist/esm/commands/store.js +9 -9
- package/dist/esm/download.js +220 -95
- package/dist/esm/handler/imap-compiler.js +19 -10
- package/dist/esm/handler/limits.d.ts +11 -0
- package/dist/esm/handler/limits.js +14 -0
- package/dist/esm/handler/parser-instance.d.ts +10 -0
- package/dist/esm/handler/parser-instance.js +25 -10
- package/dist/esm/handler/token-parser.js +37 -29
- package/dist/esm/imap-flow.d.ts +2 -2
- package/dist/esm/imap-flow.js +256 -95
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +7 -7
- package/dist/esm/search-compiler.js +33 -13
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +14 -4
- package/dist/esm/tools.js +68 -9
- package/dist/esm/types.d.ts +19 -4
- package/package.json +5 -3
|
@@ -234,11 +234,11 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
234
234
|
* @returns An IPv4 address.
|
|
235
235
|
*/
|
|
236
236
|
const resolveIPv4 = async (hostname, deadline) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
return
|
|
237
|
+
// lookup() goes through the system resolver (getaddrinfo), so hosts files and mDNS names
|
|
238
|
+
// resolve the same way they would for a direct connection. It rejects (ENOTFOUND) rather
|
|
239
|
+
// than returning nothing when the name has no IPv4 address
|
|
240
|
+
let { address } = await deadline.race(dns.promises.lookup(hostname, { family: 4 }));
|
|
241
|
+
return address;
|
|
242
242
|
};
|
|
243
243
|
/**
|
|
244
244
|
* Establishes a tunnel through a SOCKS proxy.
|
|
@@ -282,8 +282,8 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
282
282
|
set_tcp_nodelay: true
|
|
283
283
|
};
|
|
284
284
|
if (proxyUrl.username || proxyUrl.password) {
|
|
285
|
-
connectionOpts.proxy.userId = proxyUrl.username;
|
|
286
|
-
connectionOpts.proxy.password = proxyUrl.password;
|
|
285
|
+
connectionOpts.proxy.userId = decodeUserInfo(proxyUrl.username);
|
|
286
|
+
connectionOpts.proxy.password = decodeUserInfo(proxyUrl.password);
|
|
287
287
|
}
|
|
288
288
|
// The dependency treats a zero timeout as its own 30 second default, so only a strictly
|
|
289
289
|
// positive remaining budget may be passed.
|
|
@@ -50,17 +50,12 @@ let toSequenceValue = (value) => [].concat(value).join(',');
|
|
|
50
50
|
let toSearchValue = (value) => UNICODE_PATTERN.test(value) && !value.includes('\0') ? { type: 'LITERAL', value: Buffer.from(value) } : { type: 'ATOM', value };
|
|
51
51
|
/**
|
|
52
52
|
* Adds a search option with its value(s) to the attributes array.
|
|
53
|
-
* Handles NOT operations and array values.
|
|
54
53
|
*
|
|
55
54
|
* @param attributes - Array to append the attribute to
|
|
56
55
|
* @param term - The search term (e.g., 'FROM', 'SUBJECT')
|
|
57
|
-
* @param value - The value for the search term (string
|
|
56
|
+
* @param value - The value for the search term (string or array)
|
|
58
57
|
*/
|
|
59
58
|
let setOpt = (attributes, term, value) => {
|
|
60
|
-
// Handle NOT operations for false or null values
|
|
61
|
-
if (value === false || value === null) {
|
|
62
|
-
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
63
|
-
}
|
|
64
59
|
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
|
|
65
60
|
// Handle array values (e.g. HEADER name/value pairs)
|
|
66
61
|
if (Array.isArray(value)) {
|
|
@@ -250,6 +245,11 @@ export const searchCompiler = (connection, query) => {
|
|
|
250
245
|
break;
|
|
251
246
|
// Email ID support (OBJECTID or Gmail extension)
|
|
252
247
|
case 'EMAILID':
|
|
248
|
+
// A falsy value means no criterion, as for the text fields. It used to
|
|
249
|
+
// compile into NOT EMAILID "false", which matched every message
|
|
250
|
+
if (!params[term]) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
253
|
if (connection.capabilities.has('OBJECTID')) {
|
|
254
254
|
setOpt(attributes, 'EMAILID', params[term]);
|
|
255
255
|
}
|
|
@@ -257,7 +257,7 @@ export const searchCompiler = (connection, query) => {
|
|
|
257
257
|
// Fallback to Gmail message ID
|
|
258
258
|
setOpt(attributes, 'X-GM-MSGID', params[term]);
|
|
259
259
|
}
|
|
260
|
-
else
|
|
260
|
+
else {
|
|
261
261
|
// Dropping the criterion would widen the search to every message
|
|
262
262
|
// matching the rest of the query, which a delete or move acts on
|
|
263
263
|
fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for EMAILID');
|
|
@@ -265,6 +265,11 @@ export const searchCompiler = (connection, query) => {
|
|
|
265
265
|
break;
|
|
266
266
|
// Thread ID support (OBJECTID or Gmail extension)
|
|
267
267
|
case 'THREADID':
|
|
268
|
+
// A falsy value means no criterion, as for the text fields. It used to
|
|
269
|
+
// compile into NOT THREADID "false", which matched every message
|
|
270
|
+
if (!params[term]) {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
268
273
|
if (connection.capabilities.has('OBJECTID')) {
|
|
269
274
|
setOpt(attributes, 'THREADID', params[term]);
|
|
270
275
|
}
|
|
@@ -272,7 +277,7 @@ export const searchCompiler = (connection, query) => {
|
|
|
272
277
|
// Fallback to Gmail thread ID
|
|
273
278
|
setOpt(attributes, 'X-GM-THRID', params[term]);
|
|
274
279
|
}
|
|
275
|
-
else
|
|
280
|
+
else {
|
|
276
281
|
// Dropping the criterion would widen the search to every message
|
|
277
282
|
// matching the rest of the query, which a delete or move acts on
|
|
278
283
|
fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for THREADID');
|
|
@@ -335,11 +340,11 @@ export const searchCompiler = (connection, query) => {
|
|
|
335
340
|
if (!value) {
|
|
336
341
|
break;
|
|
337
342
|
}
|
|
338
|
-
// Use WITHIN extension for better timezone handling if available
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
// Use WITHIN extension for better timezone handling if available.
|
|
344
|
+
// The interval is an nz-number (RFC 5032), so a date that is not in the
|
|
345
|
+
// past (OLDER 0 would be answered with BAD) takes the date path instead
|
|
346
|
+
const withinSeconds = Math.round((Date.now() - value.getTime()) / 1000);
|
|
347
|
+
if (connection.capabilities.has('WITHIN') && withinSeconds >= 1) {
|
|
343
348
|
const withinKeyword = term.toUpperCase() === 'BEFORE' ? 'OLDER' : 'YOUNGER';
|
|
344
349
|
setOpt(attributes, withinKeyword, withinSeconds.toString());
|
|
345
350
|
break;
|
|
@@ -359,7 +364,18 @@ export const searchCompiler = (connection, query) => {
|
|
|
359
364
|
case 'KEYWORD':
|
|
360
365
|
case 'UNKEYWORD':
|
|
361
366
|
{
|
|
367
|
+
if (typeof params[term] !== 'string') {
|
|
368
|
+
fail('InvalidSearchQuery', `Search value for ${term.toLowerCase()} must be a string`);
|
|
369
|
+
}
|
|
362
370
|
let flag = formatFlag(params[term]);
|
|
371
|
+
// formatFlag() refuses \Recent, which is not a keyword, and values that are
|
|
372
|
+
// not atoms. Dropping the criterion would widen the search, so the query is
|
|
373
|
+
// refused instead
|
|
374
|
+
if (flag === false) {
|
|
375
|
+
fail('InvalidSearchQuery', /^\\recent$/i.test(params[term])
|
|
376
|
+
? `${params[term]} can not be searched as a keyword, use the "recent" search key instead`
|
|
377
|
+
: `${params[term]} is not a valid keyword`);
|
|
378
|
+
}
|
|
363
379
|
// Compiled even when the mailbox does not allow the keyword: the
|
|
364
380
|
// correct answer is then the empty set, which dropping the
|
|
365
381
|
// criterion would turn into every message matching the rest
|
|
@@ -457,6 +473,10 @@ export const searchCompiler = (connection, query) => {
|
|
|
457
473
|
walkOrTree(genOrTree(params[term]));
|
|
458
474
|
}
|
|
459
475
|
break;
|
|
476
|
+
default:
|
|
477
|
+
// An unknown key is refused: dropping it silently widened the search (a
|
|
478
|
+
// query of only unknown keys matched every message)
|
|
479
|
+
fail('InvalidSearchQuery', `Unknown search key "${term}"`);
|
|
460
480
|
}
|
|
461
481
|
});
|
|
462
482
|
};
|
package/dist/esm/special-use.js
CHANGED
|
@@ -74,6 +74,7 @@ const TOKEN_SPLIT = /[\s\-_/.,()[\]]+/;
|
|
|
74
74
|
// we fall back to matching folder names against these lists of known
|
|
75
75
|
// translations in various languages (including non-Latin scripts).
|
|
76
76
|
export const flags = ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash'];
|
|
77
|
+
const LOWERCASE_FLAGS = flags.map(flag => flag.toLowerCase());
|
|
77
78
|
export const names = {
|
|
78
79
|
'\\Sent': [
|
|
79
80
|
'aika',
|
|
@@ -860,9 +861,10 @@ for (let flag of Object.keys(names)) {
|
|
|
860
861
|
}
|
|
861
862
|
}
|
|
862
863
|
// Fold a folder name into the form the name tables are stored in.
|
|
863
|
-
// Remove U+200E (LEFT-TO-RIGHT MARK) which some mail clients
|
|
864
|
-
// RTL languages like Arabic, Hebrew) insert into folder names for display
|
|
865
|
-
// These invisible marks would otherwise prevent exact string matching.
|
|
864
|
+
// Remove U+200E and U+200F (LEFT-TO-RIGHT and RIGHT-TO-LEFT MARK) which some mail clients
|
|
865
|
+
// (especially for RTL languages like Arabic, Hebrew) insert into folder names for display
|
|
866
|
+
// purposes. These invisible marks would otherwise prevent exact string matching. U+200B
|
|
867
|
+
// (ZERO WIDTH SPACE) is kept, Khmer names in the tables use it as their word separator.
|
|
866
868
|
// Normalize with NFKC last: the same folder name can arrive in different but
|
|
867
869
|
// equivalent forms depending on the client that created it, and matching is exact.
|
|
868
870
|
// NFKC rather than NFC because the compatibility folding is what maps halfwidth
|
|
@@ -871,7 +873,7 @@ for (let flag of Object.keys(names)) {
|
|
|
871
873
|
function normalizeName(name) {
|
|
872
874
|
return name
|
|
873
875
|
.toLowerCase()
|
|
874
|
-
.replace(
|
|
876
|
+
.replace(/[\u200e\u200f]/g, '')
|
|
875
877
|
.trim()
|
|
876
878
|
.normalize('NFKC');
|
|
877
879
|
}
|
|
@@ -880,7 +882,10 @@ export const specialUse = (hasSpecialUseExtension, folder) => {
|
|
|
880
882
|
// Extension-provided flags take precedence over name-based detection because they
|
|
881
883
|
// are authoritative - the server explicitly marks the folder's role.
|
|
882
884
|
if (hasSpecialUseExtension) {
|
|
883
|
-
|
|
885
|
+
// Flags are atoms and compare case-insensitively (RFC 9051 section 9)
|
|
886
|
+
const listed = new Set();
|
|
887
|
+
folder.flags.forEach(flag => listed.add(flag.toLowerCase()));
|
|
888
|
+
const flag = flags.find((flag, i) => listed.has(LOWERCASE_FLAGS[i]));
|
|
884
889
|
if (flag) {
|
|
885
890
|
return { flag, source: 'extension' };
|
|
886
891
|
}
|
package/dist/esm/tools.d.ts
CHANGED
|
@@ -119,6 +119,14 @@ export declare function unrefTimer<T extends NodeJS.Timeout | null | undefined>(
|
|
|
119
119
|
* @param err - The error to log
|
|
120
120
|
*/
|
|
121
121
|
export declare function logConnectionError(connection: ImapFlow, msg: string, err: ImapFlowError | null | undefined): void;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a command failed because the server answered it with a tagged NO or BAD, as opposed
|
|
124
|
+
* to a lost connection, a timeout or a local failure that says nothing about the server's answer.
|
|
125
|
+
*
|
|
126
|
+
* @param err - The error the command failed with
|
|
127
|
+
* @returns True for a tagged NO or BAD
|
|
128
|
+
*/
|
|
129
|
+
export declare function isServerRefusal(err: ImapFlowError | null | undefined): boolean;
|
|
122
130
|
/**
|
|
123
131
|
* Checks whether IMAP4rev2 semantics are active for the connection: either the
|
|
124
132
|
* client enabled IMAP4rev2 explicitly, or the server is rev2-only (advertises
|
|
@@ -285,9 +293,10 @@ export declare function getColorFlags(color: string | null | undefined): {
|
|
|
285
293
|
* @param untagged - Parsed untagged IMAP response
|
|
286
294
|
* @param mailbox - Current mailbox state object
|
|
287
295
|
* @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
|
|
296
|
+
* @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
|
|
288
297
|
* @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
|
|
289
298
|
*/
|
|
290
|
-
export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string): Promise<FetchMessageObject>;
|
|
299
|
+
export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string, connection?: ImapFlow): Promise<FetchMessageObject>;
|
|
291
300
|
/**
|
|
292
301
|
* Strips surrounding double quotes from a name string.
|
|
293
302
|
*
|
|
@@ -356,8 +365,8 @@ export declare function formatDate(value: unknown): string | undefined;
|
|
|
356
365
|
*/
|
|
357
366
|
export declare function formatDateTime(value: unknown): string | undefined;
|
|
358
367
|
/**
|
|
359
|
-
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent)
|
|
360
|
-
* and capitalizes system flags properly.
|
|
368
|
+
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent) and for
|
|
369
|
+
* values that are not valid flags (keywords must be atoms), and capitalizes system flags properly.
|
|
361
370
|
*
|
|
362
371
|
* @param flag - Flag string to normalize
|
|
363
372
|
* @returns Normalized flag string, or false if the flag cannot be set
|
|
@@ -407,7 +416,8 @@ export declare function isUnsafeKey(key: unknown): boolean;
|
|
|
407
416
|
/**
|
|
408
417
|
* Reads a parsed attribute list of atoms or strings (a flag list, a capability list) into
|
|
409
418
|
* an array of strings. Any element can be a parsed NIL, and the list itself can be NIL,
|
|
410
|
-
* so both levels are guarded here rather than at each call site.
|
|
419
|
+
* so both levels are guarded here rather than at each call site. A literal (a Gmail label
|
|
420
|
+
* the server could not send quoted) arrives as a Buffer and is decoded as UTF-8.
|
|
411
421
|
*
|
|
412
422
|
* @param list - Parsed attribute list from a response.
|
|
413
423
|
* @returns The string values, in order, with unusable entries dropped.
|
package/dist/esm/tools.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import libmime from 'libmime';
|
|
3
3
|
import { resolveCharset } from './charsets.js';
|
|
4
4
|
import { compiler } from './handler/imap-handler.js';
|
|
5
|
+
import imapFormalSyntax from './handler/imap-formal-syntax.js';
|
|
5
6
|
import { createHash } from 'node:crypto';
|
|
6
7
|
import { JPDecoder } from './jp-decoder.js';
|
|
7
8
|
import iconv from 'iconv-lite';
|
|
@@ -211,6 +212,16 @@ export function logConnectionError(connection, msg, err) {
|
|
|
211
212
|
let routine = !!err && CONNECTION_GONE_CODES.has(err.code) && !err.reason;
|
|
212
213
|
connection.log[routine ? 'debug' : 'warn']({ msg, err, cid: connection.id });
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Whether a command failed because the server answered it with a tagged NO or BAD, as opposed
|
|
217
|
+
* to a lost connection, a timeout or a local failure that says nothing about the server's answer.
|
|
218
|
+
*
|
|
219
|
+
* @param err - The error the command failed with
|
|
220
|
+
* @returns True for a tagged NO or BAD
|
|
221
|
+
*/
|
|
222
|
+
export function isServerRefusal(err) {
|
|
223
|
+
return !!err && (err.responseStatus === 'NO' || err.responseStatus === 'BAD');
|
|
224
|
+
}
|
|
214
225
|
/**
|
|
215
226
|
* Checks whether IMAP4rev2 semantics are active for the connection: either the
|
|
216
227
|
* client enabled IMAP4rev2 explicitly, or the server is rev2-only (advertises
|
|
@@ -270,7 +281,8 @@ export function buildStatusQueryAttributes(connection, statusQuery) {
|
|
|
270
281
|
}
|
|
271
282
|
break;
|
|
272
283
|
case 'HIGHESTMODSEQ':
|
|
273
|
-
|
|
284
|
+
// QRESYNC implies CONDSTORE (RFC 7162 3.2.3)
|
|
285
|
+
if (connection.capabilities.has('CONDSTORE') || connection.capabilities.has('QRESYNC')) {
|
|
274
286
|
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
275
287
|
}
|
|
276
288
|
break;
|
|
@@ -538,7 +550,12 @@ export function getFolderTree(folders) {
|
|
|
538
550
|
}
|
|
539
551
|
return node;
|
|
540
552
|
};
|
|
541
|
-
|
|
553
|
+
// Parents are inserted before their children, as getTreeNode() can only descend into nodes
|
|
554
|
+
// that already exist. LIST gives no ordering guarantee, and the LIST command sorts special-use
|
|
555
|
+
// mailboxes first, which put a child such as "[Gmail]/Sent Mail" ahead of "[Gmail]" and left
|
|
556
|
+
// it at the root. The sort is stable, so siblings keep their listing order
|
|
557
|
+
let byDepth = [...folders].sort((a, b) => (a.parent ? a.parent.length : 0) - (b.parent ? b.parent.length : 0));
|
|
558
|
+
for (let folder of byDepth) {
|
|
542
559
|
let parent = getTreeNode(folder.parent);
|
|
543
560
|
// see if entry already exists
|
|
544
561
|
let existing = parent.folders && parent.folders.find(existing => existing.name === folder.name);
|
|
@@ -646,24 +663,40 @@ export function getColorFlags(color) {
|
|
|
646
663
|
* @param untagged - Parsed untagged IMAP response
|
|
647
664
|
* @param mailbox - Current mailbox state object
|
|
648
665
|
* @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
|
|
666
|
+
* @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
|
|
649
667
|
* @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
|
|
650
668
|
*/
|
|
651
|
-
export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
|
|
669
|
+
export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
|
|
652
670
|
let map = {};
|
|
653
671
|
// The sequence number indexes into mailbox state, so an unusable one is dropped rather
|
|
654
672
|
// than coerced to NaN or Infinity
|
|
655
673
|
map.seq = parseUintValue(untagged.command, MAX_UINT32_DIGITS) || undefined;
|
|
656
674
|
let key;
|
|
675
|
+
// the <origin> of a partial section ("BODY[2]<1024>"), kept for the value that follows
|
|
676
|
+
let origin = false;
|
|
677
|
+
let partialOrigins;
|
|
678
|
+
let recordOrigin = (sectionKey) => {
|
|
679
|
+
if (origin !== false) {
|
|
680
|
+
if (!partialOrigins) {
|
|
681
|
+
partialOrigins = new Map();
|
|
682
|
+
}
|
|
683
|
+
partialOrigins.set(sectionKey, origin);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
657
686
|
let attributes = ((untagged.attributes && untagged.attributes[1]) || []);
|
|
658
687
|
for (let i = 0, len = attributes.length; i < len; i++) {
|
|
659
688
|
let attribute = attributes[i];
|
|
660
689
|
if (i % 2 === 0) {
|
|
690
|
+
origin = false;
|
|
661
691
|
key = (await compiler({
|
|
662
692
|
attributes: [attribute]
|
|
663
693
|
}))
|
|
664
694
|
.toString()
|
|
665
695
|
.toLowerCase()
|
|
666
|
-
.replace(
|
|
696
|
+
.replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
|
|
697
|
+
origin = parseUintValue(start, MAX_UINT32_DIGITS);
|
|
698
|
+
return '';
|
|
699
|
+
});
|
|
667
700
|
continue;
|
|
668
701
|
}
|
|
669
702
|
/* c8 ignore start */ // defensive: key is always a string produced by the compiler above
|
|
@@ -708,6 +741,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
|
|
|
708
741
|
case 'body[]':
|
|
709
742
|
case 'binary[]':
|
|
710
743
|
map.source = getBuffer(attribute);
|
|
744
|
+
recordOrigin('');
|
|
711
745
|
break;
|
|
712
746
|
case 'uid':
|
|
713
747
|
// A UID feeds mailbox.uidNext one line below, and from there every range
|
|
@@ -754,7 +788,8 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
|
|
|
754
788
|
map.threadId = getString(attribute);
|
|
755
789
|
break;
|
|
756
790
|
case 'x-gm-labels':
|
|
757
|
-
|
|
791
|
+
// labels are mailbox names, modified UTF-7 unless UTF-8 is enabled
|
|
792
|
+
map.labels = new Set(getArray(attribute).map(label => (connection ? decodePath(connection, label) : label)));
|
|
758
793
|
break;
|
|
759
794
|
case 'rfc822.size':
|
|
760
795
|
map.size = getUint(attribute) || 0;
|
|
@@ -800,6 +835,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
|
|
|
800
835
|
map.bodyParts = new Map();
|
|
801
836
|
}
|
|
802
837
|
map.bodyParts.set(partKey, value);
|
|
838
|
+
recordOrigin(partKey);
|
|
803
839
|
if (match[1].toLowerCase() === 'binary') {
|
|
804
840
|
// The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
|
|
805
841
|
// into IMAP4rev2), so the server has already removed the
|
|
@@ -841,6 +877,10 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
|
|
|
841
877
|
.update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
|
|
842
878
|
.digest('hex');
|
|
843
879
|
}
|
|
880
|
+
if (partialOrigins) {
|
|
881
|
+
// non-enumerable, so it stays out of logged and serialized fetch results
|
|
882
|
+
Object.defineProperty(map, 'partialOrigins', { value: partialOrigins, writable: true, configurable: true });
|
|
883
|
+
}
|
|
844
884
|
if (map.flags) {
|
|
845
885
|
let flagColor = getFlagColor(map.flags);
|
|
846
886
|
if (flagColor) {
|
|
@@ -1283,14 +1323,22 @@ export function formatDateTime(value) {
|
|
|
1283
1323
|
let timeStr = date.toISOString().substring(11, 19);
|
|
1284
1324
|
return `${dateStr} ${timeStr} +0000`;
|
|
1285
1325
|
}
|
|
1326
|
+
// the memoized ATOM-CHAR set of RFC 9051 section 9
|
|
1327
|
+
const atomChars = imapFormalSyntax['ATOM-CHAR'];
|
|
1286
1328
|
/**
|
|
1287
|
-
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent)
|
|
1288
|
-
* and capitalizes system flags properly.
|
|
1329
|
+
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent) and for
|
|
1330
|
+
* values that are not valid flags (keywords must be atoms), and capitalizes system flags properly.
|
|
1289
1331
|
*
|
|
1290
1332
|
* @param flag - Flag string to normalize
|
|
1291
1333
|
* @returns Normalized flag string, or false if the flag cannot be set
|
|
1292
1334
|
*/
|
|
1293
1335
|
export function formatFlag(flag) {
|
|
1336
|
+
// RFC 9051 section 9: flag-keyword is an atom and flag-extension is "\\" atom, the same check
|
|
1337
|
+
// the compiler uses to decide what it can send unquoted
|
|
1338
|
+
let atom = flag.charAt(0) === '\\' ? flag.slice(1) : flag;
|
|
1339
|
+
if (!atom || imapFormalSyntax.verify(atom, atomChars()) >= 0) {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1294
1342
|
switch (flag.toLowerCase()) {
|
|
1295
1343
|
case '\\recent':
|
|
1296
1344
|
// can not set or remove
|
|
@@ -1357,7 +1405,8 @@ export function isUnsafeKey(key) {
|
|
|
1357
1405
|
/**
|
|
1358
1406
|
* Reads a parsed attribute list of atoms or strings (a flag list, a capability list) into
|
|
1359
1407
|
* an array of strings. Any element can be a parsed NIL, and the list itself can be NIL,
|
|
1360
|
-
* so both levels are guarded here rather than at each call site.
|
|
1408
|
+
* so both levels are guarded here rather than at each call site. A literal (a Gmail label
|
|
1409
|
+
* the server could not send quoted) arrives as a Buffer and is decoded as UTF-8.
|
|
1361
1410
|
*
|
|
1362
1411
|
* @param list - Parsed attribute list from a response.
|
|
1363
1412
|
* @returns The string values, in order, with unusable entries dropped.
|
|
@@ -1366,7 +1415,17 @@ export function getStringList(list) {
|
|
|
1366
1415
|
if (!Array.isArray(list)) {
|
|
1367
1416
|
return [];
|
|
1368
1417
|
}
|
|
1369
|
-
|
|
1418
|
+
let strings = [];
|
|
1419
|
+
for (let entry of list) {
|
|
1420
|
+
let value = entry && entry.value;
|
|
1421
|
+
if (Buffer.isBuffer(value)) {
|
|
1422
|
+
value = value.toString();
|
|
1423
|
+
}
|
|
1424
|
+
if (value && typeof value === 'string') {
|
|
1425
|
+
strings.push(value);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return strings;
|
|
1370
1429
|
}
|
|
1371
1430
|
/**
|
|
1372
1431
|
* Parses an untrusted decimal value from a server response into a BigInt.
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -155,7 +155,15 @@ export interface ImapFlowOptions {
|
|
|
155
155
|
* 30 minutes. Set to 0 or false to disable.
|
|
156
156
|
*/
|
|
157
157
|
maxLockHoldTime?: number | false | undefined;
|
|
158
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* STARTTLS policy for a connection that was not opened with `secure: true`. If true, the
|
|
160
|
+
* connection must upgrade to TLS, and connecting fails when the server does not offer
|
|
161
|
+
* STARTTLS. If false, the connection stays in cleartext. If not set, the connection upgrades
|
|
162
|
+
* when the server offers STARTTLS and otherwise continues in cleartext with a warning logged:
|
|
163
|
+
* the capability list that decides this was itself received in cleartext, so an attacker on
|
|
164
|
+
* the path can strip it. Set `doSTARTTLS: true` whenever the credentials must never cross the
|
|
165
|
+
* wire unencrypted.
|
|
166
|
+
*/
|
|
159
167
|
doSTARTTLS?: boolean | undefined;
|
|
160
168
|
/** Custom instance ID string for logs */
|
|
161
169
|
id?: string | undefined;
|
|
@@ -646,8 +654,8 @@ export interface DownloadOptions {
|
|
|
646
654
|
/** How large content parts to ask from the server. Defaults to 65536 */
|
|
647
655
|
chunkSize?: number | undefined;
|
|
648
656
|
}
|
|
649
|
-
/** Options for downloadMany()
|
|
650
|
-
export type DownloadManyOptions = DownloadOptions
|
|
657
|
+
/** Options for downloadMany(): the download() options without `chunkSize`, as the parts come in one FETCH */
|
|
658
|
+
export type DownloadManyOptions = Pick<DownloadOptions, 'uid' | 'maxBytes'>;
|
|
651
659
|
export interface DownloadManyPart {
|
|
652
660
|
meta: DownloadMeta;
|
|
653
661
|
content?: Buffer | null | undefined;
|
|
@@ -769,6 +777,13 @@ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
|
769
777
|
*/
|
|
770
778
|
export type InternalLogger = {
|
|
771
779
|
[level in LogLevel]: (obj: any) => void;
|
|
780
|
+
} & {
|
|
781
|
+
/**
|
|
782
|
+
* Whether an entry logged at this level reaches anyone (the logger, or a 'log' event
|
|
783
|
+
* listener). Set on the logger the connection builds; a logger assigned from outside may
|
|
784
|
+
* lack it, and every level then counts as enabled
|
|
785
|
+
*/
|
|
786
|
+
isLevelEnabled?: ((level: LogLevel) => boolean) | undefined;
|
|
772
787
|
};
|
|
773
788
|
export interface LogEvent {
|
|
774
789
|
/** Log level */
|
|
@@ -802,7 +817,7 @@ export interface ESearchResult {
|
|
|
802
817
|
partial?: {
|
|
803
818
|
/** The requested range, e.g. "1:100" */
|
|
804
819
|
range: string;
|
|
805
|
-
/** Matching UIDs in that range as compact sequence-set */
|
|
820
|
+
/** Matching UIDs in that range as compact sequence-set, an empty string when the range lies past the end of the results */
|
|
806
821
|
messages: string;
|
|
807
822
|
} | undefined;
|
|
808
823
|
/** Highest mod-sequence of the matching messages (RFC 7162, present when the search used a modseq criterion on a CONDSTORE session) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.8",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/imap-flow.js",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"lint": "npm run typecheck && eslint .",
|
|
52
52
|
"lint:fix": "eslint . --fix",
|
|
53
53
|
"prepare": "npm run build",
|
|
54
|
-
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
54
|
+
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
|
|
55
|
+
"test:james": "bash test/integration/run-james-tests.sh"
|
|
55
56
|
},
|
|
56
57
|
"repository": {
|
|
57
58
|
"type": "git",
|
|
@@ -74,12 +75,13 @@
|
|
|
74
75
|
"eslint": "10.12.0",
|
|
75
76
|
"eslint-config-prettier": "10.1.8",
|
|
76
77
|
"globals": "17.13.0",
|
|
78
|
+
"imapkit": "4.1.0",
|
|
77
79
|
"prettier": "3.9.9",
|
|
78
80
|
"tsx": "4.23.15",
|
|
79
81
|
"types-node-legacy": "npm:@types/node@20.0.0",
|
|
80
82
|
"typescript": "6.0.3",
|
|
81
83
|
"typescript-eslint": "8.71.1",
|
|
82
|
-
"wrangler": "4.
|
|
84
|
+
"wrangler": "4.148.0"
|
|
83
85
|
},
|
|
84
86
|
"dependencies": {
|
|
85
87
|
"@zone-eu/mailsplit": "5.4.20",
|