imapflow 1.3.7 → 1.4.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.3.7"
2
+ ".": "1.4.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0](https://github.com/postalsys/imapflow/compare/v1.3.7...v1.4.0) (2026-06-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * add Gmail label search term to the search compiler ([4b2d173](https://github.com/postalsys/imapflow/commit/4b2d1736660441e95cd55a30ad6632a676fb605c))
9
+
3
10
  ## [1.3.7](https://github.com/postalsys/imapflow/compare/v1.3.6...v1.3.7) (2026-06-08)
4
11
 
5
12
 
@@ -357,6 +357,8 @@ export interface SearchObject {
357
357
  gmraw?: string;
358
358
  /** Gmail raw search query (alias for gmraw) */
359
359
  gmailraw?: string;
360
+ /** Gmail label filter (only for Gmail). Compiles to an X-GM-RAW "label:"/"-label:" query. "has" matches messages carrying all listed labels, "not" excludes messages carrying any listed label */
361
+ labels?: { has?: string[]; not?: string[] };
360
362
  }
361
363
 
362
364
  export interface FetchQueryObject {
@@ -373,12 +375,14 @@ export interface FetchQueryObject {
373
375
  /** If true then include message size in the response */
374
376
  size?: boolean;
375
377
  /** If true then include full message in the response */
376
- source?: boolean | {
377
- /** Include full message in the response starting from start byte */
378
- start?: number;
379
- /** Include full message in the response, up to maxLength bytes */
380
- maxLength?: number;
381
- };
378
+ source?:
379
+ | boolean
380
+ | {
381
+ /** Include full message in the response starting from start byte */
382
+ start?: number;
383
+ /** Include full message in the response, up to maxLength bytes */
384
+ maxLength?: number;
385
+ };
382
386
  /** If true then include thread ID in the response (only if server supports either OBJECTID or X-GM-EXT-1 extensions) */
383
387
  threadId?: boolean;
384
388
  /** If true then include GMail labels in the response (only if server supports X-GM-EXT-1 extension) */
@@ -740,14 +744,17 @@ export class ImapFlow extends EventEmitter {
740
744
  mailboxClose(): Promise<boolean>;
741
745
 
742
746
  /** Requests the status of the indicated mailbox */
743
- status(path: string, query: {
744
- messages?: boolean;
745
- recent?: boolean;
746
- uidNext?: boolean;
747
- uidValidity?: boolean;
748
- unseen?: boolean;
749
- highestModseq?: boolean;
750
- }): Promise<StatusObject>;
747
+ status(
748
+ path: string,
749
+ query: {
750
+ messages?: boolean;
751
+ recent?: boolean;
752
+ uidNext?: boolean;
753
+ uidValidity?: boolean;
754
+ unseen?: boolean;
755
+ highestModseq?: boolean;
756
+ }
757
+ ): Promise<StatusObject>;
751
758
 
752
759
  /** Starts listening for new or deleted messages from the currently opened mailbox */
753
760
  idle(): Promise<boolean>;
@@ -779,10 +786,13 @@ export class ImapFlow extends EventEmitter {
779
786
  /** Search messages from the currently opened mailbox — returns number[] (backward-compatible) */
780
787
  search(query: SearchObject, options?: { uid?: boolean }): Promise<number[] | false>;
781
788
  /** Search messages with ESEARCH RETURN options — returns ESearchResult */
782
- search(query: SearchObject, options: {
783
- uid?: boolean;
784
- returnOptions: Array<'MIN' | 'MAX' | 'COUNT' | 'ALL' | { partial: string }>;
785
- }): Promise<ESearchResult | number[] | false>;
789
+ search(
790
+ query: SearchObject,
791
+ options: {
792
+ uid?: boolean;
793
+ returnOptions: Array<'MIN' | 'MAX' | 'COUNT' | 'ALL' | { partial: string }>;
794
+ }
795
+ ): Promise<ESearchResult | number[] | false>;
786
796
 
787
797
  /** Fetch messages from the currently opened mailbox */
788
798
  fetch(range: SequenceString | number[] | SearchObject, query: FetchQueryObject, options?: FetchOptions): AsyncIterableIterator<FetchMessageObject>;
@@ -794,14 +804,22 @@ export class ImapFlow extends EventEmitter {
794
804
  fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions): Promise<FetchMessageObject | false>;
795
805
 
796
806
  /** Download either full rfc822 formatted message or a specific bodystructure part as a Stream */
797
- download(range: SequenceString, part?: string, options?: {
798
- uid?: boolean;
799
- maxBytes?: number;
800
- chunkSize?: number;
801
- }): Promise<DownloadObject>;
807
+ download(
808
+ range: SequenceString,
809
+ part?: string,
810
+ options?: {
811
+ uid?: boolean;
812
+ maxBytes?: number;
813
+ chunkSize?: number;
814
+ }
815
+ ): Promise<DownloadObject>;
802
816
 
803
817
  /** Fetch multiple attachments as Buffer values */
804
- downloadMany(range: SequenceString, parts: string[], options?: { uid?: boolean }): Promise<{
818
+ downloadMany(
819
+ range: SequenceString,
820
+ parts: string[],
821
+ options?: { uid?: boolean }
822
+ ): Promise<{
805
823
  [part: string]: {
806
824
  meta: {
807
825
  contentType?: string;
@@ -811,7 +829,7 @@ export class ImapFlow extends EventEmitter {
811
829
  encoding?: string;
812
830
  };
813
831
  content: Buffer | null;
814
- }
832
+ };
815
833
  }>;
816
834
 
817
835
  /** Opens a mailbox if not already open and returns a lock */
@@ -849,4 +867,4 @@ export class ImapFlow extends EventEmitter {
849
867
 
850
868
  /** Response event */
851
869
  on(event: 'response', listener: (response: ResponseEvent) => void): this;
852
- }
870
+ }
@@ -261,6 +261,54 @@ module.exports.searchCompiler = (connection, query) => {
261
261
  }
262
262
  break;
263
263
 
264
+ // Gmail label search. Compiles { has, not } into an X-GM-RAW "label:"/"-label:" query
265
+ // since Gmail labels are not a native IMAP SEARCH key. Gmail-only (X-GM-EXT-1).
266
+ case 'LABELS': {
267
+ let labelQuery = params[term];
268
+ if (!labelQuery || typeof labelQuery !== 'object') {
269
+ break;
270
+ }
271
+
272
+ // Collapse whitespace/quotes and quote multi-word names so they survive as a single token
273
+ let formatLabel = name => {
274
+ name = (name || '')
275
+ .toString()
276
+ .replace(/[\s"]+/g, ' ')
277
+ .trim();
278
+ return name.indexOf(' ') >= 0 ? `"${name}"` : name;
279
+ };
280
+
281
+ let rawParts = [];
282
+ for (let name of [].concat(labelQuery.has || [])) {
283
+ if (name) {
284
+ rawParts.push(`label:${formatLabel(name)}`);
285
+ }
286
+ }
287
+ for (let name of [].concat(labelQuery.not || [])) {
288
+ if (name) {
289
+ rawParts.push(`-label:${formatLabel(name)}`);
290
+ }
291
+ }
292
+
293
+ // Empty filter is a no-op on any server (do not require the extension)
294
+ if (!rawParts.length) {
295
+ break;
296
+ }
297
+
298
+ if (!connection.capabilities.has('X-GM-EXT-1')) {
299
+ let error = new Error('Server does not support X-GM-EXT-1 extension required for label search');
300
+ error.code = 'MissingServerExtension';
301
+ throw error;
302
+ }
303
+
304
+ let rawQuery = rawParts.join(' ');
305
+ if (isUnicodeString(rawQuery)) {
306
+ hasUnicode = true;
307
+ }
308
+ setOpt(attributes, 'X-GM-RAW', rawQuery);
309
+ break;
310
+ }
311
+
264
312
  // Date searches with WITHIN extension support
265
313
  case 'BEFORE':
266
314
  case 'SINCE':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.3.7",
3
+ "version": "1.4.0",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -443,6 +443,86 @@ module.exports['Search Compiler: GMRAW throws without capability'] = test => {
443
443
  test.done();
444
444
  };
445
445
 
446
+ // ============================================
447
+ // Gmail label search tests
448
+ // ============================================
449
+
450
+ module.exports['Search Compiler: LABELS has compiles to label: via X-GM-RAW'] = test => {
451
+ let connection = createMockConnection({
452
+ capabilities: [['X-GM-EXT-1', true]]
453
+ });
454
+ let compiled = searchCompiler(connection, { labels: { has: ['Horizon'] } });
455
+
456
+ test.ok(hasAttr(compiled, 'X-GM-RAW'));
457
+ test.ok(hasAttr(compiled, 'label:Horizon'));
458
+ test.done();
459
+ };
460
+
461
+ module.exports['Search Compiler: LABELS not compiles to -label: via X-GM-RAW'] = test => {
462
+ let connection = createMockConnection({
463
+ capabilities: [['X-GM-EXT-1', true]]
464
+ });
465
+ let compiled = searchCompiler(connection, { labels: { not: ['Horizon'] } });
466
+
467
+ test.ok(hasAttr(compiled, 'X-GM-RAW'));
468
+ test.ok(hasAttr(compiled, '-label:Horizon'));
469
+ test.done();
470
+ };
471
+
472
+ module.exports['Search Compiler: LABELS has and not combined'] = test => {
473
+ let connection = createMockConnection({
474
+ capabilities: [['X-GM-EXT-1', true]]
475
+ });
476
+ let compiled = searchCompiler(connection, { labels: { has: ['Imported'], not: ['Horizon'] } });
477
+
478
+ test.ok(hasAttr(compiled, 'label:Imported -label:Horizon'));
479
+ test.done();
480
+ };
481
+
482
+ module.exports['Search Compiler: LABELS quotes multi-word names'] = test => {
483
+ let connection = createMockConnection({
484
+ capabilities: [['X-GM-EXT-1', true]]
485
+ });
486
+ let compiled = searchCompiler(connection, { labels: { has: ['Some Label'] } });
487
+
488
+ test.ok(hasAttr(compiled, 'label:"Some Label"'));
489
+ test.done();
490
+ };
491
+
492
+ module.exports['Search Compiler: LABELS coexists with gmraw'] = test => {
493
+ let connection = createMockConnection({
494
+ capabilities: [['X-GM-EXT-1', true]]
495
+ });
496
+ let compiled = searchCompiler(connection, { gmraw: 'has:attachment', labels: { not: ['Horizon'] } });
497
+
498
+ test.ok(hasAttr(compiled, 'has:attachment'));
499
+ test.ok(hasAttr(compiled, '-label:Horizon'));
500
+ test.done();
501
+ };
502
+
503
+ module.exports['Search Compiler: LABELS throws without capability'] = test => {
504
+ let connection = createMockConnection();
505
+
506
+ try {
507
+ searchCompiler(connection, { labels: { not: ['Horizon'] } });
508
+ test.ok(false, 'Should have thrown');
509
+ } catch (err) {
510
+ test.equal(err.code, 'MissingServerExtension');
511
+ test.ok(err.message.includes('X-GM-EXT-1'));
512
+ }
513
+
514
+ test.done();
515
+ };
516
+
517
+ module.exports['Search Compiler: empty LABELS is a no-op without capability'] = test => {
518
+ let connection = createMockConnection();
519
+ let compiled = searchCompiler(connection, { labels: {} });
520
+
521
+ test.ok(!hasAttr(compiled, 'X-GM-RAW'));
522
+ test.equal(compiled.length, 0);
523
+ test.done();
524
+ };
525
+
446
526
  // ============================================
447
527
  // Date search tests
448
528
  // ============================================