@bobfrankston/iflow-direct 0.1.59 → 0.1.62

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/imap-compat.d.ts CHANGED
@@ -87,7 +87,12 @@ export declare class CompatImapClient {
87
87
  fetchMessages(mailbox: string, end: number, count: number, options?: {
88
88
  source?: boolean;
89
89
  }): Promise<FetchedMessage[]>;
90
- /** Search messages in a mailbox */
90
+ /** Search messages in a mailbox.
91
+ * Criteria: from/to/cc/subject/body/text (string or string[], each key
92
+ * repeats and ANDs), since/before (Date), seen/unseen/flagged/answered/
93
+ * draft (boolean flags), `not` (criteria object or array — each becomes
94
+ * a NOT'd key group), `or` (array of ≥2 criteria objects — compiled to
95
+ * IMAP's binary prefix OR chain). */
91
96
  searchMessages(mailbox: string, criteria: any): Promise<number[]>;
92
97
  /** Search by header value — returns matching UIDs */
93
98
  searchByHeader(mailbox: string, headerName: string, headerValue: string): Promise<number[]>;
@@ -171,4 +176,5 @@ export declare class CompatImapClient {
171
176
  /** Get flags for a UID — returns string[] for compatibility with old client */
172
177
  getFlags(mailbox: string, uid: number): Promise<string[]>;
173
178
  }
179
+ export declare function buildSearchString(criteria: any): string;
174
180
  //# sourceMappingURL=imap-compat.d.ts.map
package/imap-compat.js CHANGED
@@ -204,25 +204,16 @@ export class CompatImapClient {
204
204
  await this.native.closeMailbox();
205
205
  return msgs.map(m => new FetchedMessage(m));
206
206
  }
207
- /** Search messages in a mailbox */
207
+ /** Search messages in a mailbox.
208
+ * Criteria: from/to/cc/subject/body/text (string or string[], each key
209
+ * repeats and ANDs), since/before (Date), seen/unseen/flagged/answered/
210
+ * draft (boolean flags), `not` (criteria object or array — each becomes
211
+ * a NOT'd key group), `or` (array of ≥2 criteria objects — compiled to
212
+ * IMAP's binary prefix OR chain). */
208
213
  async searchMessages(mailbox, criteria) {
209
214
  await this.ensureConnected();
210
215
  await this.native.select(mailbox);
211
- // Convert object criteria to IMAP search string
212
- const parts = [];
213
- if (criteria.from)
214
- parts.push(`FROM "${criteria.from}"`);
215
- if (criteria.to)
216
- parts.push(`TO "${criteria.to}"`);
217
- if (criteria.subject)
218
- parts.push(`SUBJECT "${criteria.subject}"`);
219
- if (criteria.body)
220
- parts.push(`BODY "${criteria.body}"`);
221
- if (criteria.since)
222
- parts.push(`SINCE ${formatDate(criteria.since)}`);
223
- if (criteria.before)
224
- parts.push(`BEFORE ${formatDate(criteria.before)}`);
225
- const searchStr = parts.length > 0 ? parts.join(" ") : "ALL";
216
+ const searchStr = buildSearchString(criteria || {});
226
217
  const uids = await this.native.search(searchStr);
227
218
  await this.native.closeMailbox();
228
219
  return uids;
@@ -383,6 +374,80 @@ export class CompatImapClient {
383
374
  return msg?.flags ? [...msg.flags] : [];
384
375
  }
385
376
  }
377
+ /** IMAP quoted-string: backslash-escape `\` and `"`. The old inline
378
+ * `"${value}"` interpolation let a user-typed quote break the whole
379
+ * SEARCH command. */
380
+ function imapQuote(v) {
381
+ return `"${String(v).replace(/([\\"])/g, "\\$1")}"`;
382
+ }
383
+ /** Compile one criteria object's own keys (no not/or) to IMAP search keys. */
384
+ function searchKeys(c) {
385
+ const parts = [];
386
+ const each = (v, fn) => {
387
+ for (const x of Array.isArray(v) ? v : [v])
388
+ if (x)
389
+ fn(String(x));
390
+ };
391
+ if (c.from)
392
+ each(c.from, v => parts.push(`FROM ${imapQuote(v)}`));
393
+ if (c.to)
394
+ each(c.to, v => parts.push(`TO ${imapQuote(v)}`));
395
+ if (c.cc)
396
+ each(c.cc, v => parts.push(`CC ${imapQuote(v)}`));
397
+ if (c.subject)
398
+ each(c.subject, v => parts.push(`SUBJECT ${imapQuote(v)}`));
399
+ if (c.body)
400
+ each(c.body, v => parts.push(`BODY ${imapQuote(v)}`));
401
+ if (c.text)
402
+ each(c.text, v => parts.push(`TEXT ${imapQuote(v)}`));
403
+ if (c.since)
404
+ parts.push(`SINCE ${formatDate(c.since)}`);
405
+ if (c.before)
406
+ parts.push(`BEFORE ${formatDate(c.before)}`);
407
+ if (c.seen)
408
+ parts.push("SEEN");
409
+ if (c.unseen)
410
+ parts.push("UNSEEN");
411
+ if (c.flagged)
412
+ parts.push("FLAGGED");
413
+ if (c.unflagged)
414
+ parts.push("UNFLAGGED");
415
+ if (c.answered)
416
+ parts.push("ANSWERED");
417
+ if (c.draft)
418
+ parts.push("DRAFT");
419
+ return parts;
420
+ }
421
+ /** One criteria group as a single search-key (parenthesize multi-key). */
422
+ function keyGroup(c) {
423
+ const keys = searchKeys(c);
424
+ if (keys.length === 0)
425
+ return null;
426
+ return keys.length === 1 ? keys[0] : `(${keys.join(" ")})`;
427
+ }
428
+ export function buildSearchString(criteria) {
429
+ const parts = searchKeys(criteria);
430
+ // Exclusions: each entry compiles to `NOT <key-group>`; several entries
431
+ // AND together (exclude anything matching any of them).
432
+ const nots = criteria.not ? (Array.isArray(criteria.not) ? criteria.not : [criteria.not]) : [];
433
+ for (const n of nots) {
434
+ const g = keyGroup(n);
435
+ if (g)
436
+ parts.push(`NOT ${g}`);
437
+ }
438
+ // One OR group: IMAP OR is binary prefix, so [a,b,c] nests to
439
+ // `OR a OR b c`.
440
+ const ors = (Array.isArray(criteria.or) ? criteria.or : []).map(keyGroup).filter(Boolean);
441
+ if (ors.length === 1)
442
+ parts.push(ors[0]);
443
+ else if (ors.length > 1) {
444
+ let expr = ors[ors.length - 1];
445
+ for (let i = ors.length - 2; i >= 0; i--)
446
+ expr = `OR ${ors[i]} ${expr}`;
447
+ parts.push(expr);
448
+ }
449
+ return parts.length > 0 ? parts.join(" ") : "ALL";
450
+ }
386
451
  function formatDate(d) {
387
452
  const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
388
453
  return `${d.getDate()}-${months[d.getMonth()]}-${d.getFullYear()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.59",
3
+ "version": "0.1.62",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",