@bobfrankston/iflow 1.0.33 → 1.0.37

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.
@@ -0,0 +1,586 @@
1
+ /**
2
+ * Native IMAP client — transport-agnostic.
3
+ * Uses ImapTransport for I/O, imap-protocol for parsing.
4
+ * Works with NodeTransport (desktop) or BridgeTransport (Android).
5
+ *
6
+ * This is a NEW client alongside the existing ImapClient (which wraps imapflow).
7
+ * Existing callers are not affected.
8
+ */
9
+ import * as proto from "./imap-protocol.js";
10
+ export class NativeImapClient {
11
+ transport;
12
+ transportFactory;
13
+ config;
14
+ buffer = "";
15
+ pendingCommand = null;
16
+ capabilities = new Set();
17
+ _connected = false;
18
+ idleTag = null;
19
+ idleCallback = null;
20
+ verbose;
21
+ selectedMailbox = null;
22
+ mailboxInfo = { exists: 0, recent: 0, uidNext: 0, uidValidity: 0, flags: [], permanentFlags: [] };
23
+ constructor(config, transportFactory) {
24
+ this.config = config;
25
+ this.transportFactory = transportFactory;
26
+ this.transport = transportFactory();
27
+ this.verbose = config.verbose || false;
28
+ }
29
+ get connected() { return this._connected; }
30
+ // ── Connection ──
31
+ async connect() {
32
+ const useTls = this.config.port === 993;
33
+ this.transport.onData((data) => this.handleData(data));
34
+ this.transport.onClose(() => { this._connected = false; });
35
+ this.transport.onError((err) => {
36
+ if (this.verbose)
37
+ console.error(` [imap] Transport error: ${err.message}`);
38
+ });
39
+ await this.transport.connect(this.config.server, this.config.port, useTls, this.config.server);
40
+ // Read server greeting
41
+ const greeting = await this.readGreeting();
42
+ if (this.verbose)
43
+ console.log(` [imap] Greeting: ${greeting.raw}`);
44
+ // Parse capabilities from greeting
45
+ if (greeting.text.includes("CAPABILITY")) {
46
+ this.parseCapabilities(greeting.text);
47
+ }
48
+ else {
49
+ await this.capability();
50
+ }
51
+ // STARTTLS if needed
52
+ if (!useTls && this.capabilities.has("STARTTLS")) {
53
+ await this.starttls();
54
+ }
55
+ this._connected = true;
56
+ // Authenticate
57
+ await this.authenticate();
58
+ }
59
+ async readGreeting() {
60
+ return new Promise((resolve) => {
61
+ const check = () => {
62
+ const lineEnd = this.buffer.indexOf("\r\n");
63
+ if (lineEnd >= 0) {
64
+ const line = this.buffer.substring(0, lineEnd + 2);
65
+ this.buffer = this.buffer.substring(lineEnd + 2);
66
+ resolve(proto.parseResponseLine(line));
67
+ }
68
+ else {
69
+ setTimeout(check, 10);
70
+ }
71
+ };
72
+ check();
73
+ });
74
+ }
75
+ async authenticate() {
76
+ if (this.config.tokenProvider) {
77
+ const token = await this.config.tokenProvider();
78
+ const tag = proto.nextTag();
79
+ const cmd = proto.xoauth2Command(tag, this.config.username, token);
80
+ if (this.verbose)
81
+ console.log(` [imap] > AUTHENTICATE XOAUTH2 ...`);
82
+ const responses = await this.sendCommand(tag, cmd);
83
+ const tagged = responses.find(r => r.tag === tag);
84
+ if (!tagged || tagged.type !== "OK") {
85
+ const errText = tagged?.text || responses.map(r => r.raw).join("; ");
86
+ throw new Error(`Authentication failed: ${errText}`);
87
+ }
88
+ }
89
+ else if (this.config.password) {
90
+ const tag = proto.nextTag();
91
+ const cmd = proto.loginCommand(tag, this.config.username, this.config.password);
92
+ if (this.verbose)
93
+ console.log(` [imap] > LOGIN ${this.config.username} ***`);
94
+ const responses = await this.sendCommand(tag, cmd);
95
+ const tagged = responses.find(r => r.tag === tag);
96
+ if (!tagged || tagged.type !== "OK") {
97
+ const errText = tagged?.text || responses.map(r => r.raw).join("; ");
98
+ throw new Error(`Login failed: ${errText}`);
99
+ }
100
+ }
101
+ else {
102
+ throw new Error("No password or token provider configured");
103
+ }
104
+ // Re-read capabilities after auth (they may change)
105
+ await this.capability();
106
+ }
107
+ async starttls() {
108
+ const tag = proto.nextTag();
109
+ const responses = await this.sendCommand(tag, proto.starttlsCommand(tag));
110
+ const tagged = responses.find(r => r.tag === tag);
111
+ if (!tagged || tagged.type !== "OK")
112
+ throw new Error("STARTTLS failed");
113
+ await this.transport.upgradeTLS(this.config.server);
114
+ await this.capability();
115
+ }
116
+ async capability() {
117
+ const tag = proto.nextTag();
118
+ const responses = await this.sendCommand(tag, proto.capabilityCommand(tag));
119
+ for (const r of responses) {
120
+ if (r.tag === "*" && r.type === "CAPABILITY") {
121
+ this.parseCapabilities(r.text);
122
+ }
123
+ }
124
+ return this.capabilities;
125
+ }
126
+ parseCapabilities(text) {
127
+ const caps = text.replace(/^CAPABILITY\s*/i, "").split(/\s+/);
128
+ this.capabilities.clear();
129
+ for (const c of caps)
130
+ this.capabilities.add(c.toUpperCase());
131
+ }
132
+ async logout() {
133
+ try {
134
+ if (this._connected) {
135
+ const tag = proto.nextTag();
136
+ await this.sendCommand(tag, proto.logoutCommand(tag));
137
+ }
138
+ }
139
+ catch { /* ignore */ }
140
+ this.transport.close();
141
+ this._connected = false;
142
+ }
143
+ // ── Mailbox Operations ──
144
+ async select(mailbox) {
145
+ const tag = proto.nextTag();
146
+ const responses = await this.sendCommand(tag, proto.selectCommand(tag, mailbox));
147
+ const tagged = responses.find(r => r.tag === tag);
148
+ if (!tagged || tagged.type !== "OK") {
149
+ throw new Error(`SELECT ${mailbox} failed: ${tagged?.text || "unknown"}`);
150
+ }
151
+ // Parse mailbox info from untagged responses
152
+ for (const r of responses) {
153
+ if (r.tag !== "*")
154
+ continue;
155
+ if (r.type === "EXISTS") {
156
+ this.mailboxInfo.exists = parseInt(r.text);
157
+ }
158
+ else if (r.type === "RECENT") {
159
+ this.mailboxInfo.recent = parseInt(r.text);
160
+ }
161
+ else if (r.type === "FLAGS") {
162
+ this.mailboxInfo.flags = [...proto.parseFlags(r.text)];
163
+ }
164
+ else if (r.type === "OK") {
165
+ const uidNextMatch = r.text.match(/UIDNEXT\s+(\d+)/i);
166
+ if (uidNextMatch)
167
+ this.mailboxInfo.uidNext = parseInt(uidNextMatch[1]);
168
+ const uidValMatch = r.text.match(/UIDVALIDITY\s+(\d+)/i);
169
+ if (uidValMatch)
170
+ this.mailboxInfo.uidValidity = parseInt(uidValMatch[1]);
171
+ const permMatch = r.text.match(/PERMANENTFLAGS\s+\(([^)]*)\)/i);
172
+ if (permMatch)
173
+ this.mailboxInfo.permanentFlags = permMatch[1].split(/\s+/).filter(Boolean);
174
+ }
175
+ }
176
+ this.selectedMailbox = mailbox;
177
+ return { ...this.mailboxInfo };
178
+ }
179
+ async examine(mailbox) {
180
+ const tag = proto.nextTag();
181
+ const responses = await this.sendCommand(tag, proto.examineCommand(tag, mailbox));
182
+ const tagged = responses.find(r => r.tag === tag);
183
+ if (!tagged || tagged.type !== "OK") {
184
+ throw new Error(`EXAMINE ${mailbox} failed: ${tagged?.text || "unknown"}`);
185
+ }
186
+ for (const r of responses) {
187
+ if (r.tag !== "*")
188
+ continue;
189
+ if (r.type === "EXISTS")
190
+ this.mailboxInfo.exists = parseInt(r.text);
191
+ else if (r.type === "RECENT")
192
+ this.mailboxInfo.recent = parseInt(r.text);
193
+ }
194
+ this.selectedMailbox = mailbox;
195
+ return { ...this.mailboxInfo };
196
+ }
197
+ /** Close the currently selected mailbox */
198
+ async closeMailbox() {
199
+ if (!this.selectedMailbox)
200
+ return;
201
+ const tag = proto.nextTag();
202
+ await this.sendCommand(tag, proto.buildCommand(tag, "CLOSE"));
203
+ this.selectedMailbox = null;
204
+ }
205
+ // ── Folder Operations ──
206
+ async listFolders() {
207
+ const tag = proto.nextTag();
208
+ const responses = await this.sendCommand(tag, proto.listCommand(tag));
209
+ const folders = [];
210
+ for (const r of responses) {
211
+ if (r.tag === "*" && r.type === "LIST") {
212
+ const parsed = proto.parseListResponse(r.text);
213
+ if (parsed)
214
+ folders.push(parsed);
215
+ }
216
+ }
217
+ return folders;
218
+ }
219
+ async getStatus(mailbox) {
220
+ const tag = proto.nextTag();
221
+ const responses = await this.sendCommand(tag, proto.statusCommand(tag, mailbox, ["MESSAGES", "UIDNEXT", "UNSEEN"]));
222
+ for (const r of responses) {
223
+ if (r.tag === "*" && r.type === "STATUS") {
224
+ const data = proto.parseStatusResponse(r.text);
225
+ if (data)
226
+ return data;
227
+ }
228
+ }
229
+ return {};
230
+ }
231
+ async createMailbox(mailbox) {
232
+ const tag = proto.nextTag();
233
+ const responses = await this.sendCommand(tag, proto.createCommand(tag, mailbox));
234
+ const tagged = responses.find(r => r.tag === tag);
235
+ if (!tagged || tagged.type !== "OK")
236
+ throw new Error(`CREATE failed: ${tagged?.text || "unknown"}`);
237
+ }
238
+ async deleteMailbox(mailbox) {
239
+ const tag = proto.nextTag();
240
+ const responses = await this.sendCommand(tag, proto.deleteMailboxCommand(tag, mailbox));
241
+ const tagged = responses.find(r => r.tag === tag);
242
+ if (!tagged || tagged.type !== "OK")
243
+ throw new Error(`DELETE failed: ${tagged?.text || "unknown"}`);
244
+ }
245
+ async renameMailbox(from, to) {
246
+ const tag = proto.nextTag();
247
+ const responses = await this.sendCommand(tag, proto.renameCommand(tag, from, to));
248
+ const tagged = responses.find(r => r.tag === tag);
249
+ if (!tagged || tagged.type !== "OK")
250
+ throw new Error(`RENAME failed: ${tagged?.text || "unknown"}`);
251
+ }
252
+ // ── Message Operations ──
253
+ /** Fetch messages by UID range */
254
+ async fetchMessages(range, options = {}) {
255
+ const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE"];
256
+ if (options.headers !== false)
257
+ items.push("BODY.PEEK[HEADER]");
258
+ if (options.source)
259
+ items.push("BODY.PEEK[]");
260
+ const tag = proto.nextTag();
261
+ const responses = await this.sendCommand(tag, proto.fetchCommand(tag, range, items));
262
+ return this.parseFetchResponses(responses);
263
+ }
264
+ /** Fetch messages since a UID */
265
+ async fetchSinceUid(sinceUid, options = {}) {
266
+ return this.fetchMessages(`${sinceUid + 1}:*`, options);
267
+ }
268
+ /** Fetch messages by date range */
269
+ async fetchByDate(since, before, options = {}) {
270
+ // First search for UIDs in date range, then fetch
271
+ const criteria = proto.buildSearchCriteria({
272
+ since,
273
+ before: before || undefined,
274
+ });
275
+ const uids = await this.search(criteria);
276
+ if (uids.length === 0)
277
+ return [];
278
+ // Fetch in chunks to avoid very long command lines
279
+ const chunkSize = 500;
280
+ const allMessages = [];
281
+ for (let i = 0; i < uids.length; i += chunkSize) {
282
+ const chunk = uids.slice(i, i + chunkSize);
283
+ const range = chunk.join(",");
284
+ const msgs = await this.fetchMessages(range, options);
285
+ allMessages.push(...msgs);
286
+ }
287
+ return allMessages;
288
+ }
289
+ /** Fetch a single message by UID */
290
+ async fetchMessage(uid, options = {}) {
291
+ const msgs = await this.fetchMessages(String(uid), options);
292
+ return msgs.find(m => m.uid === uid) || null;
293
+ }
294
+ /** Get all UIDs in the current mailbox */
295
+ async getUids() {
296
+ return this.search("ALL");
297
+ }
298
+ /** UID SEARCH */
299
+ async search(criteria) {
300
+ const tag = proto.nextTag();
301
+ const responses = await this.sendCommand(tag, proto.searchCommand(tag, criteria));
302
+ for (const r of responses) {
303
+ if (r.tag === "*" && r.type === "SEARCH") {
304
+ return proto.parseSearchResponse(r.text);
305
+ }
306
+ }
307
+ return [];
308
+ }
309
+ /** Set flags on a message */
310
+ async addFlags(uid, flags) {
311
+ const tag = proto.nextTag();
312
+ const responses = await this.sendCommand(tag, proto.storeCommand(tag, uid, "+FLAGS.SILENT", flags));
313
+ const tagged = responses.find(r => r.tag === tag);
314
+ if (!tagged || tagged.type !== "OK")
315
+ throw new Error(`STORE +FLAGS failed: ${tagged?.text || "unknown"}`);
316
+ }
317
+ /** Remove flags from a message */
318
+ async removeFlags(uid, flags) {
319
+ const tag = proto.nextTag();
320
+ const responses = await this.sendCommand(tag, proto.storeCommand(tag, uid, "-FLAGS.SILENT", flags));
321
+ const tagged = responses.find(r => r.tag === tag);
322
+ if (!tagged || tagged.type !== "OK")
323
+ throw new Error(`STORE -FLAGS failed: ${tagged?.text || "unknown"}`);
324
+ }
325
+ /** Copy a message to another mailbox */
326
+ async copyMessage(uid, destination) {
327
+ const tag = proto.nextTag();
328
+ const responses = await this.sendCommand(tag, proto.copyCommand(tag, uid, destination));
329
+ const tagged = responses.find(r => r.tag === tag);
330
+ if (!tagged || tagged.type !== "OK")
331
+ throw new Error(`COPY failed: ${tagged?.text || "unknown"}`);
332
+ }
333
+ /** Move a message to another mailbox (MOVE or COPY+DELETE) */
334
+ async moveMessage(uid, destination) {
335
+ if (this.capabilities.has("MOVE")) {
336
+ const tag = proto.nextTag();
337
+ const responses = await this.sendCommand(tag, proto.moveCommand(tag, uid, destination));
338
+ const tagged = responses.find(r => r.tag === tag);
339
+ if (!tagged || tagged.type !== "OK")
340
+ throw new Error(`MOVE failed: ${tagged?.text || "unknown"}`);
341
+ }
342
+ else {
343
+ await this.copyMessage(uid, destination);
344
+ await this.addFlags(uid, ["\\Deleted"]);
345
+ await this.expunge();
346
+ }
347
+ }
348
+ /** Delete a message by UID (flag + expunge) */
349
+ async deleteMessage(uid) {
350
+ await this.addFlags(uid, ["\\Deleted"]);
351
+ await this.expunge();
352
+ }
353
+ /** Expunge deleted messages */
354
+ async expunge() {
355
+ const tag = proto.nextTag();
356
+ await this.sendCommand(tag, proto.buildCommand(tag, "EXPUNGE"));
357
+ }
358
+ /** Append a message to a mailbox */
359
+ async appendMessage(mailbox, message, flags = []) {
360
+ const data = typeof message === "string" ? message : new TextDecoder().decode(message);
361
+ const size = new TextEncoder().encode(data).length;
362
+ const tag = proto.nextTag();
363
+ const cmd = proto.appendCommand(tag, mailbox, flags, size);
364
+ // Send command, wait for continuation
365
+ await this.transport.write(cmd);
366
+ // Wait for "+" continuation
367
+ const contResp = await this.waitForContinuation(tag);
368
+ if (!contResp)
369
+ throw new Error("APPEND: server did not send continuation");
370
+ // Send the message data + CRLF
371
+ await this.transport.write(data + "\r\n");
372
+ // Wait for tagged response
373
+ const responses = await this.waitForTagged(tag);
374
+ const tagged = responses.find(r => r.tag === tag);
375
+ if (!tagged || tagged.type !== "OK")
376
+ throw new Error(`APPEND failed: ${tagged?.text || "unknown"}`);
377
+ // Try to extract APPENDUID
378
+ const uidMatch = tagged.text.match(/APPENDUID\s+\d+\s+(\d+)/i);
379
+ return uidMatch ? parseInt(uidMatch[1]) : null;
380
+ }
381
+ // ── IDLE ──
382
+ async startIdle(onNewMail) {
383
+ this.idleCallback = onNewMail;
384
+ const tag = proto.nextTag();
385
+ this.idleTag = tag;
386
+ await this.transport.write(proto.idleCommand(tag));
387
+ // Wait for "+" continuation
388
+ await this.waitForContinuation(tag);
389
+ return async () => {
390
+ this.idleTag = null;
391
+ this.idleCallback = null;
392
+ await this.transport.write(proto.doneCommand());
393
+ await this.waitForTagged(tag);
394
+ };
395
+ }
396
+ // ── Message count (lightweight STATUS) ──
397
+ async getMessageCount(mailbox) {
398
+ const status = await this.getStatus(mailbox);
399
+ return status.messages || 0;
400
+ }
401
+ // ── Low-level command handling ──
402
+ sendCommand(tag, command) {
403
+ return new Promise((resolve, reject) => {
404
+ if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
405
+ console.log(` [imap] > ${command.trimEnd()}`);
406
+ }
407
+ this.pendingCommand = { tag, resolve, reject, responses: [] };
408
+ this.transport.write(command).catch(reject);
409
+ });
410
+ }
411
+ waitForContinuation(tag) {
412
+ return new Promise((resolve) => {
413
+ const timeout = setTimeout(() => resolve(null), 30000);
414
+ const check = () => {
415
+ const lineEnd = this.buffer.indexOf("\r\n");
416
+ if (lineEnd >= 0) {
417
+ const line = this.buffer.substring(0, lineEnd + 2);
418
+ const resp = proto.parseResponseLine(line);
419
+ if (resp.tag === "+") {
420
+ this.buffer = this.buffer.substring(lineEnd + 2);
421
+ clearTimeout(timeout);
422
+ resolve(resp);
423
+ return;
424
+ }
425
+ // Not continuation — process and keep waiting
426
+ this.buffer = this.buffer.substring(lineEnd + 2);
427
+ this.handleUntaggedResponse(resp);
428
+ }
429
+ if (this.transport.connected)
430
+ setTimeout(check, 5);
431
+ else {
432
+ clearTimeout(timeout);
433
+ resolve(null);
434
+ }
435
+ };
436
+ check();
437
+ });
438
+ }
439
+ waitForTagged(tag) {
440
+ return new Promise((resolve, reject) => {
441
+ this.pendingCommand = { tag, resolve, reject, responses: [] };
442
+ });
443
+ }
444
+ handleData(data) {
445
+ this.buffer += data;
446
+ this.processBuffer();
447
+ }
448
+ processBuffer() {
449
+ while (true) {
450
+ // Check for literal {size}\r\n
451
+ if (this.pendingCommand?.literalBytes != null) {
452
+ if (this.buffer.length >= this.pendingCommand.literalBytes) {
453
+ const literal = this.buffer.substring(0, this.pendingCommand.literalBytes);
454
+ this.buffer = this.buffer.substring(this.pendingCommand.literalBytes);
455
+ this.pendingCommand.literalBuffer = (this.pendingCommand.literalBuffer || "") + literal;
456
+ this.pendingCommand.literalBytes = undefined;
457
+ continue;
458
+ }
459
+ break; // Wait for more data
460
+ }
461
+ const lineEnd = this.buffer.indexOf("\r\n");
462
+ if (lineEnd < 0)
463
+ break;
464
+ const line = this.buffer.substring(0, lineEnd + 2);
465
+ this.buffer = this.buffer.substring(lineEnd + 2);
466
+ // Check for literal announcement {size}
467
+ const literalMatch = line.match(/\{(\d+)\}\r\n$/);
468
+ if (literalMatch && this.pendingCommand) {
469
+ this.pendingCommand.literalBytes = parseInt(literalMatch[1]);
470
+ // Store the line prefix before the literal
471
+ const linePrefix = line.substring(0, line.indexOf(`{${literalMatch[1]}}`));
472
+ this.pendingCommand.literalBuffer = linePrefix;
473
+ continue;
474
+ }
475
+ // If we have buffered literal data, prepend it
476
+ let fullLine = line;
477
+ if (this.pendingCommand?.literalBuffer) {
478
+ fullLine = this.pendingCommand.literalBuffer + line;
479
+ this.pendingCommand.literalBuffer = undefined;
480
+ }
481
+ const resp = proto.parseResponseLine(fullLine);
482
+ if (this.verbose && resp.raw.length < 200) {
483
+ console.log(` [imap] < ${resp.raw}`);
484
+ }
485
+ // During IDLE, handle EXISTS notifications
486
+ if (this.idleTag && resp.tag === "*" && resp.type === "EXISTS") {
487
+ const count = parseInt(resp.text);
488
+ if (this.idleCallback && count > this.mailboxInfo.exists) {
489
+ const newCount = count - this.mailboxInfo.exists;
490
+ this.mailboxInfo.exists = count;
491
+ this.idleCallback(newCount);
492
+ }
493
+ continue;
494
+ }
495
+ // Collect untagged responses for the pending command
496
+ if (resp.tag === "*" && this.pendingCommand) {
497
+ this.pendingCommand.responses.push(resp);
498
+ continue;
499
+ }
500
+ // Continuation — resolve if waiting
501
+ if (resp.tag === "+") {
502
+ // Handled by waitForContinuation
503
+ continue;
504
+ }
505
+ // Tagged response — command complete
506
+ if (this.pendingCommand && resp.tag === this.pendingCommand.tag) {
507
+ this.pendingCommand.responses.push(resp);
508
+ const { resolve, responses } = this.pendingCommand;
509
+ this.pendingCommand = null;
510
+ resolve(responses);
511
+ continue;
512
+ }
513
+ // Unhandled
514
+ this.handleUntaggedResponse(resp);
515
+ }
516
+ }
517
+ handleUntaggedResponse(resp) {
518
+ if (resp.type === "EXISTS") {
519
+ this.mailboxInfo.exists = parseInt(resp.text);
520
+ }
521
+ else if (resp.type === "EXPUNGE") {
522
+ this.mailboxInfo.exists = Math.max(0, this.mailboxInfo.exists - 1);
523
+ }
524
+ }
525
+ // ── FETCH Response Parser ──
526
+ parseFetchResponses(responses) {
527
+ const messages = [];
528
+ for (const r of responses) {
529
+ if (r.tag !== "*" || r.type !== "FETCH")
530
+ continue;
531
+ const msg = {
532
+ seq: 0, uid: 0, flags: new Set(), date: null,
533
+ subject: "", messageId: "", from: [], to: [], cc: [], bcc: [],
534
+ sender: [], replyTo: [], inReplyTo: "", size: 0,
535
+ source: "", headers: "", seen: false, flagged: false,
536
+ answered: false, draft: false,
537
+ };
538
+ // Extract sequence number from "5 FETCH (...)"
539
+ const seqMatch = r.text.match(/^(\d+)\s+FETCH/);
540
+ if (seqMatch)
541
+ msg.seq = parseInt(seqMatch[1]);
542
+ // Extract UID
543
+ const uidMatch = r.text.match(/UID\s+(\d+)/);
544
+ if (uidMatch)
545
+ msg.uid = parseInt(uidMatch[1]);
546
+ // Extract FLAGS
547
+ const flagsMatch = r.text.match(/FLAGS\s+\(([^)]*)\)/);
548
+ if (flagsMatch) {
549
+ msg.flags = new Set(flagsMatch[1].split(/\s+/).filter(Boolean));
550
+ msg.seen = msg.flags.has("\\Seen");
551
+ msg.flagged = msg.flags.has("\\Flagged");
552
+ msg.answered = msg.flags.has("\\Answered");
553
+ msg.draft = msg.flags.has("\\Draft");
554
+ }
555
+ // Extract RFC822.SIZE
556
+ const sizeMatch = r.text.match(/RFC822\.SIZE\s+(\d+)/);
557
+ if (sizeMatch)
558
+ msg.size = parseInt(sizeMatch[1]);
559
+ // Extract INTERNALDATE
560
+ const dateMatch = r.text.match(/INTERNALDATE\s+"([^"]+)"/);
561
+ if (dateMatch)
562
+ msg.date = new Date(dateMatch[1]);
563
+ // Extract ENVELOPE
564
+ const envMatch = r.text.match(/ENVELOPE\s+(\(.*\))/);
565
+ if (envMatch) {
566
+ const env = proto.parseEnvelope(envMatch[1]);
567
+ msg.date = msg.date || env.date;
568
+ msg.subject = env.subject;
569
+ msg.messageId = env.messageId;
570
+ msg.from = env.from;
571
+ msg.to = env.to;
572
+ msg.cc = env.cc;
573
+ msg.bcc = env.bcc;
574
+ msg.sender = env.sender;
575
+ msg.replyTo = env.replyTo;
576
+ msg.inReplyTo = env.inReplyTo;
577
+ }
578
+ // Source body is in literal data (handled by literal buffering in processBuffer)
579
+ // For now, simplified — the literal handling needs more work for production
580
+ // TODO: improve literal parsing for BODY[] and BODY[HEADER]
581
+ messages.push(msg);
582
+ }
583
+ return messages;
584
+ }
585
+ }
586
+ //# sourceMappingURL=imap-native.js.map