@intx/mail-memory 0.1.2

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,798 @@
1
+ import type {
2
+ MessageTransport,
3
+ OutboundMessage,
4
+ SendReceipt,
5
+ InboundMessage,
6
+ MessageRef,
7
+ Mailbox,
8
+ MailboxStatus,
9
+ SearchQuery,
10
+ Thread,
11
+ MessageHeaders,
12
+ BodyStructure,
13
+ MessagePart,
14
+ SyncState,
15
+ SyncResult,
16
+ ListInfo,
17
+ MailboxEvent,
18
+ Unsubscribe,
19
+ CryptoProvider,
20
+ } from "@intx/types/runtime";
21
+ import {
22
+ createAddressEntry,
23
+ createMailboxStore,
24
+ requireMessage,
25
+ appendToMailbox,
26
+ type AddressEntry,
27
+ } from "./mailbox";
28
+ import { parseHeaderSection } from "@intx/mime";
29
+ import { buildMessageHeaders } from "./headers";
30
+ import {
31
+ executeSend,
32
+ type RemoteSendHandler,
33
+ type MessageSentHandler,
34
+ } from "./send";
35
+ import { executeSearch } from "./search";
36
+ import { executeThread } from "./thread";
37
+ import {
38
+ fetchHeaders as doFetchHeaders,
39
+ fetchStructure as doFetchStructure,
40
+ fetchPart as doFetchPart,
41
+ fetchFull as doFetchFull,
42
+ } from "./fetch";
43
+
44
+ /**
45
+ * The hub-side surface a transport must expose to coordinate per-agent
46
+ * registration, mail routing, and outbound-audit hooks. SessionManager
47
+ * and HubLink in `@intx/hub-agent` depend on this interface rather
48
+ * than on `InMemoryTransport` directly so custom hosts can supply
49
+ * their own backend (e.g. an SMTP/IMAP relay) without touching the
50
+ * package's seams.
51
+ */
52
+ export interface HubTransport {
53
+ register(address: string, crypto: CryptoProvider): void;
54
+ unregister(address: string): void;
55
+ getTransportFor(address: string): MessageTransport;
56
+ setRemoteSendHandler(handler: RemoteSendHandler): void;
57
+ addMessageSentHandler(handler: MessageSentHandler): void;
58
+ /**
59
+ * Drop a hub-routed RFC 2822 message directly into an address's
60
+ * inbox. Used by the wire layer (HubLink) for inbound mail frames.
61
+ */
62
+ deliver(address: string, message: Uint8Array): void;
63
+ }
64
+
65
+ /**
66
+ * In-memory MessageTransport implementing full IMAP semantics within a
67
+ * single process. Messages are stored as real RFC 2822 MIME byte buffers.
68
+ *
69
+ * Every outbound message is PGP/MIME signed with the sender's CryptoProvider.
70
+ * Signature verification runs on fetchFull().
71
+ *
72
+ * Addresses must be registered before sending or receiving messages.
73
+ */
74
+ export class InMemoryTransport implements MessageTransport, HubTransport {
75
+ readonly #entries = new Map<string, AddressEntry>();
76
+ #remoteSendHandler: RemoteSendHandler | undefined;
77
+ readonly #messageSentHandlers = new Set<MessageSentHandler>();
78
+
79
+ /**
80
+ * Set a handler for delivering messages to recipients not registered on
81
+ * this transport. The federation layer calls this to wire up the websocket
82
+ * connection to the hub. When set, send() forwards unregistered recipients
83
+ * to this handler instead of throwing.
84
+ */
85
+ setRemoteSendHandler(handler: RemoteSendHandler): void {
86
+ this.#remoteSendHandler = handler;
87
+ }
88
+
89
+ /**
90
+ * Register a handler that fires after every successful send(). Multiple
91
+ * handlers may be registered. The message is already delivered when
92
+ * handlers fire — a handler rejection does not mean the message was not
93
+ * delivered.
94
+ */
95
+ addMessageSentHandler(handler: MessageSentHandler): void {
96
+ this.#messageSentHandlers.add(handler);
97
+ }
98
+
99
+ /**
100
+ * Register an address with its CryptoProvider. Creates the default set
101
+ * of mailboxes (INBOX, Sent, Drafts, Archive, Trash).
102
+ *
103
+ * Throws if the address is already registered.
104
+ */
105
+ register(address: string, crypto: CryptoProvider): void {
106
+ if (this.#entries.has(address)) {
107
+ throw new Error(`Address "${address}" is already registered`);
108
+ }
109
+ this.#entries.set(address, createAddressEntry(crypto));
110
+ }
111
+
112
+ /**
113
+ * Remove an address's mailboxes and crypto provider. Called when a
114
+ * session is destroyed so the address can be re-registered later.
115
+ */
116
+ unregister(address: string): void {
117
+ this.#entries.delete(address);
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Outbound
122
+ // ---------------------------------------------------------------------------
123
+
124
+ async send(
125
+ _message: OutboundMessage,
126
+ _signal?: AbortSignal,
127
+ ): Promise<SendReceipt> {
128
+ throw new Error(
129
+ "Use createInMemoryTransport().getTransportFor(address) to send messages",
130
+ );
131
+ }
132
+
133
+ async append(
134
+ _mailbox: string,
135
+ _message: InboundMessage,
136
+ _flags?: string[],
137
+ _signal?: AbortSignal,
138
+ ): Promise<MessageRef> {
139
+ throw new Error(
140
+ "Use createInMemoryTransport().getTransportFor(address) to append messages",
141
+ );
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Mailbox management (per-address — use getTransportFor)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ async listMailboxes(_signal?: AbortSignal): Promise<Mailbox[]> {
149
+ throw new Error("Use getTransportFor(address) for per-address operations");
150
+ }
151
+
152
+ async createMailbox(_name: string, _signal?: AbortSignal): Promise<Mailbox> {
153
+ throw new Error("Use getTransportFor(address) for per-address operations");
154
+ }
155
+
156
+ async deleteMailbox(_name: string, _signal?: AbortSignal): Promise<void> {
157
+ throw new Error("Use getTransportFor(address) for per-address operations");
158
+ }
159
+
160
+ async getMailboxStatus(
161
+ _name: string,
162
+ _signal?: AbortSignal,
163
+ ): Promise<MailboxStatus> {
164
+ throw new Error("Use getTransportFor(address) for per-address operations");
165
+ }
166
+
167
+ async search(
168
+ _mailbox: string,
169
+ _query: SearchQuery,
170
+ _signal?: AbortSignal,
171
+ ): Promise<MessageRef[]> {
172
+ throw new Error("Use getTransportFor(address) for per-address operations");
173
+ }
174
+
175
+ async thread(
176
+ _mailbox: string,
177
+ _algorithm: "references" | "orderedsubject",
178
+ _query?: SearchQuery,
179
+ _signal?: AbortSignal,
180
+ ): Promise<Thread[]> {
181
+ throw new Error("Use getTransportFor(address) for per-address operations");
182
+ }
183
+
184
+ async fetchHeaders(
185
+ _ref: MessageRef,
186
+ _signal?: AbortSignal,
187
+ ): Promise<MessageHeaders> {
188
+ throw new Error("Use getTransportFor(address) for per-address operations");
189
+ }
190
+
191
+ async fetchStructure(
192
+ _ref: MessageRef,
193
+ _signal?: AbortSignal,
194
+ ): Promise<BodyStructure> {
195
+ throw new Error("Use getTransportFor(address) for per-address operations");
196
+ }
197
+
198
+ async fetchPart(
199
+ _ref: MessageRef,
200
+ _partPath: string,
201
+ _signal?: AbortSignal,
202
+ ): Promise<MessagePart> {
203
+ throw new Error("Use getTransportFor(address) for per-address operations");
204
+ }
205
+
206
+ async fetchFull(
207
+ _ref: MessageRef,
208
+ _signal?: AbortSignal,
209
+ ): Promise<InboundMessage> {
210
+ throw new Error("Use getTransportFor(address) for per-address operations");
211
+ }
212
+
213
+ async setFlags(
214
+ _ref: MessageRef,
215
+ _flags: string[],
216
+ _signal?: AbortSignal,
217
+ ): Promise<void> {
218
+ throw new Error("Use getTransportFor(address) for per-address operations");
219
+ }
220
+
221
+ async clearFlags(
222
+ _ref: MessageRef,
223
+ _flags: string[],
224
+ _signal?: AbortSignal,
225
+ ): Promise<void> {
226
+ throw new Error("Use getTransportFor(address) for per-address operations");
227
+ }
228
+
229
+ async move(
230
+ _ref: MessageRef,
231
+ _toMailbox: string,
232
+ _signal?: AbortSignal,
233
+ ): Promise<void> {
234
+ throw new Error("Use getTransportFor(address) for per-address operations");
235
+ }
236
+
237
+ async copy(
238
+ _ref: MessageRef,
239
+ _toMailbox: string,
240
+ _signal?: AbortSignal,
241
+ ): Promise<void> {
242
+ throw new Error("Use getTransportFor(address) for per-address operations");
243
+ }
244
+
245
+ async expunge(_mailbox: string, _signal?: AbortSignal): Promise<void> {
246
+ throw new Error("Use getTransportFor(address) for per-address operations");
247
+ }
248
+
249
+ watch(
250
+ _mailbox: string,
251
+ _callback: (event: MailboxEvent) => void,
252
+ ): Unsubscribe {
253
+ throw new Error("Use getTransportFor(address) for per-address operations");
254
+ }
255
+
256
+ async sync(
257
+ _mailbox: string,
258
+ _knownState: SyncState,
259
+ _signal?: AbortSignal,
260
+ ): Promise<SyncResult> {
261
+ throw new Error("sync() (QRESYNC) is not implemented");
262
+ }
263
+
264
+ async createList(
265
+ _address: string,
266
+ _name: string,
267
+ _signal?: AbortSignal,
268
+ ): Promise<ListInfo> {
269
+ throw new Error("Distribution list management is not implemented");
270
+ }
271
+
272
+ async listMembers(
273
+ _address: string,
274
+ _signal?: AbortSignal,
275
+ ): Promise<string[]> {
276
+ throw new Error("Distribution list management is not implemented");
277
+ }
278
+
279
+ async subscribe(
280
+ _listAddress: string,
281
+ _subscriberAddress: string,
282
+ _signal?: AbortSignal,
283
+ ): Promise<void> {
284
+ throw new Error("Distribution list management is not implemented");
285
+ }
286
+
287
+ async unsubscribe(
288
+ _listAddress: string,
289
+ _subscriberAddress: string,
290
+ _signal?: AbortSignal,
291
+ ): Promise<void> {
292
+ throw new Error("Distribution list management is not implemented");
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Inbound delivery from federation
297
+ // ---------------------------------------------------------------------------
298
+
299
+ /**
300
+ * Deliver a signed MIME message to an address's INBOX. Used by the
301
+ * federation layer when a message arrives from the hub over the
302
+ * websocket — the message is already assembled and signed by the
303
+ * originating sender, so no further processing is needed beyond
304
+ * envelope parsing and storage.
305
+ *
306
+ * Throws if the address is not registered.
307
+ */
308
+ deliver(address: string, message: Uint8Array): void {
309
+ const entry = this.#entries.get(address);
310
+ if (entry === undefined) {
311
+ throw new Error(
312
+ `Address "${address}" is not registered — cannot deliver mail`,
313
+ );
314
+ }
315
+ const inbox = entry.mailboxes.get("INBOX");
316
+ if (inbox === undefined) {
317
+ throw new Error(`Address "${address}" has no INBOX`);
318
+ }
319
+
320
+ const { headers } = parseHeaderSection(message);
321
+
322
+ const messageId = headers.get("message-id");
323
+ const from = headers.get("from");
324
+ const dateRaw = headers.get("date");
325
+ if (messageId === undefined) {
326
+ throw new Error("Cannot deliver message: missing Message-ID header");
327
+ }
328
+ if (from === undefined) {
329
+ throw new Error("Cannot deliver message: missing From header");
330
+ }
331
+ if (dateRaw === undefined) {
332
+ throw new Error("Cannot deliver message: missing Date header");
333
+ }
334
+
335
+ const msgHeaders = buildMessageHeaders(headers);
336
+
337
+ const toRaw = headers.get("to") ?? "";
338
+ const to = toRaw
339
+ ? toRaw
340
+ .split(",")
341
+ .map((s) => s.trim())
342
+ .filter(Boolean)
343
+ : [];
344
+
345
+ const refsRaw = headers.get("references");
346
+ const references = refsRaw ? refsRaw.split(/\s+/).filter(Boolean) : [];
347
+
348
+ const envelope: import("./mailbox").StoredEnvelope = {
349
+ messageId,
350
+ from,
351
+ to,
352
+ subject: headers.get("subject") ?? "",
353
+ date: new Date(dateRaw),
354
+ inReplyTo: headers.get("in-reply-to"),
355
+ references,
356
+ interchangeType: headers.get("interchange-type"),
357
+ interchangeCorrelationId: headers.get("interchange-correlation-id"),
358
+ };
359
+
360
+ const uid = appendToMailbox(inbox, message, envelope, []);
361
+
362
+ const callbacks = entry.watchCallbacks.get("INBOX");
363
+ if (callbacks !== undefined && callbacks.size > 0) {
364
+ const event: import("@intx/types/runtime").MailboxEvent = {
365
+ type: "exists",
366
+ uid,
367
+ headers: msgHeaders,
368
+ };
369
+ for (const cb of callbacks) {
370
+ queueMicrotask(() => cb(event));
371
+ }
372
+ }
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Internal: per-address view
377
+ // ---------------------------------------------------------------------------
378
+
379
+ /**
380
+ * Returns a MessageTransport scoped to the given address. Callers use
381
+ * this to send and read mail as that address.
382
+ */
383
+ getTransportFor(address: string): MessageTransport {
384
+ if (!this.#entries.has(address)) {
385
+ throw new Error(
386
+ `Address "${address}" is not registered — call register() first`,
387
+ );
388
+ }
389
+ return new ScopedMessageTransport(
390
+ address,
391
+ this.#entries,
392
+ () => this.#remoteSendHandler,
393
+ () => this.#messageSentHandlers,
394
+ );
395
+ }
396
+ }
397
+
398
+ /**
399
+ * MessageTransport scoped to a single address. All operations target that
400
+ * address's mailboxes. Constructed via InMemoryTransport.getTransportFor().
401
+ */
402
+ class ScopedMessageTransport implements MessageTransport {
403
+ readonly #address: string;
404
+ readonly #entries: Map<string, AddressEntry>;
405
+ readonly #getRemoteSendHandler: () => RemoteSendHandler | undefined;
406
+ readonly #getMessageSentHandlers: () => Set<MessageSentHandler>;
407
+
408
+ constructor(
409
+ address: string,
410
+ entries: Map<string, AddressEntry>,
411
+ getRemoteSendHandler: () => RemoteSendHandler | undefined,
412
+ getMessageSentHandlers: () => Set<MessageSentHandler>,
413
+ ) {
414
+ this.#address = address;
415
+ this.#entries = entries;
416
+ this.#getRemoteSendHandler = getRemoteSendHandler;
417
+ this.#getMessageSentHandlers = getMessageSentHandlers;
418
+ }
419
+
420
+ get #entry(): AddressEntry {
421
+ const e = this.#entries.get(this.#address);
422
+ if (e === undefined) {
423
+ throw new Error(`Address "${this.#address}" has been deregistered`);
424
+ }
425
+ return e;
426
+ }
427
+
428
+ #requireMailbox(name: string) {
429
+ const store = this.#entry.mailboxes.get(name);
430
+ if (store === undefined) {
431
+ throw new Error(
432
+ `Mailbox "${name}" does not exist for address "${this.#address}"`,
433
+ );
434
+ }
435
+ return store;
436
+ }
437
+
438
+ async send(
439
+ message: OutboundMessage,
440
+ _signal?: AbortSignal,
441
+ ): Promise<SendReceipt> {
442
+ // Trip the deregistered guard so callers using a stale scoped handle
443
+ // see a precise error rather than the generic "sender is not
444
+ // registered" thrown by executeSend.
445
+ void this.#entry;
446
+
447
+ const handlers = this.#getMessageSentHandlers();
448
+ const aggregatedHandler: MessageSentHandler | undefined =
449
+ handlers.size > 0
450
+ ? async (ctx) => {
451
+ await Promise.allSettled([...handlers].map((h) => h(ctx)));
452
+ }
453
+ : undefined;
454
+ return executeSend(
455
+ this.#address,
456
+ message,
457
+ this.#entries,
458
+ this.#getRemoteSendHandler(),
459
+ aggregatedHandler,
460
+ );
461
+ }
462
+
463
+ async append(
464
+ mailbox: string,
465
+ message: InboundMessage,
466
+ flags?: string[],
467
+ _signal?: AbortSignal,
468
+ ): Promise<MessageRef> {
469
+ const store = this.#requireMailbox(mailbox);
470
+ // For append, we need to convert InboundMessage back to raw bytes.
471
+ // Since InboundMessage may come from a prior fetchFull, we need the raw
472
+ // bytes. This is a design gap — append() takes InboundMessage but we
473
+ // need Uint8Array. We store a minimal representation.
474
+ //
475
+ // For now, serialize the InboundMessage as a minimal RFC 2822 message.
476
+ const raw = inboundMessageToRaw(message);
477
+ const envelope = {
478
+ messageId: message.headers.messageId,
479
+ from: message.headers.from,
480
+ to: message.headers.to,
481
+ subject: message.headers.subject ?? "",
482
+ date: new Date(message.headers.date),
483
+ inReplyTo: message.headers.inReplyTo,
484
+ references: message.headers.references ?? [],
485
+ interchangeType: message.headers.interchangeType,
486
+ interchangeCorrelationId: message.headers.interchangeCorrelationId,
487
+ };
488
+ const uid = appendToMailbox(store, raw, envelope, flags ?? []);
489
+ return { uid, mailbox };
490
+ }
491
+
492
+ async listMailboxes(_signal?: AbortSignal): Promise<Mailbox[]> {
493
+ return Array.from(this.#entry.mailboxes.keys()).map((name) => ({
494
+ name,
495
+ }));
496
+ }
497
+
498
+ async createMailbox(name: string, _signal?: AbortSignal): Promise<Mailbox> {
499
+ if (this.#entry.mailboxes.has(name)) {
500
+ throw new Error(
501
+ `Mailbox "${name}" already exists for address "${this.#address}"`,
502
+ );
503
+ }
504
+ this.#entry.mailboxes.set(name, createMailboxStore());
505
+ return { name };
506
+ }
507
+
508
+ async deleteMailbox(name: string, _signal?: AbortSignal): Promise<void> {
509
+ if (!this.#entry.mailboxes.has(name)) {
510
+ throw new Error(
511
+ `Mailbox "${name}" does not exist for address "${this.#address}"`,
512
+ );
513
+ }
514
+ this.#entry.mailboxes.delete(name);
515
+ }
516
+
517
+ async getMailboxStatus(
518
+ name: string,
519
+ _signal?: AbortSignal,
520
+ ): Promise<MailboxStatus> {
521
+ const store = this.#requireMailbox(name);
522
+ const unseen = store.messages.filter((m) => !m.flags.has("\\Seen")).length;
523
+ return {
524
+ total: store.messages.length,
525
+ unseen,
526
+ recent: 0,
527
+ uidNext: store.uidCounter,
528
+ uidValidity: store.uidValidity,
529
+ highestModSeq: store.modseqCounter - 1,
530
+ };
531
+ }
532
+
533
+ async search(
534
+ mailbox: string,
535
+ query: SearchQuery,
536
+ _signal?: AbortSignal,
537
+ ): Promise<MessageRef[]> {
538
+ const store = this.#requireMailbox(mailbox);
539
+ return executeSearch(mailbox, store, query);
540
+ }
541
+
542
+ async thread(
543
+ mailbox: string,
544
+ algorithm: "references" | "orderedsubject",
545
+ query?: SearchQuery,
546
+ _signal?: AbortSignal,
547
+ ): Promise<Thread[]> {
548
+ const store = this.#requireMailbox(mailbox);
549
+ return executeThread(mailbox, store, algorithm, query);
550
+ }
551
+
552
+ async fetchHeaders(
553
+ ref: MessageRef,
554
+ _signal?: AbortSignal,
555
+ ): Promise<MessageHeaders> {
556
+ const store = this.#requireMailbox(ref.mailbox);
557
+ return doFetchHeaders(ref, store);
558
+ }
559
+
560
+ async fetchStructure(
561
+ ref: MessageRef,
562
+ _signal?: AbortSignal,
563
+ ): Promise<BodyStructure> {
564
+ const store = this.#requireMailbox(ref.mailbox);
565
+ return doFetchStructure(ref, store);
566
+ }
567
+
568
+ async fetchPart(
569
+ ref: MessageRef,
570
+ partPath: string,
571
+ _signal?: AbortSignal,
572
+ ): Promise<MessagePart> {
573
+ const store = this.#requireMailbox(ref.mailbox);
574
+ return doFetchPart(ref, partPath, store);
575
+ }
576
+
577
+ async fetchFull(
578
+ ref: MessageRef,
579
+ _signal?: AbortSignal,
580
+ ): Promise<InboundMessage> {
581
+ const store = this.#requireMailbox(ref.mailbox);
582
+ return doFetchFull(ref, store, (addr) => this.#entries.get(addr)?.crypto);
583
+ }
584
+
585
+ async setFlags(
586
+ ref: MessageRef,
587
+ flags: string[],
588
+ _signal?: AbortSignal,
589
+ ): Promise<void> {
590
+ const store = this.#requireMailbox(ref.mailbox);
591
+ const msg = requireMessage(store, ref.uid, ref.mailbox);
592
+ for (const flag of flags) {
593
+ msg.flags.add(flag);
594
+ }
595
+ msg.modseq = store.modseqCounter++;
596
+ this.#fireWatchCallbacks(ref.mailbox, {
597
+ type: "flagsChanged",
598
+ uid: ref.uid,
599
+ flags: Array.from(msg.flags),
600
+ });
601
+ }
602
+
603
+ async clearFlags(
604
+ ref: MessageRef,
605
+ flags: string[],
606
+ _signal?: AbortSignal,
607
+ ): Promise<void> {
608
+ const store = this.#requireMailbox(ref.mailbox);
609
+ const msg = requireMessage(store, ref.uid, ref.mailbox);
610
+ for (const flag of flags) {
611
+ msg.flags.delete(flag);
612
+ }
613
+ msg.modseq = store.modseqCounter++;
614
+ this.#fireWatchCallbacks(ref.mailbox, {
615
+ type: "flagsChanged",
616
+ uid: ref.uid,
617
+ flags: Array.from(msg.flags),
618
+ });
619
+ }
620
+
621
+ async move(
622
+ ref: MessageRef,
623
+ toMailbox: string,
624
+ _signal?: AbortSignal,
625
+ ): Promise<void> {
626
+ const fromStore = this.#requireMailbox(ref.mailbox);
627
+ const toStore = this.#requireMailbox(toMailbox);
628
+ const msgIdx = fromStore.messages.findIndex((m) => m.uid === ref.uid);
629
+ if (msgIdx === -1) {
630
+ throw new Error(
631
+ `Message UID ${ref.uid} not found in mailbox "${ref.mailbox}"`,
632
+ );
633
+ }
634
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by findIndex !== -1 above
635
+ const msg = fromStore.messages[msgIdx]!;
636
+ fromStore.messages.splice(msgIdx, 1);
637
+
638
+ const newUid = appendToMailbox(
639
+ toStore,
640
+ msg.raw,
641
+ msg.envelope,
642
+ Array.from(msg.flags),
643
+ );
644
+
645
+ this.#fireWatchCallbacks(ref.mailbox, {
646
+ type: "expunged",
647
+ uid: ref.uid,
648
+ });
649
+
650
+ // Notify watchers of the new message in the destination mailbox.
651
+ const { headers: parsedHeaders } = parseHeaderSection(msg.raw);
652
+ const msgHeaders = this.#buildMessageHeaders(parsedHeaders);
653
+ this.#fireWatchCallbacks(toMailbox, {
654
+ type: "exists",
655
+ uid: newUid,
656
+ headers: msgHeaders,
657
+ });
658
+ }
659
+
660
+ async copy(
661
+ ref: MessageRef,
662
+ toMailbox: string,
663
+ _signal?: AbortSignal,
664
+ ): Promise<void> {
665
+ const fromStore = this.#requireMailbox(ref.mailbox);
666
+ const toStore = this.#requireMailbox(toMailbox);
667
+ const msg = requireMessage(fromStore, ref.uid, ref.mailbox);
668
+
669
+ const newUid = appendToMailbox(
670
+ toStore,
671
+ msg.raw,
672
+ msg.envelope,
673
+ Array.from(msg.flags),
674
+ );
675
+
676
+ const { headers: parsedHeaders } = parseHeaderSection(msg.raw);
677
+ const msgHeaders = this.#buildMessageHeaders(parsedHeaders);
678
+ this.#fireWatchCallbacks(toMailbox, {
679
+ type: "exists",
680
+ uid: newUid,
681
+ headers: msgHeaders,
682
+ });
683
+ }
684
+
685
+ async expunge(mailbox: string, _signal?: AbortSignal): Promise<void> {
686
+ const store = this.#requireMailbox(mailbox);
687
+ const toExpunge = store.messages.filter((m) => m.flags.has("\\Deleted"));
688
+
689
+ store.messages = store.messages.filter((m) => !m.flags.has("\\Deleted"));
690
+
691
+ for (const msg of toExpunge) {
692
+ this.#fireWatchCallbacks(mailbox, {
693
+ type: "expunged",
694
+ uid: msg.uid,
695
+ });
696
+ }
697
+ }
698
+
699
+ watch(mailbox: string, callback: (event: MailboxEvent) => void): Unsubscribe {
700
+ this.#requireMailbox(mailbox);
701
+ let callbacks = this.#entry.watchCallbacks.get(mailbox);
702
+ if (callbacks === undefined) {
703
+ callbacks = new Set();
704
+ this.#entry.watchCallbacks.set(mailbox, callbacks);
705
+ }
706
+ callbacks.add(callback);
707
+
708
+ return () => {
709
+ const cbs = this.#entry.watchCallbacks.get(mailbox);
710
+ cbs?.delete(callback);
711
+ };
712
+ }
713
+
714
+ async sync(
715
+ _mailbox: string,
716
+ _knownState: SyncState,
717
+ _signal?: AbortSignal,
718
+ ): Promise<SyncResult> {
719
+ throw new Error("sync() (QRESYNC) is not implemented");
720
+ }
721
+
722
+ async createList(
723
+ _address: string,
724
+ _name: string,
725
+ _signal?: AbortSignal,
726
+ ): Promise<ListInfo> {
727
+ throw new Error("Distribution list management is not implemented");
728
+ }
729
+
730
+ async listMembers(
731
+ _address: string,
732
+ _signal?: AbortSignal,
733
+ ): Promise<string[]> {
734
+ throw new Error("Distribution list management is not implemented");
735
+ }
736
+
737
+ async subscribe(
738
+ _listAddress: string,
739
+ _subscriberAddress: string,
740
+ _signal?: AbortSignal,
741
+ ): Promise<void> {
742
+ throw new Error("Distribution list management is not implemented");
743
+ }
744
+
745
+ async unsubscribe(
746
+ _listAddress: string,
747
+ _subscriberAddress: string,
748
+ _signal?: AbortSignal,
749
+ ): Promise<void> {
750
+ throw new Error("Distribution list management is not implemented");
751
+ }
752
+
753
+ #fireWatchCallbacks(mailbox: string, event: MailboxEvent): void {
754
+ const callbacks = this.#entry.watchCallbacks.get(mailbox);
755
+ if (callbacks === undefined || callbacks.size === 0) return;
756
+ for (const cb of callbacks) {
757
+ queueMicrotask(() => cb(event));
758
+ }
759
+ }
760
+
761
+ #buildMessageHeaders(
762
+ headers: Map<string, string>,
763
+ ): import("@intx/types/runtime").MessageHeaders {
764
+ return buildMessageHeaders(headers);
765
+ }
766
+ }
767
+
768
+ function inboundMessageToRaw(message: InboundMessage): Uint8Array {
769
+ const enc = new TextEncoder();
770
+ const CRLF = "\r\n";
771
+ let headers = "";
772
+ headers += `From: ${message.headers.from}${CRLF}`;
773
+ headers += `To: ${message.headers.to.join(", ")}${CRLF}`;
774
+ if (message.headers.cc && message.headers.cc.length > 0) {
775
+ headers += `Cc: ${message.headers.cc.join(", ")}${CRLF}`;
776
+ }
777
+ headers += `Date: ${message.headers.date}${CRLF}`;
778
+ headers += `Message-ID: ${message.headers.messageId}${CRLF}`;
779
+ if (message.headers.subject !== undefined) {
780
+ headers += `Subject: ${message.headers.subject}${CRLF}`;
781
+ }
782
+ if (message.headers.inReplyTo !== undefined) {
783
+ headers += `In-Reply-To: ${message.headers.inReplyTo}${CRLF}`;
784
+ }
785
+ if (message.headers.references && message.headers.references.length > 0) {
786
+ headers += `References: ${message.headers.references.join(" ")}${CRLF}`;
787
+ }
788
+ if (message.headers.interchangeType !== undefined) {
789
+ headers += `Interchange-Type: ${message.headers.interchangeType}${CRLF}`;
790
+ }
791
+
792
+ const body =
793
+ message.content ??
794
+ (message.payload !== undefined ? JSON.stringify(message.payload) : "");
795
+ headers += `Content-Type: text/plain${CRLF}`;
796
+ headers += `${CRLF}`;
797
+ return enc.encode(headers + body);
798
+ }