@yozz.app/imap 0.1.5 → 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 +45 -9
- package/dist/index.mjs +194 -72
- package/package.json +4 -4
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
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
* The selected mailbox's uids whose subject, sender or body contains `text`, ascending. One
|
|
259
|
-
* search per field and a union: `TEXT` alone is the body on Forward Email's server, and its `OR`
|
|
260
|
-
* 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
|
|
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
|
|
268
|
-
text: stringToBytes(`${tag} UID SEARCH
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
759
|
+
const { attachmentNames, nextIndex } = parseBodyStructure(tokens, idx);
|
|
641
760
|
items.push({
|
|
642
761
|
kind: "bodyStructure",
|
|
643
|
-
|
|
762
|
+
attachmentNames
|
|
644
763
|
});
|
|
645
|
-
idx =
|
|
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,28 +2172,12 @@ const createImapClient = (transport, options) => {
|
|
|
2034
2172
|
value: void 0
|
|
2035
2173
|
};
|
|
2036
2174
|
}),
|
|
2037
|
-
|
|
2038
|
-
const res = await executeCommand((tag) =>
|
|
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
|
-
"TEXT"
|
|
2051
|
-
]) {
|
|
2052
|
-
const res = await executeCommand((tag) => buildUidSearchTextCommand(tag, key, text));
|
|
2053
|
-
if (!res.ok) return res;
|
|
2054
|
-
for (const item of res.value.untagged) if (item.kind === "search") for (const uid of item.uids) found.add(uid);
|
|
2055
|
-
}
|
|
2056
|
-
return {
|
|
2057
|
-
ok: true,
|
|
2058
|
-
value: [...found].toSorted((a, b) => a - b)
|
|
2180
|
+
value: res.value.untagged.flatMap((item) => item.kind === "search" ? [...item.uids] : []).toSorted((a, b) => a - b)
|
|
2059
2181
|
};
|
|
2060
2182
|
}),
|
|
2061
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.
|
|
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.
|
|
34
|
-
"@yozz.app/tls": "0.
|
|
35
|
-
"@yozz.app/x509": "0.
|
|
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": {
|