imapflow 2.2.7 → 2.2.9

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.
@@ -293,9 +293,10 @@ export declare function getColorFlags(color: string | null | undefined): {
293
293
  * @param untagged - Parsed untagged IMAP response
294
294
  * @param mailbox - Current mailbox state object
295
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
296
297
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
297
298
  */
298
- 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>;
299
300
  /**
300
301
  * Strips surrounding double quotes from a name string.
301
302
  *
@@ -364,8 +365,8 @@ export declare function formatDate(value: unknown): string | undefined;
364
365
  */
365
366
  export declare function formatDateTime(value: unknown): string | undefined;
366
367
  /**
367
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
368
- * 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.
369
370
  *
370
371
  * @param flag - Flag string to normalize
371
372
  * @returns Normalized flag string, or false if the flag cannot be set
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';
@@ -280,7 +281,8 @@ export function buildStatusQueryAttributes(connection, statusQuery) {
280
281
  }
281
282
  break;
282
283
  case 'HIGHESTMODSEQ':
283
- if (connection.capabilities.has('CONDSTORE')) {
284
+ // QRESYNC implies CONDSTORE (RFC 7162 3.2.3)
285
+ if (connection.capabilities.has('CONDSTORE') || connection.capabilities.has('QRESYNC')) {
284
286
  attributes.push({ type: 'ATOM', value: key.toUpperCase() });
285
287
  }
286
288
  break;
@@ -661,24 +663,40 @@ export function getColorFlags(color) {
661
663
  * @param untagged - Parsed untagged IMAP response
662
664
  * @param mailbox - Current mailbox state object
663
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
664
667
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
665
668
  */
666
- export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
669
+ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
667
670
  let map = {};
668
671
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
669
672
  // than coerced to NaN or Infinity
670
673
  map.seq = parseUintValue(untagged.command, MAX_UINT32_DIGITS) || undefined;
671
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
+ };
672
686
  let attributes = ((untagged.attributes && untagged.attributes[1]) || []);
673
687
  for (let i = 0, len = attributes.length; i < len; i++) {
674
688
  let attribute = attributes[i];
675
689
  if (i % 2 === 0) {
690
+ origin = false;
676
691
  key = (await compiler({
677
692
  attributes: [attribute]
678
693
  }))
679
694
  .toString()
680
695
  .toLowerCase()
681
- .replace(/<\d+(\.\d+)?>$/, '');
696
+ .replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
697
+ origin = parseUintValue(start, MAX_UINT32_DIGITS);
698
+ return '';
699
+ });
682
700
  continue;
683
701
  }
684
702
  /* c8 ignore start */ // defensive: key is always a string produced by the compiler above
@@ -723,6 +741,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
723
741
  case 'body[]':
724
742
  case 'binary[]':
725
743
  map.source = getBuffer(attribute);
744
+ recordOrigin('');
726
745
  break;
727
746
  case 'uid':
728
747
  // A UID feeds mailbox.uidNext one line below, and from there every range
@@ -769,7 +788,8 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
769
788
  map.threadId = getString(attribute);
770
789
  break;
771
790
  case 'x-gm-labels':
772
- map.labels = new Set(getArray(attribute));
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)));
773
793
  break;
774
794
  case 'rfc822.size':
775
795
  map.size = getUint(attribute) || 0;
@@ -815,6 +835,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
815
835
  map.bodyParts = new Map();
816
836
  }
817
837
  map.bodyParts.set(partKey, value);
838
+ recordOrigin(partKey);
818
839
  if (match[1].toLowerCase() === 'binary') {
819
840
  // The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
820
841
  // into IMAP4rev2), so the server has already removed the
@@ -856,6 +877,10 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
856
877
  .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
857
878
  .digest('hex');
858
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
+ }
859
884
  if (map.flags) {
860
885
  let flagColor = getFlagColor(map.flags);
861
886
  if (flagColor) {
@@ -1298,14 +1323,22 @@ export function formatDateTime(value) {
1298
1323
  let timeStr = date.toISOString().substring(11, 19);
1299
1324
  return `${dateStr} ${timeStr} +0000`;
1300
1325
  }
1326
+ // the memoized ATOM-CHAR set of RFC 9051 section 9
1327
+ const atomChars = imapFormalSyntax['ATOM-CHAR'];
1301
1328
  /**
1302
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
1303
- * 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.
1304
1331
  *
1305
1332
  * @param flag - Flag string to normalize
1306
1333
  * @returns Normalized flag string, or false if the flag cannot be set
1307
1334
  */
1308
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
+ }
1309
1342
  switch (flag.toLowerCase()) {
1310
1343
  case '\\recent':
1311
1344
  // can not set or remove
@@ -499,6 +499,8 @@ export interface FetchQueryObject {
499
499
  /** Include full message in the response, up to maxLength bytes */
500
500
  maxLength?: number | undefined;
501
501
  } | undefined;
502
+ /** Email ID (OBJECTID EMAILID or Gmail X-GM-MSGID) is always requested when the server supports either extension, so this is accepted but changes nothing */
503
+ emailId?: boolean | undefined;
502
504
  /** If true then include thread ID in the response (only if server supports either OBJECTID or X-GM-EXT-1 extensions) */
503
505
  threadId?: boolean | undefined;
504
506
  /** If true then include GMail labels in the response (only if server supports X-GM-EXT-1 extension) */
@@ -653,9 +655,11 @@ export interface DownloadOptions {
653
655
  maxBytes?: number | undefined;
654
656
  /** How large content parts to ask from the server. Defaults to 65536 */
655
657
  chunkSize?: number | undefined;
658
+ /** If true then requests the content with FETCH BINARY when the server supports it (BINARY or IMAP4rev2), so the server removes the transfer encoding */
659
+ binary?: boolean | undefined;
656
660
  }
657
661
  /** Options for downloadMany(): the download() options without `chunkSize`, as the parts come in one FETCH */
658
- export type DownloadManyOptions = Pick<DownloadOptions, 'uid' | 'maxBytes'>;
662
+ export type DownloadManyOptions = Pick<DownloadOptions, 'uid' | 'maxBytes' | 'binary'>;
659
663
  export interface DownloadManyPart {
660
664
  meta: DownloadMeta;
661
665
  content?: Buffer | null | undefined;
@@ -706,6 +710,10 @@ export interface MailboxOpenOptions {
706
710
  readOnly?: boolean | undefined;
707
711
  /** Optional description for mailbox lock tracking */
708
712
  description?: string | undefined;
713
+ /** QRESYNC (RFC 7162): HIGHESTMODSEQ from an earlier session. With `uidValidity` and QRESYNC enabled, changes since then are reported as `flags` and `expunge` events. getMailboxLock() only applies it when it selects the mailbox, not when the mailbox is already open */
714
+ changedSince?: bigint | number | string | undefined;
715
+ /** QRESYNC (RFC 7162): the UIDVALIDITY known from the previous session, required with `changedSince` */
716
+ uidValidity?: bigint | number | string | undefined;
709
717
  }
710
718
  export interface MailboxLockOptions extends MailboxOpenOptions {
711
719
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
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.147.0"
84
+ "wrangler": "4.148.0"
83
85
  },
84
86
  "dependencies": {
85
87
  "@zone-eu/mailsplit": "5.4.20",