@yozz.app/imap 0.1.6 → 0.2.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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,36 @@
1
+ //#region src/commands.d.ts
2
+ /** One `UID SEARCH` key. */
3
+ type UidSearchCriterion =
4
+ /** `TEXT` is the body alone on some servers, and `OR` answers less than its operands on some. */
5
+ {
6
+ readonly key: 'SUBJECT' | 'FROM' | 'TO' | 'CC' | 'BCC' | 'TEXT';
7
+ readonly value: string;
8
+ } |
9
+ /** A server need only index the headers IMAP names; on a private `X-` header it may answer the empty list. */
10
+ {
11
+ readonly key: 'HEADER';
12
+ readonly header: 'Message-ID' | 'Reply-To';
13
+ readonly value: string;
14
+ } | {
15
+ readonly key: 'UNSEEN';
16
+ } | {
17
+ readonly key: 'FLAGGED';
18
+ } |
19
+ /** By INTERNALDATE's day: `SINCE` is on or after it, `BEFORE` strictly before it. */
20
+ {
21
+ readonly key: 'BEFORE';
22
+ readonly date: ImapSearchDate;
23
+ } | {
24
+ readonly key: 'SINCE';
25
+ readonly date: ImapSearchDate;
26
+ };
27
+ /** A calendar day; `month` runs 1 to 12. */
28
+ type ImapSearchDate = {
29
+ readonly year: number;
30
+ readonly month: number;
31
+ readonly day: number;
32
+ };
33
+ //#endregion
1
34
  //#region src/tokenizer.d.ts
2
35
  export declare const DEFAULT_MAX_LITERAL_BYTES: number;
