@yozz.app/imap 0.1.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.mjs ADDED
@@ -0,0 +1,2221 @@
1
+ //#region src/bytes.ts
2
+ const concatByteArrays = (arrays) => {
3
+ let totalLength = 0;
4
+ for (const arr of arrays) totalLength += arr.length;
5
+ const result = new Uint8Array(totalLength);
6
+ let offset = 0;
7
+ for (const arr of arrays) {
8
+ result.set(arr, offset);
9
+ offset += arr.length;
10
+ }
11
+ return result;
12
+ };
13
+ const asciiDecoder = new TextDecoder("ascii");
14
+ const textEncoder = new TextEncoder();
15
+ const asciiToString = (bytes) => asciiDecoder.decode(bytes);
16
+ const stringToBytes = (str) => textEncoder.encode(str);
17
+ const isDigit = (byte) => byte >= 48 && byte <= 57;
18
+ const B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
19
+ const bytesToBase64 = (bytes) => {
20
+ let result = "";
21
+ const len = bytes.length;
22
+ let i = 0;
23
+ while (i < len) {
24
+ const b0 = bytes[i] ?? 0;
25
+ const b1 = i + 1 < len ? bytes[i + 1] ?? 0 : null;
26
+ const b2 = i + 2 < len ? bytes[i + 2] ?? 0 : null;
27
+ const idx0 = b0 >> 2;
28
+ result += B64_CHARS[idx0] ?? "";
29
+ if (b1 !== null) {
30
+ const idx1 = (b0 & 3) << 4 | b1 >> 4;
31
+ result += B64_CHARS[idx1] ?? "";
32
+ if (b2 !== null) {
33
+ const idx2 = (b1 & 15) << 2 | b2 >> 6;
34
+ const idx3 = b2 & 63;
35
+ result += (B64_CHARS[idx2] ?? "") + (B64_CHARS[idx3] ?? "");
36
+ i += 3;
37
+ } else {
38
+ const idx2 = (b1 & 15) << 2;
39
+ result += (B64_CHARS[idx2] ?? "") + "=";
40
+ i += 2;
41
+ }
42
+ } else {
43
+ const idx1 = (b0 & 3) << 4;
44
+ result += (B64_CHARS[idx1] ?? "") + "==";
45
+ i += 1;
46
+ }
47
+ }
48
+ return result;
49
+ };
50
+ //#endregion
51
+ //#region src/utf7.ts
52
+ /**
53
+ * RFC 3501 §5.1.3 Modified UTF-7 encoding and decoding for IMAP mailbox names.
54
+ *
55
+ * Printable ASCII (0x20..0x7E) except '&' are direct.
56
+ * '&' is encoded as '&-'.
57
+ * Non-ASCII characters are encoded as UTF-16BE in modified Base64 (',' instead of '/', no '=' padding),
58
+ * enclosed between '&' and '-'.
59
+ */
60
+ const b64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
61
+ const encodeModifiedBase64 = (codeUnits) => {
62
+ const bytes = [];
63
+ for (const cu of codeUnits) {
64
+ bytes.push(cu >> 8 & 255);
65
+ bytes.push(cu & 255);
66
+ }
67
+ let result = "";
68
+ let i = 0;
69
+ while (i < bytes.length) {
70
+ const b0 = bytes[i] ?? 0;
71
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] ?? 0 : null;
72
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] ?? 0 : null;
73
+ const idx0 = b0 >> 2;
74
+ result += b64Chars[idx0] ?? "";
75
+ if (b1 !== null) {
76
+ const idx1 = (b0 & 3) << 4 | b1 >> 4;
77
+ result += b64Chars[idx1] ?? "";
78
+ if (b2 !== null) {
79
+ const idx2 = (b1 & 15) << 2 | b2 >> 6;
80
+ const idx3 = b2 & 63;
81
+ result += (b64Chars[idx2] ?? "") + (b64Chars[idx3] ?? "");
82
+ i += 3;
83
+ } else {
84
+ const idx2 = (b1 & 15) << 2;
85
+ result += b64Chars[idx2] ?? "";
86
+ i += 2;
87
+ }
88
+ } else {
89
+ const idx1 = (b0 & 3) << 4;
90
+ result += b64Chars[idx1] ?? "";
91
+ i += 1;
92
+ }
93
+ }
94
+ return result;
95
+ };
96
+ const decodeModifiedBase64 = (str) => {
97
+ const standardB64 = str.replace(/,/g, "/");
98
+ const remainder = standardB64.length % 4;
99
+ const padded = remainder === 0 ? standardB64 : standardB64 + "=".repeat(4 - remainder);
100
+ try {
101
+ const binary = atob(padded);
102
+ let decoded = "";
103
+ for (let i = 0; i + 1 < binary.length; i += 2) {
104
+ const high = binary.charCodeAt(i);
105
+ const low = binary.charCodeAt(i + 1);
106
+ decoded += String.fromCharCode(high << 8 | low);
107
+ }
108
+ return decoded;
109
+ } catch {
110
+ return `&${str}-`;
111
+ }
112
+ };
113
+ const encodeModifiedUtf7 = (str) => {
114
+ let result = "";
115
+ let nonAsciiBuffer = [];
116
+ const flushNonAscii = () => {
117
+ if (nonAsciiBuffer.length === 0) return;
118
+ result += `&${encodeModifiedBase64(nonAsciiBuffer)}-`;
119
+ nonAsciiBuffer = [];
120
+ };
121
+ for (let i = 0; i < str.length; i++) {
122
+ const code = str.charCodeAt(i);
123
+ if (code === 38) {
124
+ flushNonAscii();
125
+ result += "&-";
126
+ } else if (code >= 32 && code <= 126) {
127
+ flushNonAscii();
128
+ result += str[i] ?? "";
129
+ } else nonAsciiBuffer.push(code);
130
+ }
131
+ flushNonAscii();
132
+ return result;
133
+ };
134
+ const decodeModifiedUtf7 = (str) => {
135
+ let result = "";
136
+ let i = 0;
137
+ while (i < str.length) if (str[i] === "&") {
138
+ if (i + 1 < str.length && str[i + 1] === "-") {
139
+ result += "&";
140
+ i += 2;
141
+ continue;
142
+ }
143
+ const dashIndex = str.indexOf("-", i + 1);
144
+ if (dashIndex === -1) {
145
+ result += str.slice(i);
146
+ break;
147
+ }
148
+ const b64Part = str.slice(i + 1, dashIndex);
149
+ result += decodeModifiedBase64(b64Part);
150
+ i = dashIndex + 1;
151
+ } else {
152
+ result += str[i] ?? "";
153
+ i++;
154
+ }
155
+ return result;
156
+ };
157
+ //#endregion
158
+ //#region src/commands.ts
159
+ /**
160
+ * Pure builders for IMAP commands. Each builder returns exact bytes to send.
161
+ * No I/O is performed here.
162
+ */
163
+ const quoteString = (str) => {
164
+ return `"${str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
165
+ };
166
+ const isSafeQuotedString = (str) => {
167
+ for (let i = 0; i < str.length; i++) {
168
+ const code = str.charCodeAt(i);
169
+ if (code < 32 || code > 126 || code === 0) return false;
170
+ }
171
+ return true;
172
+ };
173
+ const buildSaslPlainBytes = (user, pass) => {
174
+ const userBytes = stringToBytes(user);
175
+ const passBytes = stringToBytes(pass);
176
+ const result = new Uint8Array(1 + userBytes.length + 1 + passBytes.length);
177
+ result[0] = 0;
178
+ result.set(userBytes, 1);
179
+ result[1 + userBytes.length] = 0;
180
+ result.set(passBytes, 2 + userBytes.length);
181
+ return result;
182
+ };
183
+ const buildCapabilityCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} CAPABILITY\r\n`) }] });
184
+ const buildAuthenticatePlainSaslIrCommand = (tag, user, pass) => {
185
+ return { lines: [{ text: stringToBytes(`${tag} AUTHENTICATE PLAIN ${bytesToBase64(buildSaslPlainBytes(user, pass))}\r\n`) }] };
186
+ };
187
+ const buildAuthenticatePlainInitialCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} AUTHENTICATE PLAIN\r\n`) }] });
188
+ const buildAuthenticatePlainResponse = (user, pass) => {
189
+ return { lines: [{ text: stringToBytes(`${bytesToBase64(buildSaslPlainBytes(user, pass))}\r\n`) }] };
190
+ };
191
+ const buildLoginCommand = (tag, user, pass) => {
192
+ const userSafe = isSafeQuotedString(user);
193
+ const passSafe = isSafeQuotedString(pass);
194
+ if (userSafe && passSafe) return { lines: [{ text: stringToBytes(`${tag} LOGIN ${quoteString(user)} ${quoteString(pass)}\r\n`) }] };
195
+ if (!userSafe && passSafe) return { lines: [{
196
+ text: stringToBytes(`${tag} LOGIN `),
197
+ literal: stringToBytes(user)
198
+ }, { text: stringToBytes(` ${quoteString(pass)}\r\n`) }] };
199
+ if (userSafe && !passSafe) return { lines: [{
200
+ text: stringToBytes(`${tag} LOGIN ${quoteString(user)} `),
201
+ literal: stringToBytes(pass)
202
+ }, { text: stringToBytes("\r\n") }] };
203
+ return { lines: [
204
+ {
205
+ text: stringToBytes(`${tag} LOGIN `),
206
+ literal: stringToBytes(user)
207
+ },
208
+ {
209
+ text: stringToBytes(" "),
210
+ literal: stringToBytes(pass)
211
+ },
212
+ { text: stringToBytes("\r\n") }
213
+ ] };
214
+ };
215
+ const buildListCommand = (tag, reference, pattern) => {
216
+ const refUtf7 = encodeModifiedUtf7(reference);
217
+ const patUtf7 = encodeModifiedUtf7(pattern);
218
+ return { lines: [{ text: stringToBytes(`${tag} LIST ${quoteString(refUtf7)} ${quoteString(patUtf7)}\r\n`) }] };
219
+ };
220
+ const buildSelectCommand = (tag, mailbox, readOnly = false) => {
221
+ return { lines: [{ text: stringToBytes(`${tag} ${readOnly ? "EXAMINE" : "SELECT"} ${quoteString(encodeModifiedUtf7(mailbox))}\r\n`) }] };
222
+ };
223
+ /**
224
+ * Everything a list row and client-side threading need, in one round trip. `References` is the
225
+ * one header ENVELOPE does not carry; `X-GM-THRID` is Gmail's own threading answer and is only
226
+ * asked for when the server advertised `X-GM-EXT-1`, since an unknown item is a BAD.
227
+ *
228
+ * `bySeq` drops the `UID` prefix, so `set` is read as message sequence numbers instead. `UID`
229
+ * stays in the items either way: a summary is worthless without the id its folder is keyed by.
230
+ */
231
+ 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`) }] });
232
+ const buildFetchFlagsCommand = (tag, uidSet) => ({ lines: [{ text: stringToBytes(`${tag} UID FETCH ${uidSet} (FLAGS UID)\r\n`) }] });
233
+ const buildFetchRawCommand = (tag, uid) => ({ lines: [{ text: stringToBytes(`${tag} UID FETCH ${uid} (BODY.PEEK[])\r\n`) }] });
234
+ const buildStoreFlagsCommand = (tag, uidSet, mode, flags) => {
235
+ return { lines: [{ text: stringToBytes(`${tag} UID STORE ${uidSet} ${mode === "add" ? "+FLAGS" : mode === "remove" ? "-FLAGS" : "FLAGS"} (${flags.join(" ")})\r\n`) }] };
236
+ };
237
+ /** APPEND with the message as a literal; flags are IMAP atoms such as `\\Seen`, sent unquoted. */
238
+ const buildAppendCommand = (tag, mailbox, flags, message) => {
239
+ return { lines: [{
240
+ text: stringToBytes(`${tag} APPEND ${quoteString(encodeModifiedUtf7(mailbox))} (${flags.join(" ")}) `),
241
+ literal: message
242
+ }, { text: stringToBytes("\r\n") }] };
243
+ };
244
+ /** RFC 6851 UID MOVE — relocates messages into another mailbox in one round trip. */
245
+ const buildMoveCommand = (tag, uidSet, mailbox) => ({ lines: [{ text: stringToBytes(`${tag} UID MOVE ${uidSet} ${quoteString(encodeModifiedUtf7(mailbox))}\r\n`) }] });
246
+ /** CREATE a mailbox (e.g. Archive the first time the client needs one). */
247
+ const buildCreateCommand = (tag, mailbox) => ({ lines: [{ text: stringToBytes(`${tag} CREATE ${quoteString(encodeModifiedUtf7(mailbox))}\r\n`) }] });
248
+ const buildNoopCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} NOOP\r\n`) }] });
249
+ const buildIdleCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} IDLE\r\n`) }] });
250
+ /** RFC 2177: the client ends IDLE with a bare DONE line (no tag). */
251
+ const buildIdleDoneLine = () => stringToBytes("DONE\r\n");
252
+ const buildLogoutCommand = (tag) => ({ lines: [{ text: stringToBytes(`${tag} LOGOUT\r\n`) }] });
253
+ //#endregion
254
+ //#region src/rfc2047.ts
255
+ /**
256
+ * RFC 2047 MIME Part Three: Message Header Extensions for Non-ASCII Text.
257
+ *
258
+ * Decodes encoded-words in header fields (Q and B encodings).
259
+ * Crucial invariants:
260
+ * 1. Adjacent encoded-words separated only by linear-white-space (LWS) are joined by their
261
+ * raw decoded bytes before charset decoding.
262
+ * 2. The output is NEVER re-scanned.
263
+ * 3. Unknown charsets leave the raw word intact.
264
+ */
265
+ const ENCODED_WORD_REGEX = /=\?([^?]+)\?([bBqQ])\?([^?]*)\?=/g;
266
+ const decodeQToBytes = (text) => {
267
+ const bytes = [];
268
+ let i = 0;
269
+ while (i < text.length) {
270
+ const ch = text[i];
271
+ if (ch === "_") {
272
+ bytes.push(32);
273
+ i++;
274
+ } else if (ch === "=" && i + 2 < text.length && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 1, i + 3))) {
275
+ bytes.push(Number.parseInt(text.slice(i + 1, i + 3), 16));
276
+ i += 3;
277
+ } else {
278
+ bytes.push((text.charCodeAt(i) ?? 0) & 255);
279
+ i++;
280
+ }
281
+ }
282
+ return new Uint8Array(bytes);
283
+ };
284
+ const decodeBToBytes = (text) => {
285
+ try {
286
+ const binary = atob(text.trim());
287
+ const bytes = new Uint8Array(binary.length);
288
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
289
+ return bytes;
290
+ } catch {
291
+ return null;
292
+ }
293
+ };
294
+ const decodeBytesWithCharset = (bytes, charset) => {
295
+ try {
296
+ return new TextDecoder(charset).decode(bytes);
297
+ } catch {
298
+ return null;
299
+ }
300
+ };
301
+ const decodeRfc2047 = (header) => {
302
+ const matches = [];
303
+ let match = null;
304
+ ENCODED_WORD_REGEX.lastIndex = 0;
305
+ while (true) {
306
+ match = ENCODED_WORD_REGEX.exec(header);
307
+ if (match === null) break;
308
+ const raw = match[0];
309
+ const charset = match[1] ?? "";
310
+ const encodingLetter = (match[2] ?? "").toUpperCase();
311
+ const encodedText = match[3] ?? "";
312
+ const start = match.index;
313
+ const end = start + raw.length;
314
+ if (encodingLetter === "B" || encodingLetter === "Q") matches.push({
315
+ start,
316
+ end,
317
+ raw,
318
+ charset,
319
+ encoding: encodingLetter,
320
+ encodedText
321
+ });
322
+ }
323
+ if (matches.length === 0) return header;
324
+ let result = "";
325
+ let cursor = 0;
326
+ let i = 0;
327
+ while (i < matches.length) {
328
+ const current = matches[i];
329
+ if (current === void 0) break;
330
+ if (current.start > cursor) result += header.slice(cursor, current.start);
331
+ const adjacentWords = [current];
332
+ let nextIndex = i + 1;
333
+ while (nextIndex < matches.length) {
334
+ const prev = adjacentWords[adjacentWords.length - 1];
335
+ const next = matches[nextIndex];
336
+ if (prev === void 0 || next === void 0) break;
337
+ const between = header.slice(prev.end, next.start);
338
+ if (/^[ \t\r\n]+$/.test(between)) {
339
+ adjacentWords.push(next);
340
+ nextIndex++;
341
+ } else break;
342
+ }
343
+ let j = 0;
344
+ while (j < adjacentWords.length) {
345
+ const firstInCharset = adjacentWords[j];
346
+ if (firstInCharset === void 0) break;
347
+ const sameCharsetWords = [firstInCharset];
348
+ let k = j + 1;
349
+ while (k < adjacentWords.length) {
350
+ const candidate = adjacentWords[k];
351
+ if (candidate !== void 0 && candidate.charset.toLowerCase() === firstInCharset.charset.toLowerCase()) {
352
+ sameCharsetWords.push(candidate);
353
+ k++;
354
+ } else break;
355
+ }
356
+ let isCharsetSupported = true;
357
+ try {
358
+ new TextDecoder(firstInCharset.charset);
359
+ } catch {
360
+ isCharsetSupported = false;
361
+ }
362
+ if (!isCharsetSupported) for (const w of sameCharsetWords) result += w.raw;
363
+ else {
364
+ const byteChunks = [];
365
+ let hasDecodeError = false;
366
+ for (const w of sameCharsetWords) {
367
+ const chunk = w.encoding === "B" ? decodeBToBytes(w.encodedText) : decodeQToBytes(w.encodedText);
368
+ if (chunk === null) {
369
+ hasDecodeError = true;
370
+ break;
371
+ }
372
+ byteChunks.push(chunk);
373
+ }
374
+ if (hasDecodeError) for (const w of sameCharsetWords) result += w.raw;
375
+ else {
376
+ const decodedText = decodeBytesWithCharset(concatByteArrays(byteChunks), firstInCharset.charset);
377
+ if (decodedText !== null) result += decodedText;
378
+ else for (const w of sameCharsetWords) result += w.raw;
379
+ }
380
+ }
381
+ j = k;
382
+ }
383
+ const lastWord = adjacentWords[adjacentWords.length - 1];
384
+ cursor = lastWord !== void 0 ? lastWord.end : current.end;
385
+ i = nextIndex;
386
+ }
387
+ if (cursor < header.length) result += header.slice(cursor);
388
+ return result;
389
+ };
390
+ //#endregion
391
+ //#region src/envelope.ts
392
+ /**
393
+ * Parsers for IMAP FETCH items: ENVELOPE, FLAGS, INTERNALDATE, RFC822.SIZE, BODYSTRUCTURE.
394
+ */
395
+ const tokenToString$1 = (token) => {
396
+ if (token === void 0 || token.kind === "nil") return null;
397
+ if (token.kind === "quoted" || token.kind === "atom") return token.value;
398
+ if (token.kind === "number") return String(token.value);
399
+ if (token.kind === "literal") return asciiToString(token.value);
400
+ return null;
401
+ };
402
+ /**
403
+ * Parses a single address tuple `(name adl mailbox host)` and advances index past `)`.
404
+ */
405
+ const parseAddressTuple = (tokens, start) => {
406
+ let idx = start;
407
+ if (tokens[idx]?.kind !== "lparen") return null;
408
+ idx++;
409
+ const rawName = tokenToString$1(tokens[idx]);
410
+ idx++;
411
+ idx++;
412
+ const rawMailbox = tokenToString$1(tokens[idx]);
413
+ idx++;
414
+ const rawHost = tokenToString$1(tokens[idx]);
415
+ idx++;
416
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") idx++;
417
+ if (tokens[idx]?.kind === "rparen") idx++;
418
+ const name = rawName !== null ? decodeRfc2047(rawName) : null;
419
+ const mailbox = rawMailbox;
420
+ const host = rawHost;
421
+ if (host === null) {
422
+ if (mailbox !== null) return {
423
+ address: "group-start",
424
+ nextIndex: idx
425
+ };
426
+ return {
427
+ address: "group-end",
428
+ nextIndex: idx
429
+ };
430
+ }
431
+ return {
432
+ address: {
433
+ name,
434
+ mailbox,
435
+ host
436
+ },
437
+ nextIndex: idx
438
+ };
439
+ };
440
+ /**
441
+ * Parses an address list `( (addr1) (addr2) ... )` or `NIL`.
442
+ * Flattening groups to their member addresses.
443
+ */
444
+ const parseAddressList = (tokens, start) => {
445
+ const first = tokens[start];
446
+ if (first === void 0 || first.kind === "nil") return {
447
+ addresses: [],
448
+ nextIndex: start + 1
449
+ };
450
+ if (first.kind !== "lparen") return {
451
+ addresses: [],
452
+ nextIndex: start + 1
453
+ };
454
+ let idx = start + 1;
455
+ const addresses = [];
456
+ while (idx < tokens.length) {
457
+ const tok = tokens[idx];
458
+ if (tok === void 0 || tok.kind === "rparen") {
459
+ idx++;
460
+ break;
461
+ }
462
+ if (tok.kind === "lparen") {
463
+ const parsed = parseAddressTuple(tokens, idx);
464
+ if (parsed === null) idx++;
465
+ else {
466
+ if (parsed.address !== "group-start" && parsed.address !== "group-end") addresses.push(parsed.address);
467
+ idx = parsed.nextIndex;
468
+ }
469
+ } else idx++;
470
+ }
471
+ return {
472
+ addresses,
473
+ nextIndex: idx
474
+ };
475
+ };
476
+ /**
477
+ * Parses an ENVELOPE structure:
478
+ * `(date subject from sender reply-to to cc bcc in-reply-to message-id)`
479
+ */
480
+ const parseEnvelope = (tokens, start) => {
481
+ if (tokens[start]?.kind !== "lparen") return null;
482
+ let idx = start + 1;
483
+ const date = tokenToString$1(tokens[idx]);
484
+ idx++;
485
+ const subjectRaw = tokenToString$1(tokens[idx]);
486
+ const subject = subjectRaw !== null ? decodeRfc2047(subjectRaw) : null;
487
+ idx++;
488
+ const fromResult = parseAddressList(tokens, idx);
489
+ const from = fromResult.addresses;
490
+ idx = fromResult.nextIndex;
491
+ const senderResult = parseAddressList(tokens, idx);
492
+ const sender = senderResult.addresses;
493
+ idx = senderResult.nextIndex;
494
+ const replyToResult = parseAddressList(tokens, idx);
495
+ const replyTo = replyToResult.addresses;
496
+ idx = replyToResult.nextIndex;
497
+ const toResult = parseAddressList(tokens, idx);
498
+ const to = toResult.addresses;
499
+ idx = toResult.nextIndex;
500
+ const ccResult = parseAddressList(tokens, idx);
501
+ const cc = ccResult.addresses;
502
+ idx = ccResult.nextIndex;
503
+ const bccResult = parseAddressList(tokens, idx);
504
+ const bcc = bccResult.addresses;
505
+ idx = bccResult.nextIndex;
506
+ const inReplyTo = tokenToString$1(tokens[idx]);
507
+ idx++;
508
+ const messageId = tokenToString$1(tokens[idx]);
509
+ idx++;
510
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") idx++;
511
+ if (tokens[idx]?.kind === "rparen") idx++;
512
+ return {
513
+ envelope: {
514
+ date,
515
+ subject,
516
+ subjectRaw,
517
+ from,
518
+ sender,
519
+ replyTo,
520
+ to,
521
+ cc,
522
+ bcc,
523
+ inReplyTo,
524
+ messageId
525
+ },
526
+ nextIndex: idx
527
+ };
528
+ };
529
+ /**
530
+ * Parses BODYSTRUCTURE, extracting only the list of MIME parts (e.g. ['TEXT/PLAIN', 'TEXT/HTML']).
531
+ */
532
+ const parseBodyStructureParts = (tokens, start) => {
533
+ if (tokens[start]?.kind !== "lparen") return {
534
+ parts: [],
535
+ nextIndex: start + 1
536
+ };
537
+ let idx = start + 1;
538
+ const parts = [];
539
+ if (tokens[idx]?.kind === "lparen") {
540
+ while (idx < tokens.length && tokens[idx]?.kind === "lparen") {
541
+ const child = parseBodyStructureParts(tokens, idx);
542
+ parts.push(...child.parts);
543
+ idx = child.nextIndex;
544
+ }
545
+ let depth = 1;
546
+ while (idx < tokens.length && depth > 0) {
547
+ if (tokens[idx]?.kind === "lparen") depth++;
548
+ else if (tokens[idx]?.kind === "rparen") depth--;
549
+ idx++;
550
+ }
551
+ return {
552
+ parts,
553
+ nextIndex: idx
554
+ };
555
+ }
556
+ const mediaType = tokenToString$1(tokens[idx]) ?? "APPLICATION";
557
+ idx++;
558
+ const subType = tokenToString$1(tokens[idx]) ?? "OCTET-STREAM";
559
+ idx++;
560
+ parts.push(`${mediaType.toUpperCase()}/${subType.toUpperCase()}`);
561
+ let depth = 1;
562
+ while (idx < tokens.length && depth > 0) {
563
+ if (tokens[idx]?.kind === "lparen") depth++;
564
+ else if (tokens[idx]?.kind === "rparen") depth--;
565
+ idx++;
566
+ }
567
+ return {
568
+ parts,
569
+ nextIndex: idx
570
+ };
571
+ };
572
+ /**
573
+ * Parses items inside a FETCH response `(...)`.
574
+ */
575
+ const parseFetchItems = (tokens, start) => {
576
+ if (tokens[start]?.kind !== "lparen") return {
577
+ items: [],
578
+ nextIndex: start + 1
579
+ };
580
+ let idx = start + 1;
581
+ const items = [];
582
+ while (idx < tokens.length) {
583
+ const tok = tokens[idx];
584
+ if (tok === void 0 || tok.kind === "rparen") {
585
+ idx++;
586
+ break;
587
+ }
588
+ if (tok.kind === "atom") {
589
+ const itemName = tok.value.toUpperCase();
590
+ idx++;
591
+ if (itemName === "UID") {
592
+ const uidTok = tokens[idx];
593
+ const uid = uidTok?.kind === "number" ? uidTok.value : 0;
594
+ idx++;
595
+ items.push({
596
+ kind: "uid",
597
+ uid
598
+ });
599
+ } else if (itemName === "RFC822.SIZE") {
600
+ const sizeTok = tokens[idx];
601
+ const size = sizeTok?.kind === "number" ? sizeTok.value : 0;
602
+ idx++;
603
+ items.push({
604
+ kind: "size",
605
+ size
606
+ });
607
+ } else if (itemName === "INTERNALDATE") {
608
+ const dateStr = tokenToString$1(tokens[idx]) ?? "";
609
+ idx++;
610
+ items.push({
611
+ kind: "internalDate",
612
+ date: dateStr
613
+ });
614
+ } else if (itemName === "FLAGS") if (tokens[idx]?.kind === "lparen") {
615
+ idx++;
616
+ const flags = [];
617
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") {
618
+ const flagTok = tokens[idx];
619
+ if (flagTok?.kind === "atom") flags.push(flagTok.value);
620
+ idx++;
621
+ }
622
+ if (tokens[idx]?.kind === "rparen") idx++;
623
+ items.push({
624
+ kind: "flags",
625
+ flags
626
+ });
627
+ } else idx++;
628
+ else if (itemName === "ENVELOPE") {
629
+ const envResult = parseEnvelope(tokens, idx);
630
+ if (envResult !== null) {
631
+ items.push({
632
+ kind: "envelope",
633
+ envelope: envResult.envelope
634
+ });
635
+ idx = envResult.nextIndex;
636
+ } else idx++;
637
+ } else if (itemName === "BODYSTRUCTURE" || itemName === "BODY" || itemName === "BODY.PEEK") if (tokens[idx]?.kind === "lparen") {
638
+ const bsResult = parseBodyStructureParts(tokens, idx);
639
+ items.push({
640
+ kind: "bodyStructure",
641
+ parts: bsResult.parts
642
+ });
643
+ idx = bsResult.nextIndex;
644
+ } else if (tokens[idx]?.kind === "lbracket") {
645
+ idx++;
646
+ const sectionParts = [];
647
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") {
648
+ const secTok = tokens[idx];
649
+ if (secTok !== void 0) {
650
+ if (secTok.kind === "atom" || secTok.kind === "quoted") sectionParts.push(secTok.value);
651
+ else if (secTok.kind === "number") sectionParts.push(String(secTok.value));
652
+ else if (secTok.kind === "lparen") sectionParts.push("(");
653
+ else if (secTok.kind === "rparen") sectionParts.push(")");
654
+ else if (secTok.kind === "nil") sectionParts.push("NIL");
655
+ else if (secTok.kind === "literal") sectionParts.push(asciiToString(secTok.value));
656
+ }
657
+ idx++;
658
+ }
659
+ if (tokens[idx]?.kind === "rbracket") idx++;
660
+ const section = sectionParts.join(" ");
661
+ const originTok = tokens[idx];
662
+ if (originTok?.kind === "atom" && /^<\d+(\.\d+)?>$/.test(originTok.value)) idx++;
663
+ const bodyTok = tokens[idx];
664
+ idx++;
665
+ let bytes = null;
666
+ if (bodyTok?.kind === "literal") bytes = bodyTok.value;
667
+ else if (bodyTok?.kind === "quoted" || bodyTok?.kind === "atom") bytes = stringToBytes(bodyTok.value);
668
+ else if (bodyTok?.kind === "nil") bytes = null;
669
+ items.push({
670
+ kind: "body",
671
+ section,
672
+ bytes
673
+ });
674
+ } else idx++;
675
+ else if (itemName === "X-GM-THRID") {
676
+ const id = tokenToString$1(tokens[idx]);
677
+ idx++;
678
+ if (id !== null && /^\d+$/.test(id)) items.push({
679
+ kind: "gmailThreadId",
680
+ id
681
+ });
682
+ } else {
683
+ items.push({
684
+ kind: "other",
685
+ name: itemName
686
+ });
687
+ idx++;
688
+ }
689
+ } else idx++;
690
+ }
691
+ return {
692
+ items,
693
+ nextIndex: idx
694
+ };
695
+ };
696
+ //#endregion
697
+ //#region src/response.ts
698
+ /**
699
+ * IMAP response parser: converts token stream into typed tagged, untagged, and continuation responses.
700
+ */
701
+ const tokenToString = (token) => {
702
+ if (token === void 0 || token.kind === "nil") return null;
703
+ if (token.kind === "quoted" || token.kind === "atom") return token.value;
704
+ if (token.kind === "number") return String(token.value);
705
+ if (token.kind === "literal") return asciiToString(token.value);
706
+ return null;
707
+ };
708
+ const tokensToText = (tokens, start) => {
709
+ const parts = [];
710
+ for (let i = start; i < tokens.length; i++) {
711
+ const tok = tokens[i];
712
+ if (tok === void 0) continue;
713
+ if (tok.kind === "atom" || tok.kind === "quoted") parts.push(tok.value);
714
+ else if (tok.kind === "number") parts.push(String(tok.value));
715
+ else if (tok.kind === "literal") parts.push(asciiToString(tok.value));
716
+ else if (tok.kind === "plus") parts.push("+");
717
+ else if (tok.kind === "nil") parts.push("NIL");
718
+ else if (tok.kind === "lparen") parts.push("(");
719
+ else if (tok.kind === "rparen") parts.push(")");
720
+ else if (tok.kind === "lbracket") parts.push("[");
721
+ else if (tok.kind === "rbracket") parts.push("]");
722
+ }
723
+ return parts.join(" ");
724
+ };
725
+ const parseResponseCode = (tokens, start) => {
726
+ if (tokens[start]?.kind !== "lbracket") return null;
727
+ let idx = start + 1;
728
+ const codeTok = tokens[idx];
729
+ if (codeTok?.kind !== "atom") return null;
730
+ const codeName = codeTok.value.toUpperCase();
731
+ idx++;
732
+ if (codeName === "CAPABILITY") {
733
+ const capabilities = [];
734
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") {
735
+ const capTok = tokens[idx];
736
+ if (capTok?.kind === "atom") capabilities.push(capTok.value);
737
+ idx++;
738
+ }
739
+ if (tokens[idx]?.kind === "rbracket") idx++;
740
+ return {
741
+ code: {
742
+ kind: "capability",
743
+ capabilities
744
+ },
745
+ nextIndex: idx
746
+ };
747
+ }
748
+ if (codeName === "PERMANENTFLAGS") {
749
+ const flags = [];
750
+ if (tokens[idx]?.kind === "lparen") {
751
+ idx++;
752
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") {
753
+ const flagTok = tokens[idx];
754
+ if (flagTok?.kind === "atom") flags.push(flagTok.value);
755
+ idx++;
756
+ }
757
+ if (tokens[idx]?.kind === "rparen") idx++;
758
+ }
759
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
760
+ if (tokens[idx]?.kind === "rbracket") idx++;
761
+ return {
762
+ code: {
763
+ kind: "permanentFlags",
764
+ flags
765
+ },
766
+ nextIndex: idx
767
+ };
768
+ }
769
+ if (codeName === "UIDVALIDITY") {
770
+ const valTok = tokens[idx];
771
+ const value = valTok?.kind === "number" ? valTok.value : 0;
772
+ idx++;
773
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
774
+ if (tokens[idx]?.kind === "rbracket") idx++;
775
+ return {
776
+ code: {
777
+ kind: "uidValidity",
778
+ value
779
+ },
780
+ nextIndex: idx
781
+ };
782
+ }
783
+ if (codeName === "UIDNEXT") {
784
+ const valTok = tokens[idx];
785
+ const value = valTok?.kind === "number" ? valTok.value : 0;
786
+ idx++;
787
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
788
+ if (tokens[idx]?.kind === "rbracket") idx++;
789
+ return {
790
+ code: {
791
+ kind: "uidNext",
792
+ value
793
+ },
794
+ nextIndex: idx
795
+ };
796
+ }
797
+ if (codeName === "READ-ONLY") {
798
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
799
+ if (tokens[idx]?.kind === "rbracket") idx++;
800
+ return {
801
+ code: { kind: "readOnly" },
802
+ nextIndex: idx
803
+ };
804
+ }
805
+ if (codeName === "READ-WRITE") {
806
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
807
+ if (tokens[idx]?.kind === "rbracket") idx++;
808
+ return {
809
+ code: { kind: "readWrite" },
810
+ nextIndex: idx
811
+ };
812
+ }
813
+ if (codeName === "ALERT") {
814
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") idx++;
815
+ if (tokens[idx]?.kind === "rbracket") idx++;
816
+ return {
817
+ code: { kind: "alert" },
818
+ nextIndex: idx
819
+ };
820
+ }
821
+ const args = [];
822
+ while (idx < tokens.length && tokens[idx]?.kind !== "rbracket") {
823
+ const str = tokenToString(tokens[idx]);
824
+ if (str !== null) args.push(str);
825
+ idx++;
826
+ }
827
+ if (tokens[idx]?.kind === "rbracket") idx++;
828
+ return {
829
+ code: {
830
+ kind: "other",
831
+ code: codeName,
832
+ args
833
+ },
834
+ nextIndex: idx
835
+ };
836
+ };
837
+ const parseResponse = (tokens) => {
838
+ if (tokens.length === 0) return {
839
+ ok: false,
840
+ reason: {
841
+ kind: "protocol",
842
+ detail: "Empty response line"
843
+ }
844
+ };
845
+ const first = tokens[0];
846
+ if (first === void 0) return {
847
+ ok: false,
848
+ reason: {
849
+ kind: "protocol",
850
+ detail: "Empty token stream"
851
+ }
852
+ };
853
+ if (first.kind === "plus") return {
854
+ ok: true,
855
+ value: {
856
+ kind: "continuation",
857
+ text: tokensToText(tokens, 1)
858
+ }
859
+ };
860
+ if (first.kind === "atom" && first.value === "*") {
861
+ const second = tokens[1];
862
+ if (second === void 0) return {
863
+ ok: false,
864
+ reason: {
865
+ kind: "protocol",
866
+ detail: "Bare * untagged marker"
867
+ }
868
+ };
869
+ if (second.kind === "atom") {
870
+ const upper = second.value.toUpperCase();
871
+ if (upper === "OK" || upper === "NO" || upper === "BAD" || upper === "BYE" || upper === "PREAUTH") {
872
+ let idx = 2;
873
+ let code = null;
874
+ if (tokens[idx]?.kind === "lbracket") {
875
+ const codeResult = parseResponseCode(tokens, idx);
876
+ if (codeResult !== null) {
877
+ code = codeResult.code;
878
+ idx = codeResult.nextIndex;
879
+ }
880
+ }
881
+ const text = tokensToText(tokens, idx);
882
+ return {
883
+ ok: true,
884
+ value: {
885
+ kind: "untagged",
886
+ untagged: {
887
+ kind: "status",
888
+ status: upper,
889
+ code,
890
+ text
891
+ }
892
+ }
893
+ };
894
+ }
895
+ if (upper === "CAPABILITY") {
896
+ const capabilities = [];
897
+ for (let i = 2; i < tokens.length; i++) {
898
+ const tok = tokens[i];
899
+ if (tok?.kind === "atom") capabilities.push(tok.value);
900
+ }
901
+ return {
902
+ ok: true,
903
+ value: {
904
+ kind: "untagged",
905
+ untagged: {
906
+ kind: "capability",
907
+ capabilities
908
+ }
909
+ }
910
+ };
911
+ }
912
+ if (upper === "FLAGS") {
913
+ const flags = [];
914
+ let idx = 2;
915
+ if (tokens[idx]?.kind === "lparen") {
916
+ idx++;
917
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") {
918
+ const tok = tokens[idx];
919
+ if (tok?.kind === "atom") flags.push(tok.value);
920
+ idx++;
921
+ }
922
+ }
923
+ return {
924
+ ok: true,
925
+ value: {
926
+ kind: "untagged",
927
+ untagged: {
928
+ kind: "flags",
929
+ flags
930
+ }
931
+ }
932
+ };
933
+ }
934
+ if (upper === "LIST") {
935
+ let idx = 2;
936
+ const attributes = [];
937
+ if (tokens[idx]?.kind === "lparen") {
938
+ idx++;
939
+ while (idx < tokens.length && tokens[idx]?.kind !== "rparen") {
940
+ const tok = tokens[idx];
941
+ if (tok?.kind === "atom") attributes.push(tok.value);
942
+ idx++;
943
+ }
944
+ if (tokens[idx]?.kind === "rparen") idx++;
945
+ }
946
+ const delimiter = tokenToString(tokens[idx]);
947
+ idx++;
948
+ return {
949
+ ok: true,
950
+ value: {
951
+ kind: "untagged",
952
+ untagged: {
953
+ kind: "list",
954
+ mailbox: {
955
+ name: decodeModifiedUtf7(tokenToString(tokens[idx]) ?? ""),
956
+ delimiter,
957
+ attributes
958
+ }
959
+ }
960
+ }
961
+ };
962
+ }
963
+ if (upper === "SEARCH") {
964
+ const uids = [];
965
+ for (let i = 2; i < tokens.length; i++) {
966
+ const tok = tokens[i];
967
+ if (tok?.kind === "number") uids.push(tok.value);
968
+ }
969
+ return {
970
+ ok: true,
971
+ value: {
972
+ kind: "untagged",
973
+ untagged: {
974
+ kind: "search",
975
+ uids
976
+ }
977
+ }
978
+ };
979
+ }
980
+ }
981
+ if (second.kind === "number") {
982
+ const num = second.value;
983
+ const third = tokens[2];
984
+ if (third?.kind === "atom") {
985
+ const verb = third.value.toUpperCase();
986
+ if (verb === "EXISTS") return {
987
+ ok: true,
988
+ value: {
989
+ kind: "untagged",
990
+ untagged: {
991
+ kind: "exists",
992
+ count: num
993
+ }
994
+ }
995
+ };
996
+ if (verb === "RECENT") return {
997
+ ok: true,
998
+ value: {
999
+ kind: "untagged",
1000
+ untagged: {
1001
+ kind: "recent",
1002
+ count: num
1003
+ }
1004
+ }
1005
+ };
1006
+ if (verb === "EXPUNGE") return {
1007
+ ok: true,
1008
+ value: {
1009
+ kind: "untagged",
1010
+ untagged: {
1011
+ kind: "expunge",
1012
+ seq: num
1013
+ }
1014
+ }
1015
+ };
1016
+ if (verb === "FETCH") return {
1017
+ ok: true,
1018
+ value: {
1019
+ kind: "untagged",
1020
+ untagged: {
1021
+ kind: "fetch",
1022
+ seq: num,
1023
+ items: parseFetchItems(tokens, 3).items
1024
+ }
1025
+ }
1026
+ };
1027
+ }
1028
+ }
1029
+ const name = second.kind === "atom" ? second.value : String(tokenToString(second));
1030
+ const rest = [];
1031
+ for (let i = 2; i < tokens.length; i++) {
1032
+ const str = tokenToString(tokens[i]);
1033
+ if (str !== null) rest.push(str);
1034
+ }
1035
+ return {
1036
+ ok: true,
1037
+ value: {
1038
+ kind: "untagged",
1039
+ untagged: {
1040
+ kind: "unknown",
1041
+ name,
1042
+ rest
1043
+ }
1044
+ }
1045
+ };
1046
+ }
1047
+ if (first.kind === "atom") {
1048
+ const tag = first.value;
1049
+ const second = tokens[1];
1050
+ if (second?.kind === "atom") {
1051
+ const statusUpper = second.value.toUpperCase();
1052
+ if (statusUpper === "OK" || statusUpper === "NO" || statusUpper === "BAD") {
1053
+ let idx = 2;
1054
+ let code = null;
1055
+ if (tokens[idx]?.kind === "lbracket") {
1056
+ const codeResult = parseResponseCode(tokens, idx);
1057
+ if (codeResult !== null) {
1058
+ code = codeResult.code;
1059
+ idx = codeResult.nextIndex;
1060
+ }
1061
+ }
1062
+ const text = tokensToText(tokens, idx);
1063
+ return {
1064
+ ok: true,
1065
+ value: {
1066
+ kind: "tagged",
1067
+ tag,
1068
+ status: statusUpper,
1069
+ code,
1070
+ text
1071
+ }
1072
+ };
1073
+ }
1074
+ }
1075
+ }
1076
+ return {
1077
+ ok: false,
1078
+ reason: {
1079
+ kind: "protocol",
1080
+ detail: `Unrecognised IMAP response line starting with ${first.kind}`
1081
+ }
1082
+ };
1083
+ };
1084
+ //#endregion
1085
+ //#region src/tokenizer.ts
1086
+ /**
1087
+ * Hand-written tokenizer over byte offsets for IMAP4rev2/rev1 stream.
1088
+ *
1089
+ * Implements total parsing over attacker-controlled bytes:
1090
+ * - Line ends at CRLF; bare LF is a protocol failure.
1091
+ * - Logical line frames `{n}` literals (and `{n+}` LITERAL+).
1092
+ * - Enforces maxLiteralBytes (default 32 MiB).
1093
+ * - A number above 2^32 - 1 is an atom of digits, not a failure: RFC 9051's `number` is
1094
+ * 32-bit, but Gmail's X-GM-THRID is 64-bit and arrives as a bare number.
1095
+ * - Never throws on malformed input; returns typed ImapResult.
1096
+ */
1097
+ const DEFAULT_MAX_LITERAL_BYTES = 32 * 1024 * 1024;
1098
+ const MAX_UINT32 = 4294967295;
1099
+ /**
1100
+ * Checks for bare LF in a slice of bytes.
1101
+ */
1102
+ const checkBareLf = (bytes, start, end) => {
1103
+ for (let i = start; i < end; i++) if (bytes[i] === 10 && (i === 0 || bytes[i - 1] !== 13)) return {
1104
+ kind: "protocol",
1105
+ detail: `Bare LF at index ${i}`
1106
+ };
1107
+ return null;
1108
+ };
1109
+ /**
1110
+ * Parses literal length from `{<digits>}` or `{<digits>+}` before CRLF.
1111
+ * Returns valid length, invalid failure, or none if not a literal header.
1112
+ */
1113
+ const parseLiteralHeader = (bytes, textStart, crlfIndex, maxLiteralBytes) => {
1114
+ if (crlfIndex === 0) return { kind: "none" };
1115
+ let i = crlfIndex - 1;
1116
+ if (bytes[i] !== 125) return { kind: "none" };
1117
+ i--;
1118
+ if (i >= textStart && bytes[i] === 43) i--;
1119
+ const digitsEnd = i + 1;
1120
+ while (i >= textStart && isDigit(bytes[i] ?? 0)) i--;
1121
+ if (i < textStart || bytes[i] !== 123) {
1122
+ for (let j = crlfIndex - 2; j >= textStart; j--) if (bytes[j] === 123) return {
1123
+ kind: "invalid",
1124
+ detail: "Invalid literal header syntax"
1125
+ };
1126
+ return { kind: "none" };
1127
+ }
1128
+ const braceIndex = i;
1129
+ const digitsStart = i + 1;
1130
+ if (digitsStart >= digitsEnd) return {
1131
+ kind: "invalid",
1132
+ detail: "Empty literal length in literal header"
1133
+ };
1134
+ const digitsStr = asciiToString(bytes.slice(digitsStart, digitsEnd));
1135
+ const num = Number.parseInt(digitsStr, 10);
1136
+ if (!Number.isSafeInteger(num) || num > MAX_UINT32) return {
1137
+ kind: "invalid",
1138
+ detail: `Literal length ${digitsStr} is not a valid 32-bit unsigned integer`
1139
+ };
1140
+ if (num > maxLiteralBytes) return {
1141
+ kind: "invalid",
1142
+ detail: `Literal of ${num} bytes exceeds maxLiteralBytes (${maxLiteralBytes})`
1143
+ };
1144
+ return {
1145
+ kind: "valid",
1146
+ length: num,
1147
+ braceIndex
1148
+ };
1149
+ };
1150
+ /**
1151
+ * Scans `buffer` for a complete logical line (including any literals).
1152
+ */
1153
+ const readLogicalLine = (buffer, maxLiteralBytes = DEFAULT_MAX_LITERAL_BYTES, resume) => {
1154
+ const segments = resume?.segments ? [...resume.segments] : [];
1155
+ let scanOffset = resume?.resumeAt ?? 0;
1156
+ let textStart = resume?.textStart ?? resume?.resumeAt ?? 0;
1157
+ if (resume?.pendingLiteral !== void 0) {
1158
+ const { start, end } = resume.pendingLiteral;
1159
+ if (buffer.length < end) return {
1160
+ status: "incomplete",
1161
+ resumeAt: scanOffset,
1162
+ needBytes: end,
1163
+ segments,
1164
+ textStart,
1165
+ pendingLiteral: resume.pendingLiteral
1166
+ };
1167
+ segments.push({
1168
+ kind: "literal",
1169
+ bytes: buffer.slice(start, end)
1170
+ });
1171
+ scanOffset = end;
1172
+ textStart = end;
1173
+ }
1174
+ while (scanOffset < buffer.length) {
1175
+ let crlfIndex = -1;
1176
+ for (let i = scanOffset; i < buffer.length - 1; i++) if (buffer[i] === 13 && buffer[i + 1] === 10) {
1177
+ crlfIndex = i;
1178
+ break;
1179
+ }
1180
+ if (crlfIndex === -1) {
1181
+ const lfFailure = checkBareLf(buffer, scanOffset, buffer.length);
1182
+ if (lfFailure !== null) return {
1183
+ status: "failure",
1184
+ failure: lfFailure
1185
+ };
1186
+ return {
1187
+ status: "incomplete",
1188
+ resumeAt: Math.max(textStart, buffer.length - 1),
1189
+ needBytes: buffer.length + 1,
1190
+ segments,
1191
+ textStart
1192
+ };
1193
+ }
1194
+ const lfFailure = checkBareLf(buffer, scanOffset, crlfIndex);
1195
+ if (lfFailure !== null) return {
1196
+ status: "failure",
1197
+ failure: lfFailure
1198
+ };
1199
+ const headerResult = parseLiteralHeader(buffer, textStart, crlfIndex, maxLiteralBytes);
1200
+ if (headerResult.kind === "invalid") return {
1201
+ status: "failure",
1202
+ failure: {
1203
+ kind: "protocol",
1204
+ detail: headerResult.detail
1205
+ }
1206
+ };
1207
+ if (headerResult.kind === "valid") {
1208
+ const braceIndex = headerResult.braceIndex;
1209
+ const literalLength = headerResult.length;
1210
+ if (braceIndex > textStart) segments.push({
1211
+ kind: "text",
1212
+ bytes: buffer.slice(textStart, braceIndex)
1213
+ });
1214
+ const literalDataStart = crlfIndex + 2;
1215
+ const literalDataEnd = literalDataStart + literalLength;
1216
+ if (buffer.length < literalDataEnd) return {
1217
+ status: "incomplete",
1218
+ resumeAt: crlfIndex,
1219
+ needBytes: literalDataEnd,
1220
+ segments,
1221
+ textStart: literalDataEnd,
1222
+ pendingLiteral: {
1223
+ start: literalDataStart,
1224
+ end: literalDataEnd
1225
+ }
1226
+ };
1227
+ segments.push({
1228
+ kind: "literal",
1229
+ bytes: buffer.slice(literalDataStart, literalDataEnd)
1230
+ });
1231
+ scanOffset = literalDataEnd;
1232
+ textStart = literalDataEnd;
1233
+ } else {
1234
+ if (crlfIndex >= textStart) segments.push({
1235
+ kind: "text",
1236
+ bytes: buffer.slice(textStart, crlfIndex)
1237
+ });
1238
+ const consumedBytes = crlfIndex + 2;
1239
+ let rawText = "";
1240
+ for (const seg of segments) if (seg.kind === "text") rawText += asciiToString(seg.bytes);
1241
+ else rawText += `<literal:${seg.bytes.length}B>`;
1242
+ return {
1243
+ status: "complete",
1244
+ line: {
1245
+ segments,
1246
+ rawText
1247
+ },
1248
+ consumedBytes
1249
+ };
1250
+ }
1251
+ }
1252
+ return {
1253
+ status: "incomplete",
1254
+ resumeAt: Math.max(textStart, buffer.length - 1),
1255
+ needBytes: buffer.length + 1,
1256
+ segments,
1257
+ textStart
1258
+ };
1259
+ };
1260
+ /**
1261
+ * Tokenizes a text segment into IMAP tokens.
1262
+ */
1263
+ const tokenizeTextSegment = (bytes, tokens) => {
1264
+ let i = 0;
1265
+ const len = bytes.length;
1266
+ while (i < len) {
1267
+ const byte = bytes[i] ?? 0;
1268
+ if (byte === 32 || byte === 9 || byte === 13 || byte === 10) {
1269
+ i++;
1270
+ continue;
1271
+ }
1272
+ if (byte === 40) {
1273
+ tokens.push({ kind: "lparen" });
1274
+ i++;
1275
+ continue;
1276
+ }
1277
+ if (byte === 41) {
1278
+ tokens.push({ kind: "rparen" });
1279
+ i++;
1280
+ continue;
1281
+ }
1282
+ if (byte === 91) {
1283
+ tokens.push({ kind: "lbracket" });
1284
+ i++;
1285
+ continue;
1286
+ }
1287
+ if (byte === 93) {
1288
+ tokens.push({ kind: "rbracket" });
1289
+ i++;
1290
+ continue;
1291
+ }
1292
+ if (byte === 34) {
1293
+ i++;
1294
+ let quotedStr = "";
1295
+ let isTerminated = false;
1296
+ while (i < len) {
1297
+ const qb = bytes[i] ?? 0;
1298
+ if (qb === 10 && (i === 0 || bytes[i - 1] !== 13)) return {
1299
+ ok: false,
1300
+ reason: {
1301
+ kind: "protocol",
1302
+ detail: "Bare LF in quoted string"
1303
+ }
1304
+ };
1305
+ if (qb === 92) {
1306
+ i++;
1307
+ if (i >= len) return {
1308
+ ok: false,
1309
+ reason: {
1310
+ kind: "protocol",
1311
+ detail: "Unterminated escape in quoted string"
1312
+ }
1313
+ };
1314
+ quotedStr += String.fromCharCode(bytes[i] ?? 0);
1315
+ i++;
1316
+ } else if (qb === 34) {
1317
+ isTerminated = true;
1318
+ i++;
1319
+ break;
1320
+ } else {
1321
+ quotedStr += String.fromCharCode(qb);
1322
+ i++;
1323
+ }
1324
+ }
1325
+ if (!isTerminated) return {
1326
+ ok: false,
1327
+ reason: {
1328
+ kind: "protocol",
1329
+ detail: "Unterminated quoted string"
1330
+ }
1331
+ };
1332
+ tokens.push({
1333
+ kind: "quoted",
1334
+ value: quotedStr
1335
+ });
1336
+ continue;
1337
+ }
1338
+ if (byte === 43) {
1339
+ const next = i + 1 < len ? bytes[i + 1] ?? 0 : 0;
1340
+ if (!(next !== 0 && next !== 32 && next !== 9 && next !== 40 && next !== 41 && next !== 91 && next !== 93 && next !== 13 && next !== 10)) {
1341
+ tokens.push({ kind: "plus" });
1342
+ i++;
1343
+ continue;
1344
+ }
1345
+ }
1346
+ const atomStart = i;
1347
+ while (i < len) {
1348
+ const b = bytes[i] ?? 0;
1349
+ if (b === 32 || b === 9 || b === 13 || b === 10 || b === 40 || b === 41 || b === 91 || b === 93 || b === 34) break;
1350
+ i++;
1351
+ }
1352
+ const atomStr = asciiToString(bytes.slice(atomStart, i));
1353
+ if (atomStr.toUpperCase() === "NIL") tokens.push({ kind: "nil" });
1354
+ else if (/^\d+$/.test(atomStr)) {
1355
+ const num = Number.parseInt(atomStr, 10);
1356
+ if (num > MAX_UINT32) tokens.push({
1357
+ kind: "atom",
1358
+ value: atomStr
1359
+ });
1360
+ else tokens.push({
1361
+ kind: "number",
1362
+ value: num
1363
+ });
1364
+ } else tokens.push({
1365
+ kind: "atom",
1366
+ value: atomStr
1367
+ });
1368
+ }
1369
+ return {
1370
+ ok: true,
1371
+ value: void 0
1372
+ };
1373
+ };
1374
+ /**
1375
+ * Tokenizes a complete LogicalLine into ImapTokens.
1376
+ */
1377
+ const tokenizeLogicalLine = (line) => {
1378
+ const tokens = [];
1379
+ for (const seg of line.segments) if (seg.kind === "literal") tokens.push({
1380
+ kind: "literal",
1381
+ value: seg.bytes
1382
+ });
1383
+ else {
1384
+ const result = tokenizeTextSegment(seg.bytes, tokens);
1385
+ if (!result.ok) return result;
1386
+ }
1387
+ return {
1388
+ ok: true,
1389
+ value: tokens
1390
+ };
1391
+ };
1392
+ //#endregion
1393
+ //#region src/client.ts
1394
+ /**
1395
+ * The msg-ids out of a `HEADER.FIELDS (REFERENCES)` section: the header unfolded, then every
1396
+ * `<...>` in order. A truncated or absent header is simply fewer ids.
1397
+ */
1398
+ const parseReferencesHeader = (bytes) => {
1399
+ if (bytes === null) return [];
1400
+ const line = asciiToString(bytes).replace(/\r?\n[ \t]+/g, " ").split(/\r?\n/).find((l) => /^references:/i.test(l));
1401
+ if (line === void 0) return [];
1402
+ return line.slice(line.indexOf(":") + 1).match(/<[^<>\s]+>/g) ?? [];
1403
+ };
1404
+ /**
1405
+ * The summaries in a FETCH response's untagged lines, whether the command asked by uid or by
1406
+ * sequence number: `* <seq> FETCH (UID n …)` carries both either way.
1407
+ */
1408
+ const summariesFrom = (untagged) => untagged.flatMap((item) => {
1409
+ if (item.kind !== "fetch") return [];
1410
+ let uid = 0;
1411
+ let flags = [];
1412
+ let internalDate = null;
1413
+ let size = null;
1414
+ let envelope = null;
1415
+ let references = [];
1416
+ let gmailThreadId = null;
1417
+ for (const fItem of item.items) if (fItem.kind === "uid") uid = fItem.uid;
1418
+ else if (fItem.kind === "flags") flags = fItem.flags;
1419
+ else if (fItem.kind === "internalDate") internalDate = fItem.date;
1420
+ else if (fItem.kind === "size") size = fItem.size;
1421
+ else if (fItem.kind === "envelope") envelope = fItem.envelope;
1422
+ else if (fItem.kind === "gmailThreadId") gmailThreadId = fItem.id;
1423
+ else if (fItem.kind === "body" && fItem.section.toUpperCase().startsWith("HEADER.FIELDS")) references = parseReferencesHeader(fItem.bytes);
1424
+ return [{
1425
+ seq: item.seq,
1426
+ uid,
1427
+ flags,
1428
+ internalDate,
1429
+ size,
1430
+ envelope,
1431
+ references,
1432
+ gmailThreadId
1433
+ }];
1434
+ });
1435
+ const createImapClient = (transport, options) => {
1436
+ const maxLiteralBytes = options?.maxLiteralBytes ?? 33554432;
1437
+ const onUntagged = options?.onUntagged;
1438
+ let chunks = [];
1439
+ let totalBufferedBytes = 0;
1440
+ let resumeState;
1441
+ let isClosed = false;
1442
+ let failureReason = null;
1443
+ let knownCapabilities = [];
1444
+ let tagCounter = 1;
1445
+ const nextTag = () => `A${String(tagCounter++).padStart(4, "0")}`;
1446
+ /**
1447
+ * Reads and parses the next complete IMAP response from the transport.
1448
+ */
1449
+ const readNextResponse = async () => {
1450
+ if (failureReason !== null) return {
1451
+ ok: false,
1452
+ reason: failureReason
1453
+ };
1454
+ if (isClosed) return {
1455
+ ok: false,
1456
+ reason: { kind: "closed" }
1457
+ };
1458
+ while (true) {
1459
+ if (resumeState === void 0 || totalBufferedBytes >= resumeState.needBytes) {
1460
+ const buffer = chunks.length === 1 && chunks[0] !== void 0 ? chunks[0] : concatByteArrays(chunks);
1461
+ const lineResult = readLogicalLine(buffer, maxLiteralBytes, resumeState);
1462
+ if (lineResult.status === "failure") {
1463
+ isClosed = true;
1464
+ failureReason = lineResult.failure;
1465
+ return {
1466
+ ok: false,
1467
+ reason: lineResult.failure
1468
+ };
1469
+ }
1470
+ if (lineResult.status === "complete") {
1471
+ const remainingBytes = buffer.length - lineResult.consumedBytes;
1472
+ if (remainingBytes > 0) {
1473
+ chunks = [buffer.slice(lineResult.consumedBytes)];
1474
+ totalBufferedBytes = remainingBytes;
1475
+ } else {
1476
+ chunks = [];
1477
+ totalBufferedBytes = 0;
1478
+ }
1479
+ resumeState = void 0;
1480
+ const tokenResult = tokenizeLogicalLine(lineResult.line);
1481
+ if (!tokenResult.ok) {
1482
+ isClosed = true;
1483
+ failureReason = tokenResult.reason;
1484
+ return {
1485
+ ok: false,
1486
+ reason: tokenResult.reason
1487
+ };
1488
+ }
1489
+ const parseResult = parseResponse(tokenResult.value);
1490
+ if (!parseResult.ok) {
1491
+ isClosed = true;
1492
+ failureReason = parseResult.reason;
1493
+ return {
1494
+ ok: false,
1495
+ reason: parseResult.reason
1496
+ };
1497
+ }
1498
+ return parseResult;
1499
+ }
1500
+ resumeState = lineResult;
1501
+ }
1502
+ const chunk = await transport.read().catch(() => null);
1503
+ if (chunk === null) {
1504
+ isClosed = true;
1505
+ failureReason = { kind: "closed" };
1506
+ return {
1507
+ ok: false,
1508
+ reason: { kind: "closed" }
1509
+ };
1510
+ }
1511
+ chunks.push(chunk);
1512
+ totalBufferedBytes += chunk.length;
1513
+ }
1514
+ };
1515
+ /**
1516
+ * Greeting is captured immediately as soon as client is created.
1517
+ */
1518
+ const greetingPromise = (async () => {
1519
+ const res = await readNextResponse();
1520
+ if (!res.ok) return res;
1521
+ if (res.value.kind === "untagged" && res.value.untagged.kind === "status") {
1522
+ const status = res.value.untagged.status;
1523
+ if (status === "OK" || status === "PREAUTH") {
1524
+ const code = res.value.untagged.code;
1525
+ const caps = code !== null && code.kind === "capability" ? code.capabilities : null;
1526
+ if (caps !== null) knownCapabilities = [...caps];
1527
+ return {
1528
+ ok: true,
1529
+ value: {
1530
+ text: res.value.untagged.text,
1531
+ capabilities: caps
1532
+ }
1533
+ };
1534
+ }
1535
+ if (status === "BYE") {
1536
+ const reason = {
1537
+ kind: "bye",
1538
+ text: res.value.untagged.text
1539
+ };
1540
+ isClosed = true;
1541
+ failureReason = reason;
1542
+ return {
1543
+ ok: false,
1544
+ reason
1545
+ };
1546
+ }
1547
+ }
1548
+ return {
1549
+ ok: false,
1550
+ reason: {
1551
+ kind: "protocol",
1552
+ detail: "Invalid IMAP greeting received from server"
1553
+ }
1554
+ };
1555
+ })();
1556
+ let commandQueue = Promise.resolve();
1557
+ const enqueueCommand = (task) => {
1558
+ const result = commandQueue.then(task);
1559
+ commandQueue = result.then(() => void 0, () => void 0);
1560
+ return result.catch(() => ({
1561
+ ok: false,
1562
+ reason: { kind: "closed" }
1563
+ }));
1564
+ };
1565
+ /**
1566
+ * Sends a command and collects untagged responses until matching tagged response.
1567
+ */
1568
+ const executeCommand = async (build, options) => {
1569
+ if (isClosed) return {
1570
+ ok: false,
1571
+ reason: { kind: "closed" }
1572
+ };
1573
+ if (failureReason !== null) return {
1574
+ ok: false,
1575
+ reason: failureReason.kind === "protocol" ? { kind: "closed" } : failureReason
1576
+ };
1577
+ const greetRes = await greetingPromise;
1578
+ if (!greetRes.ok) return greetRes;
1579
+ const tag = nextTag();
1580
+ const command = build(tag);
1581
+ const hasLiteralPlus = knownCapabilities.some((c) => c.toUpperCase() === "LITERAL+");
1582
+ const hasLiteralMinus = knownCapabilities.some((c) => c.toUpperCase() === "LITERAL-");
1583
+ const untaggedList = [];
1584
+ let seenCapability = false;
1585
+ let allowedByeReason = null;
1586
+ for (const line of command.lines) {
1587
+ try {
1588
+ await transport.write(line.text);
1589
+ } catch {
1590
+ isClosed = true;
1591
+ failureReason = { kind: "closed" };
1592
+ return {
1593
+ ok: false,
1594
+ reason: { kind: "closed" }
1595
+ };
1596
+ }
1597
+ if (line.literal !== void 0) {
1598
+ const len = line.literal.length;
1599
+ if (hasLiteralPlus || hasLiteralMinus && len <= 4096) try {
1600
+ await transport.write(stringToBytes(`{${len}+}\r\n`));
1601
+ await transport.write(line.literal);
1602
+ } catch {
1603
+ isClosed = true;
1604
+ failureReason = { kind: "closed" };
1605
+ return {
1606
+ ok: false,
1607
+ reason: { kind: "closed" }
1608
+ };
1609
+ }
1610
+ else {
1611
+ try {
1612
+ await transport.write(stringToBytes(`{${len}}\r\n`));
1613
+ } catch {
1614
+ isClosed = true;
1615
+ failureReason = { kind: "closed" };
1616
+ return {
1617
+ ok: false,
1618
+ reason: { kind: "closed" }
1619
+ };
1620
+ }
1621
+ let continuationReceived = false;
1622
+ while (!continuationReceived) {
1623
+ const respResult = await readNextResponse();
1624
+ if (!respResult.ok) {
1625
+ if (respResult.reason.kind === "closed" && allowedByeReason !== null) return {
1626
+ ok: false,
1627
+ reason: allowedByeReason
1628
+ };
1629
+ return respResult;
1630
+ }
1631
+ const resp = respResult.value;
1632
+ if (resp.kind === "untagged") {
1633
+ untaggedList.push(resp.untagged);
1634
+ onUntagged?.(resp.untagged);
1635
+ if (resp.untagged.kind === "capability") {
1636
+ knownCapabilities = [...resp.untagged.capabilities];
1637
+ seenCapability = true;
1638
+ }
1639
+ if (resp.untagged.kind === "status" && resp.untagged.status === "BYE") {
1640
+ if (options?.allowBye !== true) {
1641
+ const reason = {
1642
+ kind: "bye",
1643
+ text: resp.untagged.text
1644
+ };
1645
+ isClosed = true;
1646
+ failureReason = reason;
1647
+ return {
1648
+ ok: false,
1649
+ reason
1650
+ };
1651
+ }
1652
+ allowedByeReason = {
1653
+ kind: "bye",
1654
+ text: resp.untagged.text
1655
+ };
1656
+ }
1657
+ } else if (resp.kind === "continuation") {
1658
+ continuationReceived = true;
1659
+ try {
1660
+ await transport.write(line.literal);
1661
+ } catch {
1662
+ isClosed = true;
1663
+ failureReason = { kind: "closed" };
1664
+ return {
1665
+ ok: false,
1666
+ reason: { kind: "closed" }
1667
+ };
1668
+ }
1669
+ } else if (resp.kind === "tagged") {
1670
+ if (resp.tag !== tag) {
1671
+ isClosed = true;
1672
+ failureReason = {
1673
+ kind: "protocol",
1674
+ detail: `Received unexpected tag ${resp.tag}, expected ${tag}`
1675
+ };
1676
+ return {
1677
+ ok: false,
1678
+ reason: failureReason
1679
+ };
1680
+ }
1681
+ if (resp.status === "NO") return {
1682
+ ok: false,
1683
+ reason: {
1684
+ kind: "no",
1685
+ text: resp.text
1686
+ }
1687
+ };
1688
+ if (resp.status === "BAD") return {
1689
+ ok: false,
1690
+ reason: {
1691
+ kind: "bad",
1692
+ text: resp.text
1693
+ }
1694
+ };
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+ }
1700
+ while (true) {
1701
+ const respResult = await readNextResponse();
1702
+ if (!respResult.ok) {
1703
+ if (respResult.reason.kind === "closed" && allowedByeReason !== null) return {
1704
+ ok: false,
1705
+ reason: allowedByeReason
1706
+ };
1707
+ return respResult;
1708
+ }
1709
+ const resp = respResult.value;
1710
+ if (resp.kind === "untagged") {
1711
+ untaggedList.push(resp.untagged);
1712
+ onUntagged?.(resp.untagged);
1713
+ if (resp.untagged.kind === "capability") {
1714
+ knownCapabilities = [...resp.untagged.capabilities];
1715
+ seenCapability = true;
1716
+ }
1717
+ if (resp.untagged.kind === "status" && resp.untagged.status === "BYE") {
1718
+ if (options?.allowBye !== true) {
1719
+ const reason = {
1720
+ kind: "bye",
1721
+ text: resp.untagged.text
1722
+ };
1723
+ isClosed = true;
1724
+ failureReason = reason;
1725
+ return {
1726
+ ok: false,
1727
+ reason
1728
+ };
1729
+ }
1730
+ allowedByeReason = {
1731
+ kind: "bye",
1732
+ text: resp.untagged.text
1733
+ };
1734
+ }
1735
+ } else if (resp.kind === "continuation") if (options?.onContinuation !== void 0) {
1736
+ const contRes = await options.onContinuation();
1737
+ if (!contRes.ok) return contRes;
1738
+ } else {
1739
+ isClosed = true;
1740
+ failureReason = {
1741
+ kind: "protocol",
1742
+ detail: "Unexpected continuation response from server"
1743
+ };
1744
+ return {
1745
+ ok: false,
1746
+ reason: failureReason
1747
+ };
1748
+ }
1749
+ else if (resp.kind === "tagged") {
1750
+ if (resp.tag !== tag) {
1751
+ isClosed = true;
1752
+ failureReason = {
1753
+ kind: "protocol",
1754
+ detail: `Received unexpected tag ${resp.tag}, expected ${tag}`
1755
+ };
1756
+ return {
1757
+ ok: false,
1758
+ reason: failureReason
1759
+ };
1760
+ }
1761
+ if (resp.code !== null && resp.code.kind === "capability") {
1762
+ knownCapabilities = [...resp.code.capabilities];
1763
+ seenCapability = true;
1764
+ }
1765
+ if (resp.status === "OK") return {
1766
+ ok: true,
1767
+ value: {
1768
+ tagged: resp,
1769
+ untagged: untaggedList,
1770
+ seenCapability
1771
+ }
1772
+ };
1773
+ if (resp.status === "NO") return {
1774
+ ok: false,
1775
+ reason: {
1776
+ kind: "no",
1777
+ text: resp.text
1778
+ }
1779
+ };
1780
+ if (resp.status === "BAD") return {
1781
+ ok: false,
1782
+ reason: {
1783
+ kind: "bad",
1784
+ text: resp.text
1785
+ }
1786
+ };
1787
+ }
1788
+ }
1789
+ };
1790
+ return {
1791
+ greeting: () => greetingPromise,
1792
+ capability: () => enqueueCommand(async () => {
1793
+ const res = await executeCommand(buildCapabilityCommand);
1794
+ if (!res.ok) return res;
1795
+ return {
1796
+ ok: true,
1797
+ value: [...knownCapabilities]
1798
+ };
1799
+ }),
1800
+ capabilities: () => [...knownCapabilities],
1801
+ hasCapability: (name) => knownCapabilities.some((c) => c.toUpperCase() === name.toUpperCase()),
1802
+ authenticate: (username, password) => enqueueCommand(async () => {
1803
+ const greetRes = await greetingPromise;
1804
+ if (!greetRes.ok) return greetRes;
1805
+ if (knownCapabilities.length === 0) {
1806
+ const capRes = await executeCommand(buildCapabilityCommand);
1807
+ if (!capRes.ok) return capRes;
1808
+ }
1809
+ const hasAuthPlain = knownCapabilities.some((c) => c.toUpperCase() === "AUTH=PLAIN" || c.toUpperCase() === "AUTHENTICATE=PLAIN");
1810
+ const hasSaslIr = knownCapabilities.some((c) => c.toUpperCase() === "SASL-IR");
1811
+ const hasLoginDisabled = knownCapabilities.some((c) => c.toUpperCase() === "LOGINDISABLED");
1812
+ let authCmdRes;
1813
+ if (hasAuthPlain) if (hasSaslIr) authCmdRes = await executeCommand((tag) => buildAuthenticatePlainSaslIrCommand(tag, username, password));
1814
+ else authCmdRes = await executeCommand((tag) => buildAuthenticatePlainInitialCommand(tag), { onContinuation: async () => {
1815
+ try {
1816
+ const respCmd = buildAuthenticatePlainResponse(username, password);
1817
+ for (const line of respCmd.lines) await transport.write(line.text);
1818
+ return {
1819
+ ok: true,
1820
+ value: void 0
1821
+ };
1822
+ } catch {
1823
+ isClosed = true;
1824
+ failureReason = { kind: "closed" };
1825
+ return {
1826
+ ok: false,
1827
+ reason: { kind: "closed" }
1828
+ };
1829
+ }
1830
+ } });
1831
+ else if (!hasLoginDisabled) authCmdRes = await executeCommand((tag) => buildLoginCommand(tag, username, password));
1832
+ else return {
1833
+ ok: false,
1834
+ reason: {
1835
+ kind: "unsupported",
1836
+ detail: "Server does not support AUTH=PLAIN and LOGIN is disabled"
1837
+ }
1838
+ };
1839
+ if (!authCmdRes.ok) return authCmdRes;
1840
+ if (!authCmdRes.value.seenCapability) {
1841
+ const postAuthCapRes = await executeCommand(buildCapabilityCommand);
1842
+ if (!postAuthCapRes.ok) return postAuthCapRes;
1843
+ }
1844
+ return {
1845
+ ok: true,
1846
+ value: void 0
1847
+ };
1848
+ }),
1849
+ list: (reference, pattern) => enqueueCommand(async () => {
1850
+ const res = await executeCommand((tag) => buildListCommand(tag, reference, pattern));
1851
+ if (!res.ok) return res;
1852
+ const mailboxes = [];
1853
+ for (const item of res.value.untagged) if (item.kind === "list") mailboxes.push(item.mailbox);
1854
+ return {
1855
+ ok: true,
1856
+ value: mailboxes
1857
+ };
1858
+ }),
1859
+ select: (mailbox, options) => enqueueCommand(async () => {
1860
+ const isReadOnlyRequested = options?.readOnly ?? false;
1861
+ const res = await executeCommand((tag) => buildSelectCommand(tag, mailbox, isReadOnlyRequested));
1862
+ if (!res.ok) return res;
1863
+ let exists = 0;
1864
+ let flags = [];
1865
+ let permanentFlags = [];
1866
+ let uidValidity = null;
1867
+ let uidNext = null;
1868
+ let isReadOnly = isReadOnlyRequested;
1869
+ const applyCode = (code) => {
1870
+ if (code === null) return;
1871
+ if (code.kind === "uidValidity") uidValidity = code.value;
1872
+ else if (code.kind === "uidNext") uidNext = code.value;
1873
+ else if (code.kind === "permanentFlags") permanentFlags = [...code.flags];
1874
+ else if (code.kind === "readOnly") isReadOnly = true;
1875
+ else if (code.kind === "readWrite") isReadOnly = false;
1876
+ };
1877
+ for (const item of res.value.untagged) if (item.kind === "exists") exists = item.count;
1878
+ else if (item.kind === "flags") flags = [...item.flags];
1879
+ else if (item.kind === "status") applyCode(item.code);
1880
+ applyCode(res.value.tagged.code);
1881
+ return {
1882
+ ok: true,
1883
+ value: {
1884
+ name: mailbox,
1885
+ exists,
1886
+ uidValidity,
1887
+ uidNext,
1888
+ flags,
1889
+ permanentFlags,
1890
+ readOnly: isReadOnly
1891
+ }
1892
+ };
1893
+ }),
1894
+ fetchSummaries: (uidSet) => enqueueCommand(async () => {
1895
+ const gmail = knownCapabilities.some((c) => c.toUpperCase() === "X-GM-EXT-1");
1896
+ const res = await executeCommand((tag) => buildFetchSummariesCommand(tag, uidSet, { gmail }));
1897
+ if (!res.ok) return res;
1898
+ return {
1899
+ ok: true,
1900
+ value: summariesFrom(res.value.untagged)
1901
+ };
1902
+ }),
1903
+ fetchSummariesBySeq: (seqSet) => enqueueCommand(async () => {
1904
+ const gmail = knownCapabilities.some((c) => c.toUpperCase() === "X-GM-EXT-1");
1905
+ const res = await executeCommand((tag) => buildFetchSummariesCommand(tag, seqSet, {
1906
+ gmail,
1907
+ bySeq: true
1908
+ }));
1909
+ if (!res.ok) return res;
1910
+ return {
1911
+ ok: true,
1912
+ value: summariesFrom(res.value.untagged)
1913
+ };
1914
+ }),
1915
+ fetchFlags: (uidSet) => enqueueCommand(async () => {
1916
+ const res = await executeCommand((tag) => buildFetchFlagsCommand(tag, uidSet));
1917
+ if (!res.ok) return res;
1918
+ const result = [];
1919
+ for (const item of res.value.untagged) {
1920
+ if (item.kind !== "fetch") continue;
1921
+ let uid = 0;
1922
+ let flags = [];
1923
+ for (const fItem of item.items) if (fItem.kind === "uid") uid = fItem.uid;
1924
+ else if (fItem.kind === "flags") flags = fItem.flags;
1925
+ if (uid !== 0) result.push({
1926
+ uid,
1927
+ flags
1928
+ });
1929
+ }
1930
+ return {
1931
+ ok: true,
1932
+ value: result
1933
+ };
1934
+ }),
1935
+ fetchRaw: (uid) => enqueueCommand(async () => {
1936
+ const res = await executeCommand((tag) => buildFetchRawCommand(tag, uid));
1937
+ if (!res.ok) return res;
1938
+ for (const item of res.value.untagged) if (item.kind === "fetch") {
1939
+ for (const fItem of item.items) if (fItem.kind === "body" && fItem.bytes !== null) return {
1940
+ ok: true,
1941
+ value: fItem.bytes
1942
+ };
1943
+ }
1944
+ return {
1945
+ ok: false,
1946
+ reason: {
1947
+ kind: "no",
1948
+ text: "Raw message body not found"
1949
+ }
1950
+ };
1951
+ }),
1952
+ storeFlags: (uidSet, mode, flags) => enqueueCommand(async () => {
1953
+ const res = await executeCommand((tag) => buildStoreFlagsCommand(tag, uidSet, mode, flags));
1954
+ if (!res.ok) return res;
1955
+ return {
1956
+ ok: true,
1957
+ value: void 0
1958
+ };
1959
+ }),
1960
+ append: (mailbox, message, flags) => enqueueCommand(async () => {
1961
+ const res = await executeCommand((tag) => buildAppendCommand(tag, mailbox, flags, message));
1962
+ if (!res.ok) return res;
1963
+ return {
1964
+ ok: true,
1965
+ value: void 0
1966
+ };
1967
+ }),
1968
+ move: (uidSet, mailbox) => enqueueCommand(async () => {
1969
+ const greetRes = await greetingPromise;
1970
+ if (!greetRes.ok) return greetRes;
1971
+ if (!knownCapabilities.some((c) => {
1972
+ const name = c.toUpperCase();
1973
+ return name === "MOVE" || name === "IMAP4REV2";
1974
+ })) return {
1975
+ ok: false,
1976
+ reason: {
1977
+ kind: "no",
1978
+ text: "MOVE is not supported by this server"
1979
+ }
1980
+ };
1981
+ const res = await executeCommand((tag) => buildMoveCommand(tag, uidSet, mailbox));
1982
+ if (!res.ok) return res;
1983
+ return {
1984
+ ok: true,
1985
+ value: void 0
1986
+ };
1987
+ }),
1988
+ create: (mailbox) => enqueueCommand(async () => {
1989
+ const res = await executeCommand((tag) => buildCreateCommand(tag, mailbox));
1990
+ if (!res.ok) return res;
1991
+ return {
1992
+ ok: true,
1993
+ value: void 0
1994
+ };
1995
+ }),
1996
+ noop: () => enqueueCommand(async () => {
1997
+ const res = await executeCommand(buildNoopCommand);
1998
+ if (!res.ok) return res;
1999
+ return {
2000
+ ok: true,
2001
+ value: void 0
2002
+ };
2003
+ }),
2004
+ idle: () => {
2005
+ if (isClosed || failureReason !== null) {
2006
+ const ended = Promise.resolve({
2007
+ ok: false,
2008
+ reason: { kind: "closed" }
2009
+ });
2010
+ return {
2011
+ done: async () => ended,
2012
+ ended
2013
+ };
2014
+ }
2015
+ let doneRequested = false;
2016
+ let doneSent = false;
2017
+ /** True once '+' arrived; DONE may be written (including from `done()` while a read waits). */
2018
+ let isIdling = false;
2019
+ /** True once the IDLE has settled either way; a `done()` after that has nothing to end. */
2020
+ let isEnded = false;
2021
+ const sendDone = async () => {
2022
+ if (doneSent || !isIdling || isEnded) return null;
2023
+ doneSent = true;
2024
+ try {
2025
+ await transport.write(buildIdleDoneLine());
2026
+ return null;
2027
+ } catch {
2028
+ isClosed = true;
2029
+ failureReason = { kind: "closed" };
2030
+ return {
2031
+ ok: false,
2032
+ reason: { kind: "closed" }
2033
+ };
2034
+ }
2035
+ };
2036
+ const ended = enqueueCommand(async () => {
2037
+ if (failureReason !== null) return {
2038
+ ok: false,
2039
+ reason: failureReason.kind === "protocol" ? { kind: "closed" } : failureReason
2040
+ };
2041
+ if (isClosed) return {
2042
+ ok: false,
2043
+ reason: { kind: "closed" }
2044
+ };
2045
+ const greetRes = await greetingPromise;
2046
+ if (!greetRes.ok) return greetRes;
2047
+ const tag = nextTag();
2048
+ const command = buildIdleCommand(tag);
2049
+ for (const line of command.lines) try {
2050
+ await transport.write(line.text);
2051
+ } catch {
2052
+ isClosed = true;
2053
+ failureReason = { kind: "closed" };
2054
+ return {
2055
+ ok: false,
2056
+ reason: { kind: "closed" }
2057
+ };
2058
+ }
2059
+ while (true) {
2060
+ const respResult = await readNextResponse();
2061
+ if (!respResult.ok) return respResult;
2062
+ const resp = respResult.value;
2063
+ if (resp.kind === "untagged") {
2064
+ onUntagged?.(resp.untagged);
2065
+ if (resp.untagged.kind === "status" && resp.untagged.status === "BYE") {
2066
+ const reason = {
2067
+ kind: "bye",
2068
+ text: resp.untagged.text
2069
+ };
2070
+ isClosed = true;
2071
+ failureReason = reason;
2072
+ return {
2073
+ ok: false,
2074
+ reason
2075
+ };
2076
+ }
2077
+ continue;
2078
+ }
2079
+ if (resp.kind === "continuation") break;
2080
+ if (resp.kind === "tagged") {
2081
+ if (resp.tag !== tag) {
2082
+ isClosed = true;
2083
+ failureReason = {
2084
+ kind: "protocol",
2085
+ detail: `Received unexpected tag ${resp.tag}, expected ${tag}`
2086
+ };
2087
+ return {
2088
+ ok: false,
2089
+ reason: failureReason
2090
+ };
2091
+ }
2092
+ if (resp.status === "NO") return {
2093
+ ok: false,
2094
+ reason: {
2095
+ kind: "no",
2096
+ text: resp.text
2097
+ }
2098
+ };
2099
+ if (resp.status === "BAD") return {
2100
+ ok: false,
2101
+ reason: {
2102
+ kind: "bad",
2103
+ text: resp.text
2104
+ }
2105
+ };
2106
+ isClosed = true;
2107
+ failureReason = {
2108
+ kind: "protocol",
2109
+ detail: "IDLE completed without a continuation"
2110
+ };
2111
+ return {
2112
+ ok: false,
2113
+ reason: failureReason
2114
+ };
2115
+ }
2116
+ }
2117
+ isIdling = true;
2118
+ if (doneRequested) {
2119
+ const sendFail = await sendDone();
2120
+ if (sendFail !== null) return sendFail;
2121
+ }
2122
+ while (true) {
2123
+ const respResult = await readNextResponse();
2124
+ if (!respResult.ok) return respResult;
2125
+ const resp = respResult.value;
2126
+ if (resp.kind === "untagged") {
2127
+ onUntagged?.(resp.untagged);
2128
+ if (resp.untagged.kind === "status" && resp.untagged.status === "BYE") {
2129
+ const reason = {
2130
+ kind: "bye",
2131
+ text: resp.untagged.text
2132
+ };
2133
+ isClosed = true;
2134
+ failureReason = reason;
2135
+ return {
2136
+ ok: false,
2137
+ reason
2138
+ };
2139
+ }
2140
+ continue;
2141
+ }
2142
+ if (resp.kind === "continuation") {
2143
+ isClosed = true;
2144
+ failureReason = {
2145
+ kind: "protocol",
2146
+ detail: "Unexpected continuation while IDLE"
2147
+ };
2148
+ return {
2149
+ ok: false,
2150
+ reason: failureReason
2151
+ };
2152
+ }
2153
+ if (resp.kind === "tagged") {
2154
+ if (resp.tag !== tag) {
2155
+ isClosed = true;
2156
+ failureReason = {
2157
+ kind: "protocol",
2158
+ detail: `Received unexpected tag ${resp.tag}, expected ${tag}`
2159
+ };
2160
+ return {
2161
+ ok: false,
2162
+ reason: failureReason
2163
+ };
2164
+ }
2165
+ if (!doneSent) {
2166
+ isClosed = true;
2167
+ failureReason = {
2168
+ kind: "protocol",
2169
+ detail: "Tagged response while IDLE before DONE"
2170
+ };
2171
+ return {
2172
+ ok: false,
2173
+ reason: failureReason
2174
+ };
2175
+ }
2176
+ if (resp.status === "OK") return {
2177
+ ok: true,
2178
+ value: void 0
2179
+ };
2180
+ if (resp.status === "NO") return {
2181
+ ok: false,
2182
+ reason: {
2183
+ kind: "no",
2184
+ text: resp.text
2185
+ }
2186
+ };
2187
+ if (resp.status === "BAD") return {
2188
+ ok: false,
2189
+ reason: {
2190
+ kind: "bad",
2191
+ text: resp.text
2192
+ }
2193
+ };
2194
+ }
2195
+ }
2196
+ });
2197
+ ended.finally(() => {
2198
+ isEnded = true;
2199
+ });
2200
+ return {
2201
+ done: async () => {
2202
+ doneRequested = true;
2203
+ await sendDone();
2204
+ return ended;
2205
+ },
2206
+ ended
2207
+ };
2208
+ },
2209
+ logout: () => enqueueCommand(async () => {
2210
+ const res = await executeCommand(buildLogoutCommand, { allowBye: true });
2211
+ isClosed = true;
2212
+ if (!res.ok) return res;
2213
+ return {
2214
+ ok: true,
2215
+ value: void 0
2216
+ };
2217
+ })
2218
+ };
2219
+ };
2220
+ //#endregion
2221
+ export { DEFAULT_MAX_LITERAL_BYTES, createImapClient, decodeRfc2047, parseResponse };