@bobfrankston/iflow-direct 0.1.59 → 0.1.61
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 +7 -1
- package/imap-compat.js +81 -16
- package/package.json +1 -1
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
|
-
|
|
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()}`;
|