3
36
  type ImapToken = {
@@ -91,7 +124,7 @@ type ImapFetchItem = {
91
124
  readonly bytes: Uint8Array | null;
92
125
  } | {
93
126
  readonly kind: 'bodyStructure';
94
- readonly parts: readonly string[];
127
+ readonly attachmentNames: readonly string[];
95
128
  } |
96
129
  /** Gmail's `X-GM-THRID` is 64-bit, so it stays decimal digits. */
97
130
  {
@@ -201,11 +234,17 @@ type ImapMessageSummary = {
201
234
  readonly references: readonly string[];
202
235
  /** Gmail's thread id (decimal digits), when the server was asked and answered. */
203
236
  readonly gmailThreadId: string | null;
237
+ /** The file names its parts carry, from `BODYSTRUCTURE`. */
238
+ readonly attachmentNames: readonly string[];
204
239
  };
205
240
  type ImapMessageFlags = {
206
241
  readonly uid: number;
207
242
  readonly flags: readonly string[];
208
243
  };
244
+ type ImapMessageAttachmentNames = {
245
+ readonly uid: number;
246
+ readonly attachmentNames: readonly string[];
247
+ };
209
248
  type ImapSelected = {
210
249
  readonly name: string;
211
250
  readonly exists: number;
@@ -246,6 +285,8 @@ type ImapClient = {
246
285
  /** By message sequence number (dense, 1..EXISTS) rather than uid (sparse after deletions). */
247
286
  readonly fetchSummariesBySeq: (seqSet: string) => Promise<ImapResult<readonly ImapMessageSummary[]>>;
248
287
  readonly fetchFlags: (uidSet: string) => Promise<ImapResult<readonly ImapMessageFlags[]>>;
288
+ /** Just the file names of `fetchSummaries`, for summaries read without them. Marks nothing read. */
289
+ readonly fetchAttachmentNames: (uidSet: string) => Promise<ImapResult<readonly ImapMessageAttachmentNames[]>>;
249
290
  readonly fetchRaw: (uid: number) => Promise<ImapResult<Uint8Array>>;
250
291
  readonly storeFlags: (uidSet: string, mode: 'add' | 'remove' | 'set', flags: readonly string[]) => Promise<ImapResult<void>>;
251
292
  /** Resolves with the RFC 4315 `APPENDUID`, or `null` from a server without UIDPLUS. */
@@ -253,13 +294,8 @@ type ImapClient = {
253
294
  readonly expunge: () => Promise<ImapResult<void>>;
254
295
  /** RFC 4315. Refuses without UIDPLUS rather than falling back to EXPUNGE, which erases more than asked. */
255
296
  readonly uidExpunge: (uidSet: string) => Promise<ImapResult<void>>;
256
- readonly uidSearchHeader: (header: string, value: string) => Promise<ImapResult<readonly number[]>>;
257
- /**
258
- * The selected mailbox's uids whose subject, sender, `To` or body contains `text`, ascending.
259
- * One search per field and a union: `TEXT` alone is the body on Forward Email's server, and its
260
- * `OR` answers a subset of what the operands do.
261
- */
262
- readonly uidSearchText: (text: string) => Promise<ImapResult<readonly number[]>>;
297
+ /** The selected mailbox's uids matching one criterion, ascending. */
298
+ readonly uidSearch: (criterion: UidSearchCriterion) => Promise<ImapResult<readonly number[]>>;
263
299
  /** RFC 6851. Answers with the RFC 4315 `COPYUID`, or null when the server gave none. Refuses without MOVE. */
264
300
  readonly move: (uidSet: string, mailbox: string) => Promise<ImapResult<CopyUid | null>>;
265
301
  readonly create: (mailbox: string) => Promise<ImapResult<void>>;
@@ -304,4 +340,4 @@ export declare const MAX_UID_SET_LENGTH = 4096;
304
340
  */
305
341
  export declare const uidSets: (uids: Iterable<number>, maxLength?: number) => readonly string[];
306
342
  //#endregion
307
- export type { ByteDuplex, CopyUid, ImapAddress, ImapClient, ImapClientOptions, ImapContinuation, ImapEnvelope, ImapFailure, ImapFetchItem, ImapIdle, ImapMailbox, ImapMessageSummary, ImapResponse, ImapResponseCode, ImapResult, ImapSelected, ImapTagged, ImapToken, ImapUntagged };
343
+ export type { ByteDuplex, CopyUid, ImapAddress, ImapClient, ImapClientOptions, ImapContinuation, ImapEnvelope, ImapFailure, ImapFetchItem, ImapIdle, ImapMailbox, ImapMessageAttachmentNames, ImapMessageSummary, ImapResponse, ImapResponseCode, ImapResult, ImapSearchDate, ImapSelected, ImapTagged, ImapToken, ImapUntagged, UidSearchCriterion };
package/dist/index.mjs CHANGED
@@ -214,8 +214,9 @@ const buildSelectCommand = (tag, mailbox, readOnly = false) => {
214
214
  return { lines: [{ text: stringToBytes(`${tag} ${verb} ${quoteString(mboxUtf7)}\r\n`) }] };
215
215
  };
216
216
  /** `X-GM-THRID` is asked for only when the server advertised `X-GM-EXT-1`: an unknown item is a BAD. */
217
- const buildFetchSummariesCommand = (tag, set, { gmail, bySeq = false }) => ({ lines: [{ text: stringToBytes(`${tag} ${bySeq ? "FETCH" : "UID FETCH"} ${set} (FLAGS ENVELOPE INTERNALDATE RFC822.SIZE UID BODY.PEEK[HEADER.FIELDS (REFERENCES)]${gmail ? " X-GM-THRID" : ""})\r\n`) }] });
217
+ const buildFetchSummariesCommand = (tag, set, { gmail, bySeq = false }) => ({ lines: [{ text: stringToBytes(`${tag} ${bySeq ? "FETCH" : "UID FETCH"} ${set} (FLAGS ENVELOPE INTERNALDATE RFC822.SIZE UID BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (REFERENCES)]${gmail ? " X-GM-THRID" : ""})\r\n`) }] });
218
218
  const buildFetchFlagsCommand = (tag, uidSet) => ({ lines: [{ text: stringToBytes(`${tag} UID FETCH ${uidSet} (FLAGS UID)\r\n`) }] });
219
+ const buildFetchBodyStructureCommand = (tag, uidSet) => ({ lines: [{ text: stringToBytes(`${tag} UID FETCH ${uidSet} (UID BODYSTRUCTURE)\r\n`) }] });
219
220
  const buildFetchRawCommand = (tag, uid) => ({ lines: [{ text: stringToBytes(`${tag} UID FETCH ${uid} (BODY.PEEK[])\r\n`) }] });
220
221
  const STORE_OPS = {
221
222
  add: "+FLAGS",
@@ -258,16 +259,23 @@ const buildAppendCommand = (tag, mailbox, flags, message, internalDate) => {
258
259
  const buildExpungeCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} EXPUNGE\r\n`) }] });
259
260
  /** RFC 4315. */
260
261
  const buildUidExpungeCommand = (tag, uidSet) => ({ lines: [{ text: stringToBytes(`${tag} UID EXPUNGE ${uidSet}\r\n`) }] });
261
- /** A server need only index the headers IMAP names; on a private `X-` header it may answer the empty list. */
262
- const buildUidSearchHeaderCommand = (tag, header, value) => ({ lines: [{ text: stringToBytes(`${tag} UID SEARCH HEADER ${quoteString(header)} ${quoteString(value)}\r\n`) }] });
263
262
  /**
264
- * One `UID SEARCH <key> <text>`. A printable-ASCII query is a quoted string in the default charset;
263
+ * One `UID SEARCH <key>`. A printable-ASCII value is a quoted string in the default charset;
265
264
  * anything else is a UTF-8 literal, which every server must accept.
266
265
  */
267
- const buildUidSearchTextCommand = (tag, key, text) => isSafeQuotedString(text) ? { lines: [{ text: stringToBytes(`${tag} UID SEARCH ${key} ${quoteString(text)}\r\n`) }] } : { lines: [{
268
- text: stringToBytes(`${tag} UID SEARCH CHARSET UTF-8 ${key} `),
269
- literal: stringToBytes(text)
270
- }, { text: stringToBytes("\r\n") }] };
266
+ const buildUidSearchCommand = (tag, criterion) => {
267
+ if (criterion.key === "UNSEEN" || criterion.key === "FLAGGED") return { lines: [{ text: stringToBytes(`${tag} UID SEARCH ${criterion.key}\r\n`) }] };
268
+ if (criterion.key === "BEFORE" || criterion.key === "SINCE") {
269
+ const { year, month, day } = criterion.date;
270
+ const date = `${day}-${MONTHS[month - 1]}-${String(year).padStart(4, "0")}`;
271
+ return { lines: [{ text: stringToBytes(`${tag} UID SEARCH ${criterion.key} ${date}\r\n`) }] };
272
+ }
273
+ const key = criterion.key === "HEADER" ? `HEADER ${quoteString(criterion.header)}` : criterion.key;
274
+ return isSafeQuotedString(criterion.value) ? { lines: [{ text: stringToBytes(`${tag} UID SEARCH ${key} ${quoteString(criterion.value)}\r\n`) }] } : { lines: [{
275
+ text: stringToBytes(`${tag} UID SEARCH CHARSET UTF-8 ${key} `),
276
+ literal: stringToBytes(criterion.value)
277
+ }, { text: stringToBytes("\r\n") }] };
278
+ };
271
279
  /** RFC 6851. */
272
280
  const buildMoveCommand = (tag, uidSet, mailbox) => ({ lines: [{ text: stringToBytes(`${tag} UID MOVE ${uidSet} ${quoteString(encodeModifiedUtf7(mailbox))}\r\n`) }] });
273
281
  const buildCreateCommand = (tag, mailbox) => ({ lines: [{ text: stringToBytes(`${tag} CREATE ${quoteString(encodeModifiedUtf7(mailbox))}\r\n`) }] });
@@ -405,6 +413,158 @@ const decodeRfc2047 = (header) => {
405
413
  return result;
406
414
  };
407
415
  //#endregion
416
+ //#region src/body-structure.ts
417
+ /** Past this a structure is not mail anyone wrote, and a name in it is not worth finding. */
418
+ const MAX_DEPTH = 32;
419
+ const MAX_NAMES = 64;
420
+ const MAX_NAME_LENGTH = 255;
421
+ const readList = (tokens, start) => {
422
+ if (tokens[start]?.kind !== "lparen") return {
423
+ list: null,
424
+ nextIndex: start + 1
425
+ };
426
+ const list = {
427
+ kind: "list",
428
+ items: []
429
+ };
430
+ const open = [list];
431
+ let idx = start + 1;
432
+ while (idx < tokens.length && open.length > 0) {
433
+ const token = tokens[idx];
434
+ idx++;
435
+ const top = open.at(-1);
436
+ if (token === void 0 || top === void 0) break;
437
+ if (token.kind === "rparen") open.pop();
438
+ else if (token.kind === "lparen") {
439
+ const child = {
440
+ kind: "list",
441
+ items: []
442
+ };
443
+ top.items.push(child);
444
+ open.push(child);
445
+ } else top.items.push(token);
446
+ }
447
+ return {
448
+ list,
449
+ nextIndex: idx
450
+ };
451
+ };
452
+ /** A server may send a name as raw bytes; they are UTF-8 unless they cannot be. */
453
+ const decodeRawName = (bytes) => {
454
+ try {
455
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
456
+ } catch {
457
+ return new TextDecoder("latin1").decode(bytes);
458
+ }
459
+ };
460
+ const textOf = (node) => {
461
+ if (node?.kind === "literal") return decodeRawName(node.value);
462
+ if (node?.kind === "quoted") return decodeRawName(Uint8Array.from(node.value, byteOf));
463
+ return node?.kind === "atom" ? node.value : null;
464
+ };
465
+ const byteOf = (character) => character.charCodeAt(0);
466
+ /** `("KEY" "value" …)` by lowercased key. */
467
+ const paramsIn = (node) => {
468
+ if (node?.kind !== "list") return /* @__PURE__ */ new Map();
469
+ return new Map(node.items.flatMap((key, index) => {
470
+ const value = textOf(node.items[index + 1]);
471
+ const name = index % 2 === 0 ? textOf(key) : null;
472
+ return name === null || value === null ? [] : [[name.toLowerCase(), value]];
473
+ }));
474
+ };
475
+ /** RFC 2231 §4 percent-encoding, as bytes; null when an escape is broken. */
476
+ const percentDecoded = (text) => {
477
+ const bytes = new TextEncoder().encode(text);
478
+ const out = [];
479
+ for (let i = 0; i < bytes.length; i++) {
480
+ const byte = bytes[i] ?? 0;
481
+ if (byte !== 37) {
482
+ out.push(byte);
483
+ continue;
484
+ }
485
+ const hex = String.fromCharCode(bytes[i + 1] ?? 0, bytes[i + 2] ?? 0);
486
+ if (!/^[0-9a-fA-F]{2}$/u.test(hex)) return null;
487
+ out.push(Number.parseInt(hex, 16));
488
+ i += 2;
489
+ }
490
+ return out;
491
+ };
492
+ const decodedIn = (bytes, charset) => {
493
+ try {
494
+ return new TextDecoder(charset === "" ? "utf-8" : charset, { fatal: true }).decode(Uint8Array.from(bytes));
495
+ } catch {
496
+ return null;
497
+ }
498
+ };
499
+ /**
500
+ * RFC 2231's `key*`, `key*0`, `key*0*`, `key*1`…: the sections in order, their bytes joined before
501
+ * the charset decodes them, since a character may straddle two. Null when absent or odd.
502
+ */
503
+ const extendedParam = (params, key) => {
504
+ const ordered = [...params].flatMap(([name, value]) => {
505
+ if (name === `${key}*`) return [{
506
+ index: 0,
507
+ isEncoded: true,
508
+ value
509
+ }];
510
+ if (!name.startsWith(`${key}*`)) return [];
511
+ const suffix = /^(\d{1,3})(\*?)$/u.exec(name.slice(key.length + 1));
512
+ return suffix === null ? [] : [{
513
+ index: Number(suffix[1]),
514
+ isEncoded: suffix[2] === "*",
515
+ value
516
+ }];
517
+ }).toSorted((a, b) => a.index - b.index);
518
+ const [first] = ordered;
519
+ if (first === void 0 || ordered.some(({ index }, at) => index !== at)) return null;
520
+ const opening = first.isEncoded ? /^([^']*)'[^']*'(.*)$/su.exec(first.value) : null;
521
+ if (first.isEncoded && opening === null) return null;
522
+ const bytes = ordered.map(({ isEncoded, value }, at) => {
523
+ const text = at === 0 && opening !== null ? opening[2] ?? "" : value;
524
+ return isEncoded ? percentDecoded(text) : [...new TextEncoder().encode(text)];
525
+ });
526
+ if (bytes.some((section) => section === null)) return null;
527
+ const decoded = decodedIn(bytes.flatMap((section) => section ?? []), opening?.[1] ?? "");
528
+ return decoded === null || first.isEncoded ? decoded : decodeRfc2047(decoded);
529
+ };
530
+ const paramNamed = (params, key) => {
531
+ const plain = params.get(key);
532
+ return extendedParam(params, key) ?? (plain === void 0 ? null : decodeRfc2047(plain));
533
+ };
534
+ /** The disposition's file name wins over the type's older `name`. Anything odd is no name. */
535
+ const partName = (disposition, params) => {
536
+ const name = (paramNamed(disposition, "filename") ?? paramNamed(params, "name") ?? "").trim();
537
+ return name === "" || name.length > MAX_NAME_LENGTH ? null : name;
538
+ };
539
+ /** RFC 9051 body-type-1part: where the disposition sits differs by the part's type. */
540
+ const dispositionAt = (type, subtype) => {
541
+ if (type === "TEXT") return 9;
542
+ return type === "MESSAGE" && ["RFC822", "GLOBAL"].includes(subtype) ? 11 : 8;
543
+ };
544
+ const namesIn = (node, depth) => {
545
+ if (node?.kind !== "list" || depth > MAX_DEPTH) return [];
546
+ const [first, second] = node.items;
547
+ if (first?.kind === "list") return node.items.flatMap((child) => namesIn(child, depth + 1));
548
+ const type = textOf(first)?.toUpperCase() ?? "";
549
+ const subtype = textOf(second)?.toUpperCase() ?? "";
550
+ const disposition = node.items[dispositionAt(type, subtype)];
551
+ const own = partName(paramsIn(disposition?.kind === "list" ? disposition.items[1] : void 0), paramsIn(node.items[2]));
552
+ const nested = dispositionAt(type, subtype) === 11 ? namesIn(node.items[8], depth + 1) : [];
553
+ return [...own === null ? [] : [own], ...nested];
554
+ };
555
+ /**
556
+ * The names a message's parts carry, from a `BODYSTRUCTURE` or `BODY` response: `filename` or `name`
557
+ * in the plain, RFC 2231 and RFC 2047 forms, once each, in part order. Total: what cannot be read
558
+ * is no name, never a failure.
559
+ */
560
+ const parseBodyStructure = (tokens, start) => {
561
+ const { list, nextIndex } = readList(tokens, start);
562
+ return {
563
+ attachmentNames: [...new Set(namesIn(list ?? void 0, 0))].slice(0, MAX_NAMES),
564
+ nextIndex
565
+ };
566
+ };
567
+ //#endregion
408
568
  //#region src/envelope.ts
409
569
  const tokenToString$1 = (token) => {
410
570
  if (token === void 0 || token.kind === "nil") return null;
@@ -531,47 +691,6 @@ const parseEnvelope = (tokens, start) => {
531
691
  nextIndex: idx
532
692
  };
533
693
  };
534
- /** Only the MIME types, e.g. `['TEXT/PLAIN', 'TEXT/HTML']`. */
535
- const parseBodyStructureParts = (tokens, start) => {
536
- if (tokens[start]?.kind !== "lparen") return {
537
- parts: [],
538
- nextIndex: start + 1
539
- };
540
- let idx = start + 1;
541
- const parts = [];
542
- if (tokens[idx]?.kind === "lparen") {
543
- while (idx < tokens.length && tokens[idx]?.kind === "lparen") {
544
- const child = parseBodyStructureParts(tokens, idx);
545
- parts.push(...child.parts);
546
- idx = child.nextIndex;
547
- }
548
- let depth = 1;
549
- while (idx < tokens.length && depth > 0) {
550
- if (tokens[idx]?.kind === "lparen") depth++;
551
- else if (tokens[idx]?.kind === "rparen") depth--;
552
- idx++;
553
- }
554
- return {
555
- parts,
556
- nextIndex: idx
557
- };
558
- }
559
- const mediaType = tokenToString$1(tokens[idx]) ?? "APPLICATION";
560
- idx++;
561
- const subType = tokenToString$1(tokens[idx]) ?? "OCTET-STREAM";
562
- idx++;
563
- parts.push(`${mediaType.toUpperCase()}/${subType.toUpperCase()}`);
564
- let depth = 1;
565
- while (idx < tokens.length && depth > 0) {
566
- if (tokens[idx]?.kind === "lparen") depth++;
567
- else if (tokens[idx]?.kind === "rparen") depth--;
568
- idx++;
569
- }
570
- return {
571
- parts,
572
- nextIndex: idx
573
- };
574
- };
575
694
  const parseFetchItems = (tokens, start) => {
576
695
  if (tokens[start]?.kind !== "lparen") return {
577
696
  items: [],
@@ -637,12 +756,12 @@ const parseFetchItems = (tokens, start) => {
637
756
  } else idx++;
638
757
  } else if (itemName === "BODYSTRUCTURE" || itemName === "BODY" || itemName === "BODY.PEEK") {
639
758
  if (tokens[idx]?.kind === "lparen") {
640
- const bsResult = parseBodyStructureParts(tokens, idx);
759
+ const { attachmentNames, nextIndex } = parseBodyStructure(tokens, idx);
641
760
  items.push({
642
761
  kind: "bodyStructure",
643
- parts: bsResult.parts
762
+ attachmentNames
644
763
  });
645
- idx = bsResult.nextIndex;
764
+ idx = nextIndex;
646
765
  } else if (tokens[idx]?.kind === "lbracket") {
647
766
  idx++;
648
767
  const sectionParts = [];
@@ -1465,12 +1584,14 @@ const summariesFrom = (untagged) => untagged.flatMap((item) => {
1465
1584
  let envelope = null;
1466
1585
  let references = [];
1467
1586
  let gmailThreadId = null;
1587
+ let attachmentNames = [];
1468
1588
  for (const fItem of item.items) if (fItem.kind === "uid") uid = fItem.uid;
1469
1589
  else if (fItem.kind === "flags") flags = fItem.flags;
1470
1590
  else if (fItem.kind === "internalDate") internalDate = fItem.date;
1471
1591
  else if (fItem.kind === "size") size = fItem.size;
1472
1592
  else if (fItem.kind === "envelope") envelope = fItem.envelope;
1473
1593
  else if (fItem.kind === "gmailThreadId") gmailThreadId = fItem.id;
1594
+ else if (fItem.kind === "bodyStructure") attachmentNames = fItem.attachmentNames;
1474
1595
  else if (fItem.kind === "body" && fItem.section.toUpperCase().startsWith("HEADER.FIELDS")) references = parseReferencesHeader(fItem.bytes);
1475
1596
  return [{
1476
1597
  seq: item.seq,
@@ -1480,7 +1601,8 @@ const summariesFrom = (untagged) => untagged.flatMap((item) => {
1480
1601
  size,
1481
1602
  envelope,
1482
1603
  references,
1483
- gmailThreadId
1604
+ gmailThreadId,
1605
+ attachmentNames
1484
1606
  }];
1485
1607
  });
1486
1608
  const createImapClient = (transport, options) => {
@@ -1956,6 +2078,22 @@ const createImapClient = (transport, options) => {
1956
2078
  value: summariesFrom(res.value.untagged)
1957
2079
  };
1958
2080
  }),
2081
+ fetchAttachmentNames: (uidSet) => enqueueCommand(async () => {
2082
+ const res = await executeCommand((tag) => buildFetchBodyStructureCommand(tag, uidSet));
2083
+ if (!res.ok) return res;
2084
+ return {
2085
+ ok: true,
2086
+ value: res.value.untagged.flatMap((item) => {
2087
+ if (item.kind !== "fetch") return [];
2088
+ const uid = item.items.find(({ kind }) => kind === "uid");
2089
+ const structure = item.items.find(({ kind }) => kind === "bodyStructure");
2090
+ return uid?.kind === "uid" && structure?.kind === "bodyStructure" ? [{
2091
+ uid: uid.uid,
2092
+ attachmentNames: structure.attachmentNames
2093
+ }] : [];
2094
+ })
2095
+ };
2096
+ }),
1959
2097
  fetchFlags: (uidSet) => enqueueCommand(async () => {
1960
2098
  const res = await executeCommand((tag) => buildFetchFlagsCommand(tag, uidSet));
1961
2099
  if (!res.ok) return res;
@@ -2034,29 +2172,12 @@ const createImapClient = (transport, options) => {
2034
2172
  value: void 0
2035
2173
  };
2036
2174
  }),
2037
- uidSearchHeader: (header, value) => enqueueCommand(async () => {
2038
- const res = await executeCommand((tag) => buildUidSearchHeaderCommand(tag, header, value));
2175
+ uidSearch: (criterion) => enqueueCommand(async () => {
2176
+ const res = await executeCommand((tag) => buildUidSearchCommand(tag, criterion));
2039
2177
  if (!res.ok) return res;
2040
2178
  return {
2041
2179
  ok: true,
2042
- value: res.value.untagged.flatMap((item) => item.kind === "search" ? [...item.uids] : [])
2043
- };
2044
- }),
2045
- uidSearchText: (text) => enqueueCommand(async () => {
2046
- const found = /* @__PURE__ */ new Set();
2047
- for (const key of [
2048
- "SUBJECT",
2049
- "FROM",
2050
- "TO",
2051
- "TEXT"
2052
- ]) {
2053
- const res = await executeCommand((tag) => buildUidSearchTextCommand(tag, key, text));
2054
- if (!res.ok) return res;
2055
- for (const item of res.value.untagged) if (item.kind === "search") for (const uid of item.uids) found.add(uid);
2056
- }
2057
- return {
2058
- ok: true,
2059
- value: [...found].toSorted((a, b) => a - b)
2180
+ value: res.value.untagged.flatMap((item) => item.kind === "search" ? [...item.uids] : []).toSorted((a, b) => a - b)
2060
2181
  };
2061
2182
  }),
2062
2183
  move: (uidSet, mailbox) => enqueueCommand(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yozz.app/imap",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Transport-agnostic IMAP4rev2/rev1 client core: literals, total parsing, RFC 2047, SASL PLAIN/LOGIN, IDLE, UID MOVE, APPEND with APPENDUID, UID EXPUNGE.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fishballapp/yozz",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^24.13.4",
33
- "@yozz.app/smtp": "0.1.6",
34
- "@yozz.app/tls": "0.1.6",
35
- "@yozz.app/x509": "0.1.6",
33
+ "@yozz.app/smtp": "0.2.0",
34
+ "@yozz.app/tls": "0.2.0",
35
+ "@yozz.app/x509": "0.2.0",
36
36
  "tsdown": "^0.23.0"
37
37
  },
38
38
  "scripts": {