agent-inbox 0.2.2 → 0.2.4
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/AGENTS.md +18 -0
- package/CLAUDE.md +92 -1
- package/README.md +73 -6
- package/bench/inbox-growth.bench.ts +224 -0
- package/dist/federation/connection-manager.d.ts +8 -0
- package/dist/federation/connection-manager.d.ts.map +1 -1
- package/dist/federation/connection-manager.js +12 -0
- package/dist/federation/connection-manager.js.map +1 -1
- package/dist/federation/delivery-queue.d.ts +11 -3
- package/dist/federation/delivery-queue.d.ts.map +1 -1
- package/dist/federation/delivery-queue.js +38 -8
- package/dist/federation/delivery-queue.js.map +1 -1
- package/dist/federation/queue-store.d.ts +42 -0
- package/dist/federation/queue-store.d.ts.map +1 -0
- package/dist/federation/queue-store.js +87 -0
- package/dist/federation/queue-store.js.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +124 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonrpc/mail-push-types.d.ts +9 -0
- package/dist/jsonrpc/mail-push-types.d.ts.map +1 -1
- package/dist/jsonrpc/mail-push-types.js +1 -0
- package/dist/jsonrpc/mail-push-types.js.map +1 -1
- package/dist/jsonrpc/mail-server.d.ts +8 -1
- package/dist/jsonrpc/mail-server.d.ts.map +1 -1
- package/dist/jsonrpc/mail-server.js +42 -1
- package/dist/jsonrpc/mail-server.js.map +1 -1
- package/dist/mail/address-book.d.ts +43 -0
- package/dist/mail/address-book.d.ts.map +1 -0
- package/dist/mail/address-book.js +95 -0
- package/dist/mail/address-book.js.map +1 -0
- package/dist/mail/attachment-store.d.ts +31 -0
- package/dist/mail/attachment-store.d.ts.map +1 -0
- package/dist/mail/attachment-store.js +74 -0
- package/dist/mail/attachment-store.js.map +1 -0
- package/dist/mail/email-mapper.d.ts +41 -0
- package/dist/mail/email-mapper.d.ts.map +1 -0
- package/dist/mail/email-mapper.js +216 -0
- package/dist/mail/email-mapper.js.map +1 -0
- package/dist/mail/fs-attachment-store.d.ts +38 -0
- package/dist/mail/fs-attachment-store.d.ts.map +1 -0
- package/dist/mail/fs-attachment-store.js +165 -0
- package/dist/mail/fs-attachment-store.js.map +1 -0
- package/dist/mail/mail-gateway.d.ts +114 -0
- package/dist/mail/mail-gateway.d.ts.map +1 -0
- package/dist/mail/mail-gateway.js +402 -0
- package/dist/mail/mail-gateway.js.map +1 -0
- package/dist/mail/provider-transport.d.ts +138 -0
- package/dist/mail/provider-transport.d.ts.map +1 -0
- package/dist/mail/provider-transport.js +434 -0
- package/dist/mail/provider-transport.js.map +1 -0
- package/dist/mail/rate-limiter.d.ts +20 -0
- package/dist/mail/rate-limiter.d.ts.map +1 -0
- package/dist/mail/rate-limiter.js +56 -0
- package/dist/mail/rate-limiter.js.map +1 -0
- package/dist/mail/smtp-transport.d.ts +141 -0
- package/dist/mail/smtp-transport.d.ts.map +1 -0
- package/dist/mail/smtp-transport.js +415 -0
- package/dist/mail/smtp-transport.js.map +1 -0
- package/dist/mail/types.d.ts +177 -0
- package/dist/mail/types.d.ts.map +1 -0
- package/dist/mail/types.js +11 -0
- package/dist/mail/types.js.map +1 -0
- package/dist/push/notifier.d.ts +21 -0
- package/dist/push/notifier.d.ts.map +1 -1
- package/dist/push/notifier.js +84 -2
- package/dist/push/notifier.js.map +1 -1
- package/dist/router/destination.d.ts +69 -0
- package/dist/router/destination.d.ts.map +1 -0
- package/dist/router/destination.js +106 -0
- package/dist/router/destination.js.map +1 -0
- package/dist/router/message-router.d.ts +15 -0
- package/dist/router/message-router.d.ts.map +1 -1
- package/dist/router/message-router.js +25 -3
- package/dist/router/message-router.js.map +1 -1
- package/dist/storage/interface.d.ts +21 -0
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/memory.d.ts +12 -0
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +50 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite.d.ts +14 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +79 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/traceability/traceability.d.ts.map +1 -1
- package/dist/traceability/traceability.js +7 -17
- package/dist/traceability/traceability.js.map +1 -1
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/DESIGN.md +15 -0
- package/docs/MAIL-INTEROP-PLAN.md +660 -0
- package/package.json +29 -3
- package/renovate.json5 +6 -0
- package/rules/agent-inbox.md +1 -0
- package/src/federation/connection-manager.ts +12 -0
- package/src/federation/delivery-queue.ts +38 -8
- package/src/federation/queue-store.ts +124 -0
- package/src/index.ts +186 -1
- package/src/jsonrpc/mail-push-types.ts +10 -0
- package/src/jsonrpc/mail-server.ts +48 -1
- package/src/mail/address-book.ts +111 -0
- package/src/mail/attachment-store.ts +90 -0
- package/src/mail/email-mapper.ts +288 -0
- package/src/mail/fs-attachment-store.ts +163 -0
- package/src/mail/mail-gateway.ts +505 -0
- package/src/mail/provider-transport.ts +577 -0
- package/src/mail/rate-limiter.ts +51 -0
- package/src/mail/smtp-transport.ts +589 -0
- package/src/mail/types.ts +221 -0
- package/src/push/notifier.ts +98 -2
- package/src/router/destination.ts +140 -0
- package/src/router/message-router.ts +41 -4
- package/src/storage/interface.ts +22 -0
- package/src/storage/memory.ts +59 -0
- package/src/storage/sqlite.ts +114 -1
- package/src/traceability/traceability.ts +7 -16
- package/src/types.ts +74 -0
- package/test/federation/delivery-queue-sqlite.test.ts +158 -0
- package/test/load.test.ts +288 -0
- package/test/mail/address-book.test.ts +111 -0
- package/test/mail/attachment-store-contract.test.ts +92 -0
- package/test/mail/attachment-store.test.ts +69 -0
- package/test/mail/destination.test.ts +115 -0
- package/test/mail/dsn-parse.test.ts +239 -0
- package/test/mail/email-mapper.test.ts +341 -0
- package/test/mail/external-id.test.ts +43 -0
- package/test/mail/fs-attachment-store.test.ts +134 -0
- package/test/mail/full-flow-e2e.test.ts +200 -0
- package/test/mail/mail-gateway.test.ts +419 -0
- package/test/mail/mail-transport-contract.test.ts +134 -0
- package/test/mail/mock-mail.ts +161 -0
- package/test/mail/mock-postmark.ts +66 -0
- package/test/mail/provider-transport.test.ts +381 -0
- package/test/mail/rate-limiter.test.ts +48 -0
- package/test/mail/router-mail-integration.test.ts +138 -0
- package/test/mail/smtp-e2e.test.ts +98 -0
- package/test/mail/smtp-transport.test.ts +138 -0
- package/test/mail-presence.test.ts +149 -0
- package/test/mail-push.test.ts +44 -0
- package/test/mail-server.test.ts +25 -0
- package/test/push-notifier.test.ts +81 -0
- package/test/sqlite-storage.test.ts +106 -0
- package/test/storage.test.ts +92 -0
- package/vitest.bench.config.ts +8 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional mapping between RFC 5322 email and agent-inbox Messages.
|
|
3
|
+
*
|
|
4
|
+
* Parallels src/mesh/type-mapper.ts. Two pure functions:
|
|
5
|
+
* - inboundMailToMessage: external email → inbox Message
|
|
6
|
+
* - messageToOutboundMail: inbox Message → rendered OutboundMail
|
|
7
|
+
*
|
|
8
|
+
* Threading: RFC Message-ID / In-Reply-To / References map onto the inbox's
|
|
9
|
+
* thread_tag + in_reply_to. The RFC Message-ID is preserved in metadata so a
|
|
10
|
+
* later reply can reconstruct the In-Reply-To / References chain. See §5.
|
|
11
|
+
*/
|
|
12
|
+
import { ulid } from "ulid";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { normalizeContent } from "../router/message-router.js";
|
|
15
|
+
/** Thrown when no inbound recipient resolves to a local agent. */
|
|
16
|
+
export class UnknownRecipientError extends Error {
|
|
17
|
+
recipients;
|
|
18
|
+
constructor(recipients) {
|
|
19
|
+
super(`No inbound recipient resolved to a local agent: ${recipients.join(", ")}`);
|
|
20
|
+
this.recipients = recipients;
|
|
21
|
+
this.name = "UnknownRecipientError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Inbound: email → Message
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export function inboundMailToMessage(mail, ctx) {
|
|
28
|
+
const recipients = [];
|
|
29
|
+
const unresolved = [];
|
|
30
|
+
let scope;
|
|
31
|
+
const addKind = (addrs, kind) => {
|
|
32
|
+
for (const a of addrs ?? []) {
|
|
33
|
+
const resolved = ctx.addressBook.resolveInbound(a.address);
|
|
34
|
+
if (!resolved) {
|
|
35
|
+
unresolved.push(a.address);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
recipients.push({ agent_id: resolved.agentId, kind });
|
|
39
|
+
// First resolved recipient with a plus-address scope wins. "to" is
|
|
40
|
+
// processed before "cc", so a to-scope still takes precedence, but a
|
|
41
|
+
// cc-only scoped recipient is no longer silently dropped.
|
|
42
|
+
if (scope === undefined && resolved.scope) {
|
|
43
|
+
scope = resolved.scope;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
addKind(mail.to, "to");
|
|
48
|
+
addKind(mail.cc, "cc");
|
|
49
|
+
if (recipients.length === 0) {
|
|
50
|
+
throw new UnknownRecipientError(unresolved);
|
|
51
|
+
}
|
|
52
|
+
const rfcMessageId = mail.headers.messageId;
|
|
53
|
+
const rfcInReplyTo = mail.headers.inReplyTo;
|
|
54
|
+
const rfcReferences = mail.headers.references;
|
|
55
|
+
const metadata = {};
|
|
56
|
+
if (rfcMessageId)
|
|
57
|
+
metadata.rfcMessageId = rfcMessageId;
|
|
58
|
+
if (rfcInReplyTo)
|
|
59
|
+
metadata.rfcInReplyTo = rfcInReplyTo;
|
|
60
|
+
if (rfcReferences)
|
|
61
|
+
metadata.rfcReferences = rfcReferences;
|
|
62
|
+
if (mail.html)
|
|
63
|
+
metadata.htmlBody = mail.html;
|
|
64
|
+
if (mail.authResults)
|
|
65
|
+
metadata.authResults = mail.authResults;
|
|
66
|
+
if (mail.remote)
|
|
67
|
+
metadata.remote = mail.remote;
|
|
68
|
+
if (mail.envelopeFrom)
|
|
69
|
+
metadata.envelopeFrom = mail.envelopeFrom;
|
|
70
|
+
if (mail.bounce)
|
|
71
|
+
metadata.bounce = mail.bounce;
|
|
72
|
+
if (mail.attachments?.length)
|
|
73
|
+
metadata.mailAttachments = mail.attachments;
|
|
74
|
+
// Resolve in_reply_to to a real inbox id when we can; otherwise leave it unset
|
|
75
|
+
// (the RFC id is retained in metadata for later correlation).
|
|
76
|
+
let inReplyTo;
|
|
77
|
+
if (rfcInReplyTo && ctx.lookupInboxIdByRfcMessageId) {
|
|
78
|
+
inReplyTo = ctx.lookupInboxIdByRfcMessageId(rfcInReplyTo);
|
|
79
|
+
}
|
|
80
|
+
const text = mail.text ?? "";
|
|
81
|
+
return {
|
|
82
|
+
id: ulid(),
|
|
83
|
+
scope: scope ?? ctx.defaultScope ?? "default",
|
|
84
|
+
sender_id: mail.from.address,
|
|
85
|
+
recipients,
|
|
86
|
+
subject: mail.subject,
|
|
87
|
+
content: normalizeContent(text),
|
|
88
|
+
thread_tag: deriveThreadTag(rfcReferences, rfcInReplyTo, rfcMessageId),
|
|
89
|
+
in_reply_to: inReplyTo,
|
|
90
|
+
importance: importanceFromHeaders(mail.headers.raw),
|
|
91
|
+
metadata,
|
|
92
|
+
created_at: mail.receivedAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Outbound: Message → email
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export function messageToOutboundMail(msg, ctx) {
|
|
99
|
+
const domain = ctx.messageIdDomain ?? ctx.addressBook.primaryDomain();
|
|
100
|
+
const to = recipientsOfKind(msg, "to");
|
|
101
|
+
const cc = recipientsOfKind(msg, "cc");
|
|
102
|
+
const bcc = recipientsOfKind(msg, "bcc");
|
|
103
|
+
const { inReplyTo, references } = buildThreadingHeaders(msg, ctx);
|
|
104
|
+
const text = textFromContent(msg);
|
|
105
|
+
const html = typeof msg.metadata.htmlBody === "string"
|
|
106
|
+
? msg.metadata.htmlBody
|
|
107
|
+
: undefined;
|
|
108
|
+
const attachments = validateAttachments(msg.metadata.mailAttachments);
|
|
109
|
+
// Only forward an explicit, opt-in hints object to the backend. We do NOT
|
|
110
|
+
// dump arbitrary internal message metadata to the external mail provider.
|
|
111
|
+
const backendHints = msg.metadata.mailBackendHints &&
|
|
112
|
+
typeof msg.metadata.mailBackendHints === "object"
|
|
113
|
+
? msg.metadata.mailBackendHints
|
|
114
|
+
: undefined;
|
|
115
|
+
return {
|
|
116
|
+
idempotencyKey: msg.id,
|
|
117
|
+
from: ctx.addressBook.resolveFrom(msg.sender_id),
|
|
118
|
+
to,
|
|
119
|
+
...(cc.length ? { cc } : {}),
|
|
120
|
+
...(bcc.length ? { bcc } : {}),
|
|
121
|
+
subject: msg.subject,
|
|
122
|
+
text,
|
|
123
|
+
...(html ? { html } : {}),
|
|
124
|
+
headers: {
|
|
125
|
+
messageId: mintMessageId(msg.id, domain),
|
|
126
|
+
...(inReplyTo ? { inReplyTo } : {}),
|
|
127
|
+
...(references && references.length ? { references } : {}),
|
|
128
|
+
},
|
|
129
|
+
...(attachments ? { attachments } : {}),
|
|
130
|
+
...(backendHints ? { backendHints } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Validate untrusted attachment metadata, dropping malformed entries. */
|
|
134
|
+
function validateAttachments(value) {
|
|
135
|
+
if (!Array.isArray(value))
|
|
136
|
+
return undefined;
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const a of value) {
|
|
139
|
+
if (a &&
|
|
140
|
+
typeof a === "object" &&
|
|
141
|
+
typeof a.contentRef === "string" &&
|
|
142
|
+
typeof a.contentType === "string") {
|
|
143
|
+
const att = a;
|
|
144
|
+
out.push({
|
|
145
|
+
contentRef: att.contentRef,
|
|
146
|
+
contentType: att.contentType,
|
|
147
|
+
sizeBytes: typeof att.sizeBytes === "number" ? att.sizeBytes : 0,
|
|
148
|
+
...(typeof att.filename === "string" ? { filename: att.filename } : {}),
|
|
149
|
+
...(typeof att.contentId === "string" ? { contentId: att.contentId } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out.length ? out : undefined;
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Helpers
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
function recipientsOfKind(msg, kind) {
|
|
159
|
+
return msg.recipients
|
|
160
|
+
.filter((r) => r.kind === kind)
|
|
161
|
+
.map((r) => ({ address: r.agent_id }));
|
|
162
|
+
}
|
|
163
|
+
/** Stable thread tag from the root of the References chain. */
|
|
164
|
+
function deriveThreadTag(references, inReplyTo, messageId) {
|
|
165
|
+
const root = (references && references.length > 0 ? references[0] : undefined) ??
|
|
166
|
+
inReplyTo ??
|
|
167
|
+
messageId;
|
|
168
|
+
if (!root)
|
|
169
|
+
return undefined;
|
|
170
|
+
const hash = createHash("sha1").update(root).digest("hex").slice(0, 12);
|
|
171
|
+
return `mail-${hash}`;
|
|
172
|
+
}
|
|
173
|
+
function buildThreadingHeaders(msg, ctx) {
|
|
174
|
+
if (!msg.in_reply_to) {
|
|
175
|
+
// Fresh thread, but a recorded RFC References chain (e.g. from the inbound
|
|
176
|
+
// side) should still be carried if present in metadata.
|
|
177
|
+
const meta = msg.metadata.rfcReferences;
|
|
178
|
+
return { references: Array.isArray(meta) ? meta : undefined };
|
|
179
|
+
}
|
|
180
|
+
const domain = ctx.messageIdDomain ?? ctx.addressBook.primaryDomain();
|
|
181
|
+
const parentRfcId = ctx.lookupRfcMessageId?.(msg.in_reply_to) ??
|
|
182
|
+
mintMessageId(msg.in_reply_to, domain);
|
|
183
|
+
const parentRefs = ctx.lookupReferences?.(msg.in_reply_to) ??
|
|
184
|
+
(Array.isArray(msg.metadata.rfcReferences)
|
|
185
|
+
? msg.metadata.rfcReferences
|
|
186
|
+
: []);
|
|
187
|
+
return {
|
|
188
|
+
inReplyTo: parentRfcId,
|
|
189
|
+
references: [...parentRefs, parentRfcId],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function mintMessageId(inboxId, domain) {
|
|
193
|
+
return `<${inboxId}@${domain}>`;
|
|
194
|
+
}
|
|
195
|
+
function textFromContent(msg) {
|
|
196
|
+
const c = msg.content;
|
|
197
|
+
if (c.type === "text" && typeof c.text === "string")
|
|
198
|
+
return c.text;
|
|
199
|
+
return JSON.stringify(c);
|
|
200
|
+
}
|
|
201
|
+
function importanceFromHeaders(raw) {
|
|
202
|
+
const header = (k) => {
|
|
203
|
+
const v = raw[k];
|
|
204
|
+
return (Array.isArray(v) ? v[0] : v ?? "").toString().toLowerCase();
|
|
205
|
+
};
|
|
206
|
+
const importance = header("importance");
|
|
207
|
+
const priority = header("x-priority");
|
|
208
|
+
if (importance === "high" || priority.startsWith("1") || priority.startsWith("2")) {
|
|
209
|
+
return "high";
|
|
210
|
+
}
|
|
211
|
+
if (importance === "low" || priority.startsWith("4") || priority.startsWith("5")) {
|
|
212
|
+
return "low";
|
|
213
|
+
}
|
|
214
|
+
return "normal";
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=email-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-mapper.js","sourceRoot":"","sources":["../../src/mail/email-mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-backed AttachmentStore — opt-in for large/high-volume deployments.
|
|
3
|
+
*
|
|
4
|
+
* Keeps attachment bytes out of the SQLite DB (no WAL bloat, no synchronous
|
|
5
|
+
* blob I/O blocking the event loop). Content-addressed by sha256, so identical
|
|
6
|
+
* bytes dedup. Files live at `<dir>/<aa>/<sha256>` (sharded by the first byte
|
|
7
|
+
* to avoid one enormous directory).
|
|
8
|
+
*
|
|
9
|
+
* Durability: writes go to a unique temp file, are fsync'd, then atomically
|
|
10
|
+
* renamed into place. Because the caller stores the attachment (this `put`)
|
|
11
|
+
* BEFORE committing the message row that references it, a crash can only ever
|
|
12
|
+
* leave an orphan file (reclaimable by GC) — never a row pointing at missing
|
|
13
|
+
* bytes. See docs/MAIL-INTEROP-PLAN.md §4c.
|
|
14
|
+
*/
|
|
15
|
+
import type { AttachmentStore } from "./types.js";
|
|
16
|
+
export declare class FsAttachmentStore implements AttachmentStore {
|
|
17
|
+
private dir;
|
|
18
|
+
constructor(dir: string);
|
|
19
|
+
put(bytes: Buffer, _meta: {
|
|
20
|
+
contentType: string;
|
|
21
|
+
filename?: string;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
get(ref: string): Promise<Buffer>;
|
|
24
|
+
delete(ref: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Garbage-collect orphan files: delete any stored attachment whose ref is not
|
|
27
|
+
* in `liveRefs` AND that is older than `minAgeMs` (default 1h).
|
|
28
|
+
*
|
|
29
|
+
* The age guard prevents a TOCTOU race: an attachment is `put` (file lands on
|
|
30
|
+
* disk) before the message row that references it is committed, so a just-
|
|
31
|
+
* written file may legitimately not appear in a `liveRefs` snapshot yet.
|
|
32
|
+
* Skipping recently-written files avoids deleting such in-flight attachments.
|
|
33
|
+
*/
|
|
34
|
+
gc(liveRefs: Set<string>, minAgeMs?: number): Promise<number>;
|
|
35
|
+
private pathForHex;
|
|
36
|
+
private pathForRef;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=fs-attachment-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-attachment-store.d.ts","sourceRoot":"","sources":["../../src/mail/fs-attachment-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlD,qBAAa,iBAAkB,YAAW,eAAe;IAC3C,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAEzB,GAAG,CACP,KAAK,EAAE,MAAM,EACb,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC,MAAM,CAAC;IAoCZ,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYjC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;;;;;OAQG;IACG,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAoC9E,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,UAAU;CASnB"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-backed AttachmentStore — opt-in for large/high-volume deployments.
|
|
3
|
+
*
|
|
4
|
+
* Keeps attachment bytes out of the SQLite DB (no WAL bloat, no synchronous
|
|
5
|
+
* blob I/O blocking the event loop). Content-addressed by sha256, so identical
|
|
6
|
+
* bytes dedup. Files live at `<dir>/<aa>/<sha256>` (sharded by the first byte
|
|
7
|
+
* to avoid one enormous directory).
|
|
8
|
+
*
|
|
9
|
+
* Durability: writes go to a unique temp file, are fsync'd, then atomically
|
|
10
|
+
* renamed into place. Because the caller stores the attachment (this `put`)
|
|
11
|
+
* BEFORE committing the message row that references it, a crash can only ever
|
|
12
|
+
* leave an orphan file (reclaimable by GC) — never a row pointing at missing
|
|
13
|
+
* bytes. See docs/MAIL-INTEROP-PLAN.md §4c.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import * as fs from "node:fs/promises";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
const REF_RE = /^sha256:([0-9a-f]{64})$/;
|
|
19
|
+
export class FsAttachmentStore {
|
|
20
|
+
dir;
|
|
21
|
+
constructor(dir) {
|
|
22
|
+
this.dir = dir;
|
|
23
|
+
}
|
|
24
|
+
async put(bytes, _meta) {
|
|
25
|
+
const hex = createHash("sha256").update(bytes).digest("hex");
|
|
26
|
+
const ref = `sha256:${hex}`;
|
|
27
|
+
const full = this.pathForHex(hex);
|
|
28
|
+
// Content-addressed: if the bytes are already stored, we're done.
|
|
29
|
+
if (await exists(full))
|
|
30
|
+
return ref;
|
|
31
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
32
|
+
// Atomic durable write: unique temp file → fsync → rename into place.
|
|
33
|
+
const tmp = `${full}.tmp-${process.pid}-${Date.now()}-${Math.random()
|
|
34
|
+
.toString(36)
|
|
35
|
+
.slice(2)}`;
|
|
36
|
+
let handle;
|
|
37
|
+
try {
|
|
38
|
+
handle = await fs.open(tmp, "wx");
|
|
39
|
+
await handle.writeFile(bytes);
|
|
40
|
+
await handle.sync();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await handle?.close();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await fs.rename(tmp, full);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
// A concurrent put of identical bytes may have won the race — that's fine
|
|
50
|
+
// (same content). Clean up our temp file and return the ref.
|
|
51
|
+
await fs.rm(tmp, { force: true });
|
|
52
|
+
if (!(await exists(full)))
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
// Durability: fsync the containing directory so the rename survives a crash
|
|
56
|
+
// (atomic rename alone is not crash-durable without a parent-dir fsync).
|
|
57
|
+
await fsyncDir(path.dirname(full));
|
|
58
|
+
return ref;
|
|
59
|
+
}
|
|
60
|
+
async get(ref) {
|
|
61
|
+
const full = this.pathForRef(ref);
|
|
62
|
+
try {
|
|
63
|
+
return await fs.readFile(full);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err.code === "ENOENT") {
|
|
67
|
+
throw new Error(`Attachment not found: ${ref}`);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async delete(ref) {
|
|
73
|
+
await fs.rm(this.pathForRef(ref), { force: true });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Garbage-collect orphan files: delete any stored attachment whose ref is not
|
|
77
|
+
* in `liveRefs` AND that is older than `minAgeMs` (default 1h).
|
|
78
|
+
*
|
|
79
|
+
* The age guard prevents a TOCTOU race: an attachment is `put` (file lands on
|
|
80
|
+
* disk) before the message row that references it is committed, so a just-
|
|
81
|
+
* written file may legitimately not appear in a `liveRefs` snapshot yet.
|
|
82
|
+
* Skipping recently-written files avoids deleting such in-flight attachments.
|
|
83
|
+
*/
|
|
84
|
+
async gc(liveRefs, minAgeMs = 3_600_000) {
|
|
85
|
+
let removed = 0;
|
|
86
|
+
const cutoff = Date.now() - minAgeMs;
|
|
87
|
+
let shards;
|
|
88
|
+
try {
|
|
89
|
+
shards = await fs.readdir(this.dir, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err.code === "ENOENT")
|
|
93
|
+
return 0;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
for (const shard of shards) {
|
|
97
|
+
if (!shard.isDirectory())
|
|
98
|
+
continue; // ignore stray files at the root
|
|
99
|
+
const shardDir = path.join(this.dir, shard.name);
|
|
100
|
+
let files;
|
|
101
|
+
try {
|
|
102
|
+
files = await fs.readdir(shardDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const name of files) {
|
|
108
|
+
if (name.includes(".tmp-"))
|
|
109
|
+
continue; // leave in-flight temp files
|
|
110
|
+
if (liveRefs.has(`sha256:${name}`))
|
|
111
|
+
continue;
|
|
112
|
+
const filePath = path.join(shardDir, name);
|
|
113
|
+
try {
|
|
114
|
+
const st = await fs.stat(filePath);
|
|
115
|
+
if (st.mtimeMs > cutoff)
|
|
116
|
+
continue; // too new — may be mid-ingestion
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
await fs.rm(filePath, { force: true });
|
|
122
|
+
removed++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return removed;
|
|
126
|
+
}
|
|
127
|
+
pathForHex(hex) {
|
|
128
|
+
// Shard by the first byte of the hash to bound directory fan-out.
|
|
129
|
+
return path.join(this.dir, hex.slice(0, 2), hex);
|
|
130
|
+
}
|
|
131
|
+
pathForRef(ref) {
|
|
132
|
+
const m = REF_RE.exec(ref);
|
|
133
|
+
if (!m) {
|
|
134
|
+
// Strict validation prevents path traversal via a crafted ref (refs are
|
|
135
|
+
// always sha256-of-content, never caller-chosen).
|
|
136
|
+
throw new Error(`Invalid attachment ref: ${ref}`);
|
|
137
|
+
}
|
|
138
|
+
return this.pathForHex(m[1]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function exists(p) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(p);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** fsync a directory so a rename into it is durable. Best-effort across platforms. */
|
|
151
|
+
async function fsyncDir(dir) {
|
|
152
|
+
let handle;
|
|
153
|
+
try {
|
|
154
|
+
handle = await fs.open(dir, "r");
|
|
155
|
+
await handle.sync();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Some platforms (e.g. Windows) don't permit fsync on a directory handle.
|
|
159
|
+
// Atomic rename still holds; this only weakens the durability guarantee.
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await handle?.close().catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=fs-attachment-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-attachment-store.js","sourceRoot":"","sources":["../../src/mail/fs-attachment-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailGateway — the only mail-aware component the rest of the system touches.
|
|
3
|
+
*
|
|
4
|
+
* Ingress: transport.onReceive → policy (size / allow-list / DMARC) → dedup →
|
|
5
|
+
* bounce-detect → map → commit via router.routeMessage (single ingestion path).
|
|
6
|
+
* The transport ACKs only after this resolves (commit-before-ACK); a throw
|
|
7
|
+
* NACKs so the sender retries.
|
|
8
|
+
*
|
|
9
|
+
* Egress: router calls send() for mail-class recipients. The message is rendered
|
|
10
|
+
* and attempted; transient failures are queued durably and retried; permanent
|
|
11
|
+
* failures (and inbound DSNs) converge on one config-gated bounce handler.
|
|
12
|
+
*
|
|
13
|
+
* See docs/MAIL-INTEROP-PLAN.md §7, §8.
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from "node:events";
|
|
16
|
+
import type { Storage } from "../storage/interface.js";
|
|
17
|
+
import type { MessageRouter, MailEgress } from "../router/message-router.js";
|
|
18
|
+
import { DeliveryQueue } from "../federation/delivery-queue.js";
|
|
19
|
+
import type { QueueStore } from "../federation/queue-store.js";
|
|
20
|
+
import type { Message, MailIdentityConfig, DeliveryQueueConfig } from "../types.js";
|
|
21
|
+
import type { MailTransport, InboundMail } from "./types.js";
|
|
22
|
+
export interface MailGatewayConfig {
|
|
23
|
+
identity: MailIdentityConfig;
|
|
24
|
+
/** Domains we send to without erroring; combined with identity.localDomains. */
|
|
25
|
+
routableDomains?: string[];
|
|
26
|
+
/** Override the transport's maxMessageBytes for inbound rejection. */
|
|
27
|
+
maxMessageBytes?: number;
|
|
28
|
+
/** Inbound sender allow-list (domains). Empty/undefined = allow all. */
|
|
29
|
+
allowedSenderDomains?: string[];
|
|
30
|
+
/** Drop inbound mail whose DMARC verdict is "fail". */
|
|
31
|
+
rejectDmarcFail?: boolean;
|
|
32
|
+
/** Max attachments on an inbound message before it is rejected. */
|
|
33
|
+
maxAttachments?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Inbound rate limiting (abuse control). Over-limit inbound is NACKed
|
|
36
|
+
* (transient) so legitimate senders back off and retry. Disabled if unset.
|
|
37
|
+
*/
|
|
38
|
+
rateLimit?: {
|
|
39
|
+
/** Sliding window, ms (default 60000). */
|
|
40
|
+
windowMs?: number;
|
|
41
|
+
/** Max inbound per sender domain per window. */
|
|
42
|
+
perSenderDomain?: number;
|
|
43
|
+
/** Max inbound across all senders per window. */
|
|
44
|
+
global?: number;
|
|
45
|
+
};
|
|
46
|
+
/** Bounce behavior; both paths default on. */
|
|
47
|
+
bounce?: {
|
|
48
|
+
emitEvent?: boolean;
|
|
49
|
+
synthesizeInboxMessage?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Domain used to mint outbound Message-IDs (defaults to primary local domain). */
|
|
52
|
+
messageIdDomain?: string;
|
|
53
|
+
/** Default inbox scope for inbound mail without plus-addressing. */
|
|
54
|
+
defaultScope?: string;
|
|
55
|
+
/** Delivery queue config for outbound retries. */
|
|
56
|
+
queue?: Partial<DeliveryQueueConfig>;
|
|
57
|
+
}
|
|
58
|
+
/** Thrown to NACK over-limit inbound so the sender backs off and retries. */
|
|
59
|
+
export declare class RateLimitedError extends Error {
|
|
60
|
+
readonly scope: string;
|
|
61
|
+
constructor(scope: string);
|
|
62
|
+
}
|
|
63
|
+
export declare class MailGateway implements MailEgress {
|
|
64
|
+
private transport;
|
|
65
|
+
private storage;
|
|
66
|
+
private router;
|
|
67
|
+
private events;
|
|
68
|
+
private config;
|
|
69
|
+
private addressBook;
|
|
70
|
+
private mailDomains;
|
|
71
|
+
private senderLimiter;
|
|
72
|
+
private globalLimiter;
|
|
73
|
+
readonly queue: DeliveryQueue;
|
|
74
|
+
private retryTimer?;
|
|
75
|
+
private retrying;
|
|
76
|
+
constructor(opts: {
|
|
77
|
+
transport: MailTransport;
|
|
78
|
+
storage: Storage;
|
|
79
|
+
router: MessageRouter;
|
|
80
|
+
events: EventEmitter;
|
|
81
|
+
config: MailGatewayConfig;
|
|
82
|
+
/** Durable queue store for outbound retries (sqlite). Memory if omitted. */
|
|
83
|
+
queueStore?: QueueStore;
|
|
84
|
+
});
|
|
85
|
+
/** Start the underlying transport and the retry/TTL timers. */
|
|
86
|
+
start(intervalMs?: number): Promise<void>;
|
|
87
|
+
stop(): Promise<void>;
|
|
88
|
+
/** True if this recipient's domain is one we route mail for. */
|
|
89
|
+
ownsRecipient(agentId: string): boolean;
|
|
90
|
+
/** Render and deliver the mail-class recipients of a message. */
|
|
91
|
+
send(message: Message): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Handle one inbound message. Resolves on ACK; throws on NACK (transient
|
|
94
|
+
* failure the sender should retry). Policy rejections ACK-and-drop so the
|
|
95
|
+
* sender does not retry forever.
|
|
96
|
+
*/
|
|
97
|
+
handleInbound(mail: InboundMail): Promise<void>;
|
|
98
|
+
/** Attempt delivery; classify the result and queue/bounce accordingly. */
|
|
99
|
+
private attemptDelivery;
|
|
100
|
+
/** Re-attempt all retryable queued mail. Call from a timer or deterministically. */
|
|
101
|
+
processRetries(): Promise<void>;
|
|
102
|
+
private markDelivered;
|
|
103
|
+
private handleInboundBounce;
|
|
104
|
+
private bounce;
|
|
105
|
+
/** Inject a bounce notification into the original sender's inbox. */
|
|
106
|
+
private synthesizeBounce;
|
|
107
|
+
private reject;
|
|
108
|
+
/** Throw RateLimitedError (→ NACK) when an inbound limiter is exceeded. */
|
|
109
|
+
private enforceRateLimits;
|
|
110
|
+
private senderAllowed;
|
|
111
|
+
/** Dedup key: RFC Message-ID when present, else a synthetic content hash. */
|
|
112
|
+
private externalIdFor;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=mail-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-gateway.d.ts","sourceRoot":"","sources":["../../src/mail/mail-gateway.ts"],"names":[],"mappings":"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"}
|