@open-workspace/email 0.0.0-stage → 0.1.1

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.
Files changed (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -2
  3. package/alchemy.ts +126 -0
  4. package/containers/office-preview/Dockerfile +10 -0
  5. package/index.ts +3 -0
  6. package/migrations/0000_email.sql +53 -0
  7. package/migrations/meta/0000_snapshot.json +408 -0
  8. package/migrations/meta/_journal.json +13 -0
  9. package/package.json +91 -4
  10. package/shared/attachment-preview.ts +67 -0
  11. package/shared/mail-authentication.ts +145 -0
  12. package/shared/mail-feedback.ts +90 -0
  13. package/shared/mail-headers.ts +250 -0
  14. package/shared/mail-received.ts +298 -0
  15. package/shared/mail-remote.ts +17 -0
  16. package/shared/mail-rules.ts +156 -0
  17. package/shared/mail-sender-intel.ts +125 -0
  18. package/shared/mail-translation.ts +2 -0
  19. package/shared/mail.ts +179 -0
  20. package/shared/office-preview.ts +26 -0
  21. package/worker/admin/report-routes.ts +164 -0
  22. package/worker/bindings.d.ts +11 -0
  23. package/worker/db/schema.ts +97 -0
  24. package/worker/mail/access.ts +30 -0
  25. package/worker/mail/ai-classification.ts +119 -0
  26. package/worker/mail/ai-translation.ts +31 -0
  27. package/worker/mail/auth-headers.ts +557 -0
  28. package/worker/mail/bimi.ts +172 -0
  29. package/worker/mail/classification-signals.ts +116 -0
  30. package/worker/mail/conversation-cursor.ts +43 -0
  31. package/worker/mail/delivery-events.ts +99 -0
  32. package/worker/mail/delivery-facts.ts +122 -0
  33. package/worker/mail/demo-routes.ts +33 -0
  34. package/worker/mail/doh.ts +74 -0
  35. package/worker/mail/downloads.ts +47 -0
  36. package/worker/mail/header-metadata.ts +77 -0
  37. package/worker/mail/impersonation.ts +175 -0
  38. package/worker/mail/inbound-delivery.ts +22 -0
  39. package/worker/mail/inbound-threading.ts +48 -0
  40. package/worker/mail/inbound.ts +255 -0
  41. package/worker/mail/ip-intel.ts +135 -0
  42. package/worker/mail/link-domains.ts +57 -0
  43. package/worker/mail/mailbox-deletion.ts +18 -0
  44. package/worker/mail/mailbox-directory.ts +73 -0
  45. package/worker/mail/mailing-list.ts +120 -0
  46. package/worker/mail/message-security.ts +190 -0
  47. package/worker/mail/message-view.ts +63 -0
  48. package/worker/mail/office-conversion.ts +38 -0
  49. package/worker/mail/office-preview-container.ts +57 -0
  50. package/worker/mail/office-preview-queue.ts +49 -0
  51. package/worker/mail/org-domain.ts +15 -0
  52. package/worker/mail/outbound-content.ts +208 -0
  53. package/worker/mail/outbound-delivery.ts +83 -0
  54. package/worker/mail/outbound-service.ts +145 -0
  55. package/worker/mail/outbound-threading.ts +70 -0
  56. package/worker/mail/outbound.ts +446 -0
  57. package/worker/mail/outgoing-context.ts +122 -0
  58. package/worker/mail/participants.ts +115 -0
  59. package/worker/mail/push-notifications.ts +484 -0
  60. package/worker/mail/quote-parser.ts +50 -0
  61. package/worker/mail/r2-presigned-urls.ts +69 -0
  62. package/worker/mail/receive.ts +59 -0
  63. package/worker/mail/recipients.ts +34 -0
  64. package/worker/mail/remote-proxy.ts +140 -0
  65. package/worker/mail/reply-plan.ts +136 -0
  66. package/worker/mail/reported-messages.ts +68 -0
  67. package/worker/mail/rfc.ts +73 -0
  68. package/worker/mail/routes.ts +1434 -0
  69. package/worker/mail/safety-routes.ts +350 -0
  70. package/worker/mail/schemas.ts +339 -0
  71. package/worker/mail/search.ts +14 -0
  72. package/worker/mail/security-settings.ts +38 -0
  73. package/worker/mail/sender-intel.ts +189 -0
  74. package/worker/mail/spamhaus.ts +97 -0
  75. package/worker/mail/unsubscribe.ts +118 -0
  76. package/worker/mail/uploads.ts +537 -0
  77. package/worker/mail/webhook-source.ts +71 -0
  78. package/worker/mail/workspace-safety.ts +92 -0
  79. package/worker/mailbox/demo-html.ts +136 -0
  80. package/worker/mailbox/demo-seed.ts +79 -0
  81. package/worker/mailbox/demo.ts +978 -0
  82. package/worker/mailbox/folder-model.ts +72 -0
  83. package/worker/mailbox/index.ts +2508 -0
  84. package/worker/mailbox/legacy-migrations.ts +41 -0
  85. package/worker/mailbox/migrations.ts +10 -0
  86. package/worker/mailbox/model.ts +65 -0
  87. package/worker/mailbox/office-preview-jobs.ts +73 -0
  88. package/worker/mailbox/rules.ts +110 -0
  89. package/worker/mailbox/schema.ts +353 -0
  90. package/worker/mailbox/senders.ts +240 -0
  91. package/worker/mcp/mail-action-tools.ts +210 -0
  92. package/worker/mcp/mail-compose-tools.ts +196 -0
  93. package/worker/mcp/mail-query-tools.ts +196 -0
  94. package/worker/mcp/mail-safety-tools.ts +452 -0
  95. package/worker/mcp/tools.ts +15 -0
  96. package/worker/module.ts +59 -0
@@ -0,0 +1,978 @@
1
+ import type { AuthResult, DmarcPolicy } from "../../shared/mail-authentication";
2
+ import type { SenderIntel, SpamhausListing } from "../../shared/mail-sender-intel";
3
+ import { inboundHeaderMetadata } from "../mail/auth-headers";
4
+ import { bimiObjectKey } from "../mail/bimi";
5
+ import { DEMO_UNSUBSCRIBE_HOST } from "../mail/demo-routes";
6
+ import { denseDiscoveryHtml, figmaInvoiceHtml, hirelineHtml } from "./demo-html";
7
+ import type { EmailAiClassification, MailAddress } from "./model";
8
+ import type { MailRuleInput } from "./rules";
9
+ import type { NewEmail, NewFolder } from "./schema";
10
+
11
+ type DemoInput = {
12
+ id: string;
13
+ conversationId: string;
14
+ messageId: string;
15
+ direction?: "incoming" | "outgoing";
16
+ from: MailAddress;
17
+ replyTo?: MailAddress;
18
+ to: MailAddress[];
19
+ cc?: MailAddress[];
20
+ subject: string;
21
+ preview: string;
22
+ body: string;
23
+ ageMs: number;
24
+ references?: string[];
25
+ html?: string;
26
+ /** Seeds a raw original with stored authentication and AI check results. */
27
+ checks?: DemoChecks;
28
+ };
29
+
30
+ /**
31
+ * What Cloudflare Email Routing would have reported for the message. The raw
32
+ * original is rendered with Cloudflare's header block from this, and the
33
+ * stored results are parsed back out of it by the production parser.
34
+ */
35
+ type DemoAuthentication = {
36
+ /** Connecting IP (documentation ranges only). */
37
+ ip: string;
38
+ helo: string;
39
+ /** SMTP MAIL FROM. */
40
+ mailFrom: string;
41
+ spf: AuthResult;
42
+ spfHelo: AuthResult;
43
+ dkim: Array<{ domain: string; selector: string; result: AuthResult; signs: string[] }>;
44
+ dmarc: AuthResult;
45
+ policy: DmarcPolicy | null;
46
+ spamScore: number;
47
+ /** Delivered through a forwarder that sealed an ARC set first. */
48
+ forwardedVia?: { sealer: string; authservId: string; helo: string; ip: string };
49
+ };
50
+
51
+ /**
52
+ * Pre-computed sender intelligence. Demo IPs are documentation ranges, so
53
+ * live DNS would find nothing; the rows carry what a real lookup would show.
54
+ * ASNs 64496–64511 are reserved for documentation.
55
+ */
56
+ type DemoIntel = {
57
+ ptr: string | null;
58
+ fcrdns?: boolean;
59
+ generic?: boolean;
60
+ asn: number;
61
+ asName: string;
62
+ prefix: string;
63
+ country: string;
64
+ registry?: string;
65
+ /** As if a Spamhaus DQS key were configured. */
66
+ spamhaus?: {
67
+ zen?: Array<Pick<SpamhausListing, "code" | "list" | "severity">>;
68
+ dbl?: Array<{ domain: string; code: string; list: string }>;
69
+ domains: string[];
70
+ };
71
+ /** The From domain serves the demo BIMI logo seeded next to the rows. */
72
+ bimi?: boolean;
73
+ };
74
+
75
+ type DemoChecks = {
76
+ /** Bulk senders connect straight to the MX without an internal hop. */
77
+ direct?: boolean;
78
+ intel?: DemoIntel;
79
+ mailer?: string;
80
+ list?: { id: string };
81
+ authentication: DemoAuthentication;
82
+ ai: Pick<
83
+ EmailAiClassification,
84
+ "spam" | "spamConfidence" | "folderId" | "folderConfidence" | "reason"
85
+ >;
86
+ };
87
+
88
+ /** R2 keys for demo message bodies; the objects are seeded next to the rows. */
89
+ function demoHtmlKey(id: string) {
90
+ return `demo/messages/${id}/body.html`;
91
+ }
92
+
93
+ function demoRawKey(id: string) {
94
+ return `demo/messages/${id}/original.eml`;
95
+ }
96
+
97
+ function mimeAddress(address: MailAddress) {
98
+ return address.name ? `"${address.name}" <${address.address}>` : address.address;
99
+ }
100
+
101
+ function demoToken(seed: string, length: number) {
102
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
103
+ let hash = 2_166_136_261;
104
+ for (let index = 0; index < seed.length; index += 1) {
105
+ hash = Math.imul(hash ^ seed.charCodeAt(index), 16_777_619) >>> 0;
106
+ }
107
+ let token = "";
108
+ while (token.length < length) {
109
+ // xorshift32 keeps the demo signatures stable between seeds and renders.
110
+ hash ^= hash << 13;
111
+ hash ^= hash >>> 17;
112
+ hash ^= hash << 5;
113
+ hash >>>= 0;
114
+ token += alphabet[hash % alphabet.length];
115
+ }
116
+ return token;
117
+ }
118
+
119
+ function spfComment(result: AuthResult, identity: string, ip: string) {
120
+ if (result === "pass") return `domain of ${identity} designates ${ip} as permitted sender`;
121
+ if (result === "none") return `no SPF records found for ${identity}`;
122
+ if (result === "softfail") return `domain of transitioning ${identity} does not designate ${ip} as permitted sender`;
123
+ return `domain of ${identity} does not designate ${ip} as permitted sender`;
124
+ }
125
+
126
+ /** `Name: authserv-id;` followed by one folded line per result. */
127
+ function folded(prefix: string, parts: string[]) {
128
+ return `${prefix};\r\n\t${parts.join(";\r\n\t")}`;
129
+ }
130
+
131
+ /** Cloudflare's prepended header block, topmost first, as seen on production mail. */
132
+ function cloudflareHeaders(input: DemoInput, auth: DemoAuthentication, at: (offsetMs: number) => string) {
133
+ const fromDomain = input.from.address.split("@")[1] ?? "invalid";
134
+ const recipient = input.to[0]?.address ?? "user@demo.example";
135
+ const instance = auth.forwardedVia ? 2 : 1;
136
+ const results = [
137
+ ...(auth.dkim.length
138
+ ? auth.dkim.map((dkim) =>
139
+ `dkim=${dkim.result} header.d=${dkim.domain} header.s=${dkim.selector} header.b=${demoToken(`${input.id}${dkim.domain}`, 8)}`
140
+ )
141
+ : ["dkim=none"]),
142
+ `dmarc=${auth.dmarc} header.from=${fromDomain}${auth.policy ? ` policy.dmarc=${auth.policy}` : ""}`,
143
+ `spf=${auth.spfHelo} (mx.cloudflare.net: ${spfComment(auth.spfHelo, `postmaster@${auth.helo}`, auth.ip)}) smtp.helo=${auth.helo}`,
144
+ `spf=${auth.spf} (mx.cloudflare.net: ${spfComment(auth.spf, auth.mailFrom, auth.ip)}) smtp.mailfrom=${auth.mailFrom}`,
145
+ `arc=${auth.forwardedVia ? "pass" : "none"} smtp.remote-ip=${auth.ip}`,
146
+ ];
147
+ return [
148
+ `Received: from ${auth.helo} (${auth.ip})`,
149
+ `\tby cloudflare-email.net (cloudflare) id ${demoToken(input.id, 12)}`,
150
+ `\tfor <${recipient}>; ${at(0)}`,
151
+ `ARC-Seal: i=${instance}; a=rsa-sha256; s=cf2024-1; d=cloudflare-email.net; cv=${instance > 1 ? "pass" : "none"};`,
152
+ `\tb=${demoToken(`${input.id}seal`, 64)}`,
153
+ `ARC-Message-Signature: i=${instance}; a=rsa-sha256; s=cf2024-1; d=cloudflare-email.net; c=relaxed/relaxed;`,
154
+ "\th=To:Subject:From:Date:from:reply-to:cc:in-reply-to:references:list-id; bh=",
155
+ `\t${demoToken(`${input.id}bh`, 43)}=; b=${demoToken(`${input.id}ams`, 64)}`,
156
+ folded(`ARC-Authentication-Results: i=${instance}; mx.cloudflare.net`, results),
157
+ `Received-SPF: ${auth.spf} (mx.cloudflare.net: ${spfComment(auth.spf, auth.mailFrom, auth.ip)})`,
158
+ `\treceiver=mx.cloudflare.net; client-ip=${auth.ip}; envelope-from="${auth.mailFrom}"; helo=${auth.helo};`,
159
+ folded("Authentication-Results: mx.cloudflare.net", results),
160
+ `X-CF-SpamH-Score: ${auth.spamScore}`,
161
+ ];
162
+ }
163
+
164
+ function forwarderHeaders(input: DemoInput, auth: DemoAuthentication, at: (offsetMs: number) => string) {
165
+ const via = auth.forwardedVia;
166
+ if (!via) return [];
167
+ const fromDomain = input.from.address.split("@")[1] ?? "invalid";
168
+ const original = auth.dkim.find((dkim) => dkim.domain === fromDomain);
169
+ return [
170
+ `ARC-Seal: i=1; a=rsa-sha256; t=1793000000; cv=none; d=${via.sealer}; s=arc-20240605;`,
171
+ `\tb=${demoToken(`${input.id}upseal`, 64)}`,
172
+ `ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=${via.sealer}; s=arc-20240605;`,
173
+ `\th=to:subject:message-id:date:from:mime-version; bh=${demoToken(`${input.id}upbh`, 43)}=; b=${demoToken(`${input.id}upams`, 48)}`,
174
+ folded(`ARC-Authentication-Results: i=1; ${via.authservId}`, [
175
+ `dkim=pass header.i=@${fromDomain} header.s=${original?.selector ?? "mail"}`,
176
+ `spf=pass (${via.authservId}: domain of ${input.from.address} designates ${via.ip} as permitted sender) smtp.mailfrom=${input.from.address}`,
177
+ `dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=${fromDomain}`,
178
+ ]),
179
+ `Received: by 2001:db8:4864:20::62a with SMTP id ${demoToken(`${input.id}fw`, 10)};`,
180
+ `\t${at(-2_000)}`,
181
+ folded(`Authentication-Results: ${via.authservId}`, [
182
+ `dkim=pass header.i=@${fromDomain} header.s=${original?.selector ?? "mail"}`,
183
+ `spf=pass smtp.mailfrom=${input.from.address}`,
184
+ `dmarc=pass header.from=${fromDomain}`,
185
+ ]),
186
+ `Received: from ${via.helo} (${via.helo} [${via.ip}])`,
187
+ `\tby ${via.authservId} with ESMTPS id ${demoToken(`${input.id}mx`, 10)}`,
188
+ `\tfor <${input.to[0]?.address ?? "user@demo.example"}>`,
189
+ `\t(Google Transport Security); ${at(-3_000)}`,
190
+ ];
191
+ }
192
+
193
+ function dkimSignatureHeaders(input: DemoInput, auth: DemoAuthentication) {
194
+ return auth.dkim.flatMap((dkim) => [
195
+ `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=${dkim.domain}; s=${dkim.selector};`,
196
+ `\th=${dkim.signs.join(":")};`,
197
+ `\tbh=${demoToken(`${input.id}${dkim.domain}bh`, 43)}=;`,
198
+ `\tb=${demoToken(`${input.id}${dkim.domain}`, 8)}${demoToken(`${input.id}${dkim.domain}sig`, 56)}`,
199
+ ]);
200
+ }
201
+
202
+ /** A plausible RFC 5322 original for the "Show original" view. */
203
+ function demoRawMime(input: DemoInput, now: number) {
204
+ const date = new Date(now - input.ageMs).toUTCString().replace("GMT", "+0000");
205
+ const fromDomain = input.from.address.split("@")[1] ?? "invalid";
206
+ const at = (offsetMs: number) =>
207
+ new Date(now - input.ageMs + offsetMs).toUTCString().replace("GMT", "+0000");
208
+ const checks = input.checks;
209
+ const auth = checks?.authentication;
210
+ const senderHops = !auth || checks?.direct || auth.forwardedVia
211
+ ? []
212
+ : [
213
+ "Received: from app-3.internal ([10.12.4.8])",
214
+ `\tby ${auth.helo} with ESMTPSA id ${input.id}-out; ${at(-4_000)}`,
215
+ ];
216
+ const unsubscribeId = demoToken(`${input.id}unsubscribe`, 16).replace(/[+/]/gu, "x");
217
+ const headers = [
218
+ ...(auth ? cloudflareHeaders(input, auth, at) : []),
219
+ ...(auth ? forwarderHeaders(input, auth, at) : []),
220
+ ...(auth ? dkimSignatureHeaders(input, auth) : []),
221
+ ...senderHops,
222
+ `From: ${mimeAddress(input.from)}`,
223
+ ...(input.replyTo ? [`Reply-To: ${mimeAddress(input.replyTo)}`] : []),
224
+ `To: ${input.to.map(mimeAddress).join(", ")}`,
225
+ ...(input.cc?.length ? [`Cc: ${input.cc.map(mimeAddress).join(", ")}`] : []),
226
+ `Subject: ${input.subject}`,
227
+ `Date: ${date}`,
228
+ `Message-ID: ${input.messageId}`,
229
+ ...(checks?.mailer ? [`X-Mailer: ${checks.mailer}`] : []),
230
+ ...(checks?.list
231
+ ? [
232
+ `List-Id: <${checks.list.id}>`,
233
+ // A demo-local endpoint answered in-process, never the real sender.
234
+ `List-Unsubscribe: <https://${DEMO_UNSUBSCRIBE_HOST}/one-click/${unsubscribeId}>, <mailto:unsubscribe+${unsubscribeId}@${DEMO_UNSUBSCRIBE_HOST}?subject=unsubscribe>`,
235
+ "List-Unsubscribe-Post: List-Unsubscribe=One-Click",
236
+ "Precedence: bulk",
237
+ ]
238
+ : []),
239
+ "MIME-Version: 1.0",
240
+ ];
241
+ const text = input.body.replace(/\n/gu, "\r\n");
242
+ if (!input.html) {
243
+ return [...headers, "Content-Type: text/plain; charset=utf-8", "Content-Transfer-Encoding: 8bit", "", text, ""].join("\r\n");
244
+ }
245
+ const boundary = `demo-${input.id}`;
246
+ return [
247
+ ...headers,
248
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
249
+ "",
250
+ `--${boundary}`,
251
+ "Content-Type: text/plain; charset=utf-8",
252
+ "Content-Transfer-Encoding: 8bit",
253
+ "",
254
+ text,
255
+ `--${boundary}`,
256
+ "Content-Type: text/html; charset=utf-8",
257
+ "Content-Transfer-Encoding: 8bit",
258
+ "",
259
+ input.html,
260
+ `--${boundary}--`,
261
+ "",
262
+ ].join("\r\n");
263
+ }
264
+
265
+ function demoSenderIntel(input: DemoInput, checkedAt: number): SenderIntel | null {
266
+ const checks = input.checks;
267
+ const intel = checks?.intel;
268
+ if (!checks || !intel) return null;
269
+ const ip = checks.authentication.ip;
270
+ const fromDomain = input.from.address.split("@")[1] ?? "invalid";
271
+ const fcrdns = intel.ptr ? intel.fcrdns ?? true : false;
272
+ const helo = checks.authentication.helo.toLocaleLowerCase();
273
+ return {
274
+ version: 1,
275
+ source: "demo",
276
+ checkedAt,
277
+ ip,
278
+ ipVersion: ip.includes(":") ? 6 : 4,
279
+ ptr: {
280
+ status: intel.ptr ? "ok" : "nxdomain",
281
+ names: intel.ptr ? [intel.ptr] : [],
282
+ confirmed: intel.ptr && fcrdns ? intel.ptr : null,
283
+ fcrdns,
284
+ generic: intel.ptr ? intel.generic ?? false : null,
285
+ },
286
+ heloMatchesPtr: intel.ptr
287
+ ? helo === intel.ptr || helo.split(".").slice(-2).join(".") === intel.ptr.split(".").slice(-2).join(".")
288
+ : null,
289
+ network: {
290
+ status: "ok",
291
+ asn: intel.asn,
292
+ asName: intel.asName,
293
+ prefix: intel.prefix,
294
+ country: intel.country,
295
+ registry: intel.registry ?? "arin",
296
+ },
297
+ spamhaus: intel.spamhaus
298
+ ? {
299
+ zen: {
300
+ status: "ok",
301
+ error: null,
302
+ listings: (intel.spamhaus.zen ?? []).map((listing) => ({ zone: "zen", target: ip, ...listing })),
303
+ },
304
+ dbl: {
305
+ status: "ok",
306
+ error: null,
307
+ domains: intel.spamhaus.domains,
308
+ listings: (intel.spamhaus.dbl ?? []).map((listing) => ({
309
+ zone: "dbl",
310
+ target: listing.domain,
311
+ code: listing.code,
312
+ list: listing.list,
313
+ severity: "abuse",
314
+ })),
315
+ },
316
+ }
317
+ : null,
318
+ bimi: intel.bimi ? { domain: fromDomain, status: "ok", reason: null } : null,
319
+ };
320
+ }
321
+
322
+ function email(input: DemoInput, now: number): NewEmail {
323
+ const timelineAt = new Date(now - input.ageMs);
324
+ const outgoing = input.direction === "outgoing";
325
+ return {
326
+ id: input.id,
327
+ conversationId: input.conversationId,
328
+ direction: outgoing ? "outgoing" : "incoming",
329
+ messageIdHeader: input.messageId,
330
+ inReplyToJson: input.references?.slice(-1) ?? [],
331
+ referencesJson: input.references ?? [],
332
+ fromJson: [input.from],
333
+ replyToJson: input.replyTo ? [input.replyTo] : [],
334
+ listId: input.checks?.list?.id ?? null,
335
+ toJson: input.to,
336
+ ccJson: input.cc ?? [],
337
+ subject: input.subject,
338
+ preview: input.preview,
339
+ bodyText: input.body,
340
+ bodyHtmlR2Key: input.html ? demoHtmlKey(input.id) : null,
341
+ rawMimeR2Key: input.checks ? demoRawKey(input.id) : null,
342
+ // Parsed by the production parser from the same original seeded into R2.
343
+ ...(input.checks
344
+ ? inboundHeaderMetadata({
345
+ rawHeaders: demoRawMime(input, now),
346
+ fromAddress: input.from.address,
347
+ envelopeFrom: input.checks.authentication.mailFrom,
348
+ now: timelineAt.getTime() + 1_000,
349
+ })
350
+ : {}),
351
+ envelopeFrom: input.checks?.authentication.mailFrom ?? null,
352
+ envelopeTo: outgoing ? null : input.to[0]?.address ?? null,
353
+ senderIntelJson: demoSenderIntel(input, timelineAt.getTime() + 1_500),
354
+ aiClassificationJson: input.checks
355
+ ? {
356
+ source: "workers-ai",
357
+ model: "@cf/meta/llama-3.1-8b-instruct",
358
+ processedAt: timelineAt.getTime() + 2_000,
359
+ ...input.checks.ai,
360
+ }
361
+ : null,
362
+ timelineAt,
363
+ transportState: outgoing ? "submitted" : "received",
364
+ deliveryStatusJson: outgoing
365
+ ? input.to.map((recipient, index) => ({
366
+ recipient: recipient.address,
367
+ status: "delivered" as const,
368
+ eventId: `demo_${input.id}_${index}`,
369
+ eventAt: timelineAt.getTime() + 1_500,
370
+ smtpCode: "250",
371
+ detail: null,
372
+ }))
373
+ : [],
374
+ };
375
+ }
376
+
377
+ const ilya = { address: "ilya@demo.example", name: "Ilya Morozov" };
378
+ const support = { address: "support@demo.example", name: "Customer care" };
379
+
380
+ function demoInputs(mailboxId: string): DemoInput[] {
381
+ if (mailboxId === "mbx_demo_personal") {
382
+ return [
383
+ ({
384
+ id: "msg_demo_01",
385
+ conversationId: "conv_demo_launch",
386
+ messageId: "<launch@linear.demo>",
387
+ from: { address: "karri@linear.app", name: "Karri Saarinen" },
388
+ to: [ilya],
389
+ subject: "The craft behind fast software",
390
+ preview: "I kept thinking about our conversation on speed and quality. The teams that move fastest rarely skip the details.",
391
+ body: "Hey Ilya,\n\nI kept thinking about our conversation on speed and quality. The teams that move fastest rarely skip the details — they make the right details easier to see.\n\nHere are the notes from our design review and the three decisions we should make this week.\n\nKarri",
392
+ ageMs: 540_000,
393
+ }),
394
+ ({
395
+ id: "msg_demo_07",
396
+ conversationId: "conv_demo_launch",
397
+ messageId: "<reply@openworkspace.demo>",
398
+ direction: "outgoing",
399
+ from: ilya,
400
+ to: [{ address: "karri@linear.app", name: "Karri Saarinen" }],
401
+ subject: "Re: The craft behind fast software",
402
+ preview: "That framing is exactly right. I turned the decisions into a smaller review doc.",
403
+ body: "That framing is exactly right. I turned the decisions into a smaller review doc and will send the final version tomorrow.\n\nThanks,\nIlya",
404
+ ageMs: 180_000,
405
+ references: ["<launch@linear.demo>"],
406
+ }),
407
+ ({
408
+ id: "msg_demo_02",
409
+ conversationId: "conv_demo_cloudflare",
410
+ messageId: "<workers@cloudflare.demo>",
411
+ from: { address: "jessica@cloudflare.com", name: "Jessica Lee" },
412
+ to: [ilya],
413
+ cc: [{ address: "maya@demo.example", name: "Maya Chen" }],
414
+ subject: "Your Workers architecture review",
415
+ preview: "The per-mailbox SQLite design looks solid. Keeping raw MIME in R2 leaves the mailbox database focused.",
416
+ body: "Hi Ilya,\n\nThe per-mailbox SQLite design looks solid. Keeping raw MIME in R2 leaves the mailbox database focused on RFC metadata and conversation state.\n\nI left a few notes on retries and idempotency below.\n\nBest,\nJessica",
417
+ ageMs: 4_200_000,
418
+ checks: {
419
+ mailer: "Apple Mail (2.3826.400)",
420
+ intel: { ptr: "mail-yw1-f42.google.com", asn: 15169, asName: "Google LLC", prefix: "198.51.100.0/24", country: "US" },
421
+ authentication: {
422
+ ip: "198.51.100.42",
423
+ helo: "smtp-out.cloudflare.com",
424
+ mailFrom: "jessica@cloudflare.com",
425
+ spf: "pass",
426
+ spfHelo: "none",
427
+ dkim: [{ domain: "cloudflare.com", selector: "google", result: "pass", signs: ["from", "to", "cc", "subject", "date", "message-id"] }],
428
+ dmarc: "pass",
429
+ policy: "reject",
430
+ spamScore: 0,
431
+ },
432
+ ai: {
433
+ spam: false,
434
+ spamConfidence: 0.02,
435
+ folderId: null,
436
+ folderConfidence: 0.12,
437
+ reason: "Personal work correspondence from a known contact replying to an architecture review.",
438
+ },
439
+ },
440
+ }),
441
+ ({
442
+ id: "msg_demo_03",
443
+ conversationId: "conv_demo_maya",
444
+ messageId: "<maya-update@demo>",
445
+ from: { address: "maya@demo.example", name: "Maya Chen" },
446
+ to: [ilya],
447
+ subject: "Friday launch checklist",
448
+ preview: "I grouped the remaining work into launch blockers and follow-ups. We are down to four real blockers.",
449
+ body: "I grouped the remaining work into launch blockers and follow-ups. We are down to four real blockers:\n\n1. Verify catch-all routing\n2. Run mobile QA\n3. Confirm outbound domain\n4. Write the setup guide\n\nEverything else can follow next week.",
450
+ ageMs: 86_400_000,
451
+ }),
452
+ ({
453
+ id: "msg_demo_04",
454
+ conversationId: "conv_demo_vercel",
455
+ messageId: "<design@vercel.demo>",
456
+ from: { address: "rauno@vercel.com", name: "Rauno Freiberg" },
457
+ to: [ilya],
458
+ subject: "Re: interaction details",
459
+ preview: "The reduced motion version feels right now. I would keep the mailbox transition almost instant.",
460
+ body: "The reduced-motion build is ready for review. I shortened the mailbox transition and kept the composer animation subtle.\n\nThe updated interaction notes are attached to the ticket.",
461
+ ageMs: 172_800_000,
462
+ }),
463
+ ({
464
+ id: "msg_demo_05",
465
+ conversationId: "conv_demo_weekly",
466
+ messageId: "<weekly@paper.demo>",
467
+ from: { address: "digest@dense-discovery.com", name: "Dense Discovery" },
468
+ to: [ilya],
469
+ subject: "Issue 341 — tools for thought",
470
+ preview: "A small collection of thoughtful products, strange links, and ideas worth carrying into the weekend.",
471
+ body: "A small collection of thoughtful products, strange links, and ideas worth carrying into the weekend.\n\nThis week: humane inboxes, local-first databases, and why fast tools feel calm.",
472
+ ageMs: 259_200_000,
473
+ html: denseDiscoveryHtml,
474
+ checks: {
475
+ mailer: "Campaigner 4.2",
476
+ intel: { ptr: "mta-7.campaigner-mail.example", asn: 64500, asName: "Campaigner Mail Services", prefix: "203.0.113.0/25", country: "CA" },
477
+ list: { id: "weekly.dense-discovery.com" },
478
+ authentication: {
479
+ ip: "203.0.113.24",
480
+ helo: "mta-7.campaigner-mail.example",
481
+ mailFrom: "bounce-341-8812@mail.dense-discovery.com",
482
+ spf: "pass",
483
+ spfHelo: "none",
484
+ dkim: [
485
+ {
486
+ domain: "dense-discovery.com",
487
+ selector: "cm1",
488
+ result: "pass",
489
+ signs: ["from", "to", "subject", "date", "message-id", "list-id", "list-unsubscribe", "list-unsubscribe-post"],
490
+ },
491
+ { domain: "campaigner-mail.example", selector: "esp", result: "pass", signs: ["from", "to", "subject", "date"] },
492
+ ],
493
+ dmarc: "pass",
494
+ policy: "quarantine",
495
+ spamScore: 1,
496
+ },
497
+ ai: {
498
+ spam: false,
499
+ spamConfidence: 0.07,
500
+ folderId: null,
501
+ folderConfidence: 0.22,
502
+ reason: "Newsletter you subscribed to, with a working unsubscribe link and consistent sender.",
503
+ },
504
+ },
505
+ }),
506
+ ({
507
+ id: "msg_demo_06",
508
+ conversationId: "conv_demo_invoice",
509
+ messageId: "<invoice@figma.demo>",
510
+ from: { address: "billing@figma.com", name: "Figma" },
511
+ to: [ilya],
512
+ subject: "Your July invoice",
513
+ preview: "Your invoice is ready. No action is required.",
514
+ body: "Your July invoice is ready. No action is required. You can find billing details in your workspace settings.",
515
+ ageMs: 432_000_000,
516
+ html: figmaInvoiceHtml,
517
+ }),
518
+ ({
519
+ id: "msg_demo_13",
520
+ conversationId: "conv_demo_hiring",
521
+ messageId: "<position-closed@hireline.example>",
522
+ from: { address: "notifications@hireline.example", name: "Hireline" },
523
+ to: [ilya],
524
+ subject: "This position is closed",
525
+ preview: "You applied for Senior Frontend Engineer at Northwind Labs. The employer has closed this position.",
526
+ body: "This position is closed\n\nYou applied for Senior Frontend Engineer at Northwind Labs. The employer has closed this position.\n\nWe picked a few similar roles that match your profile. Good luck with your search!",
527
+ ageMs: 302_400_000,
528
+ html: hirelineHtml,
529
+ checks: {
530
+ mailer: "Hireline Notifier",
531
+ intel: { ptr: "out-12.sendhub-mail.example", asn: 64501, asName: "SendHub Inc.", prefix: "203.0.113.64/26", country: "US" },
532
+ list: { id: "alerts.hireline.example" },
533
+ // Sent through an ESP on its own domain, with no DMARC record for
534
+ // hireline.example: nothing ties the message to the From domain.
535
+ authentication: {
536
+ ip: "203.0.113.87",
537
+ helo: "out-12.sendhub-mail.example",
538
+ mailFrom: "bounces+7731@sendhub-mail.example",
539
+ spf: "pass",
540
+ spfHelo: "pass",
541
+ dkim: [{ domain: "sendhub-mail.example", selector: "s1", result: "pass", signs: ["from", "to", "subject", "date", "list-unsubscribe"] }],
542
+ dmarc: "none",
543
+ policy: null,
544
+ spamScore: 1,
545
+ },
546
+ ai: {
547
+ spam: false,
548
+ spamConfidence: 0.18,
549
+ folderId: null,
550
+ folderConfidence: 0.31,
551
+ reason: "Automated job-board notification about an application you submitted.",
552
+ },
553
+ },
554
+ }),
555
+ ({
556
+ id: "msg_demo_14",
557
+ conversationId: "conv_demo_offsite",
558
+ messageId: "<offsite-venues@northwind.example>",
559
+ from: { address: "priya@northwind.example", name: "Priya Nair" },
560
+ to: [ilya],
561
+ subject: "Venue shortlist for the offsite",
562
+ preview: "I narrowed the offsite venues down to three. All of them can host 40 people and have a quiet room for workshops.",
563
+ body: "Hi Ilya,\n\nI narrowed the offsite venues down to three. All of them can host 40 people and have a quiet room for workshops.\n\nCould you pick your favourite by Thursday? I will book it the same day.\n\nPriya",
564
+ ageMs: 12_600_000,
565
+ checks: {
566
+ mailer: "Mozilla Thunderbird 140.2",
567
+ intel: { ptr: "mail-sor-f69.google.com", asn: 15169, asName: "Google LLC", prefix: "198.51.100.0/24", country: "US" },
568
+ // Sent to Ilya's old address and forwarded by Gmail: SPF now checks
569
+ // Gmail's forwarding envelope, while the original DKIM signature and
570
+ // Gmail's ARC seal survive.
571
+ authentication: {
572
+ ip: "198.51.100.113",
573
+ helo: "mail-sor-f69.google.com",
574
+ mailFrom: "ilya.old+caf_=ilya=demo.example@gmail.com",
575
+ spf: "pass",
576
+ spfHelo: "pass",
577
+ dkim: [{ domain: "northwind.example", selector: "nw2026", result: "pass", signs: ["from", "to", "subject", "date", "message-id"] }],
578
+ dmarc: "pass",
579
+ policy: "quarantine",
580
+ spamScore: 0,
581
+ forwardedVia: {
582
+ sealer: "google.com",
583
+ authservId: "mx.google.com",
584
+ helo: "mail.northwind.example",
585
+ ip: "203.0.113.150",
586
+ },
587
+ },
588
+ ai: {
589
+ spam: false,
590
+ spamConfidence: 0.03,
591
+ folderId: null,
592
+ folderConfidence: 0.1,
593
+ reason: "A colleague asking for a decision on offsite venues, forwarded from another inbox.",
594
+ },
595
+ },
596
+ }),
597
+ ({
598
+ id: "msg_demo_15",
599
+ conversationId: "conv_demo_favour",
600
+ messageId: "<favour-4471@freemail.example>",
601
+ // An outsider borrowing a colleague's name from a freemail account.
602
+ from: { address: "maya.chen.desk@freemail.example", name: "Maya Chen" },
603
+ to: [ilya],
604
+ subject: "Quick favour before my next meeting",
605
+ preview: "Are you at your desk? I need you to handle a small purchase for a client today and I can't take calls.",
606
+ body: "Hi Ilya,\n\nAre you at your desk? I need you to handle a small purchase for a client today and I can't take calls — I'm in back-to-back meetings.\n\nReply here as soon as you see this and I'll send the details.\n\nMaya",
607
+ ageMs: 1_500_000,
608
+ checks: {
609
+ direct: true,
610
+ mailer: "Freemail Webmail 9.4",
611
+ intel: { ptr: "smtp-out-3.freemail.example", asn: 64502, asName: "Freemail Networks B.V.", prefix: "198.51.100.128/25", country: "NL", registry: "ripencc" },
612
+ authentication: {
613
+ ip: "198.51.100.177",
614
+ helo: "smtp-out-3.freemail.example",
615
+ mailFrom: "maya.chen.desk@freemail.example",
616
+ spf: "pass",
617
+ spfHelo: "pass",
618
+ dkim: [{ domain: "freemail.example", selector: "fm1", result: "pass", signs: ["from", "to", "subject", "date", "message-id"] }],
619
+ dmarc: "pass",
620
+ policy: "reject",
621
+ spamScore: 1,
622
+ },
623
+ ai: {
624
+ spam: false,
625
+ spamConfidence: 0.62,
626
+ folderId: null,
627
+ folderConfidence: 0,
628
+ reason: "Urgent favour request from a free webmail account that uses a colleague's name; possible impersonation.",
629
+ },
630
+ },
631
+ }),
632
+ ({
633
+ id: "msg_demo_16",
634
+ conversationId: "conv_demo_password",
635
+ messageId: "<expiry-20931@demo-examp1e.com>",
636
+ from: { address: "it-support@demo-examp1e.com", name: "IT Service Desk" },
637
+ to: [ilya],
638
+ subject: "Action required: your mailbox password expires today",
639
+ preview: "Your password for ilya@demo.example expires in 4 hours. Keep your current password to avoid losing access.",
640
+ body: "Your password for ilya@demo.example expires in 4 hours.\n\nKeep your current password to avoid losing access to mail and files:\nhttps://demo-examp1e.com/account/keep-password?u=ilya\n\nIT Service Desk",
641
+ ageMs: 3_000_000,
642
+ checks: {
643
+ direct: true,
644
+ mailer: "PHPMailer 6.9",
645
+ intel: {
646
+ ptr: "vps-2207.cheap-cloud.example",
647
+ asn: 64503,
648
+ asName: "Cheap Cloud VPS",
649
+ prefix: "203.0.113.192/26",
650
+ country: "RU",
651
+ registry: "ripencc",
652
+ spamhaus: {
653
+ dbl: [{ domain: "demo-examp1e.com", code: "127.0.1.4", list: "DBL phishing domain" }],
654
+ domains: ["demo-examp1e.com"],
655
+ },
656
+ },
657
+ authentication: {
658
+ ip: "203.0.113.207",
659
+ helo: "vps-2207.cheap-cloud.example",
660
+ mailFrom: "it-support@demo-examp1e.com",
661
+ spf: "pass",
662
+ spfHelo: "none",
663
+ dkim: [],
664
+ dmarc: "none",
665
+ policy: null,
666
+ spamScore: 3,
667
+ },
668
+ ai: {
669
+ spam: false,
670
+ spamConfidence: 0.71,
671
+ folderId: null,
672
+ folderConfidence: 0,
673
+ reason: "Password-expiry notice from demo-examp1e.com, a lookalike of the workspace domain, with a credential link.",
674
+ },
675
+ },
676
+ }),
677
+ ({
678
+ id: "msg_demo_17",
679
+ conversationId: "conv_demo_meetup",
680
+ messageId: "<meetup-intro@harborlight.example>",
681
+ from: { address: "elena@harborlight.example", name: "Elena Petrova" },
682
+ to: [ilya],
683
+ subject: "Intro: design systems meetup in November",
684
+ preview: "Priya mentioned you have been rebuilding your mail client. Would you give a 15-minute talk at our meetup?",
685
+ body: "Hi Ilya,\n\nPriya mentioned you have been rebuilding your mail client from scratch. We run a small design systems meetup and would love a 15-minute talk in November.\n\nNo slides needed — a live demo is perfect.\n\nElena",
686
+ ageMs: 6_000_000,
687
+ checks: {
688
+ mailer: "Mozilla Thunderbird 140.2",
689
+ intel: { ptr: "mx-out.harborlight.example", asn: 64504, asName: "Harborlight Studio", prefix: "198.51.100.0/27", country: "PT", registry: "ripencc" },
690
+ authentication: {
691
+ ip: "198.51.100.12",
692
+ helo: "mx-out.harborlight.example",
693
+ mailFrom: "elena@harborlight.example",
694
+ spf: "pass",
695
+ spfHelo: "pass",
696
+ dkim: [{ domain: "harborlight.example", selector: "hl", result: "pass", signs: ["from", "to", "subject", "date", "message-id"] }],
697
+ dmarc: "pass",
698
+ policy: "quarantine",
699
+ spamScore: 0,
700
+ },
701
+ ai: {
702
+ spam: false,
703
+ spamConfidence: 0.04,
704
+ folderId: null,
705
+ folderConfidence: 0.08,
706
+ reason: "A personal speaking invitation that references a mutual colleague.",
707
+ },
708
+ },
709
+ }),
710
+ ({
711
+ id: "msg_demo_18",
712
+ conversationId: "conv_demo_receipt",
713
+ messageId: "<receipt-1031@orbitalcloud.example>",
714
+ from: { address: "billing@orbitalcloud.example", name: "Orbital Cloud" },
715
+ to: [ilya],
716
+ subject: "Your October receipt",
717
+ preview: "Thanks for your payment of $48.00 for the Team plan. Your next invoice is due on November 30.",
718
+ body: "Thanks for your payment of $48.00 for the Team plan.\n\nReceipt OC-1031 · Paid with card ending 4242\nYour next invoice is due on November 30.\n\nOrbital Cloud Billing",
719
+ ageMs: 20_000_000,
720
+ checks: {
721
+ direct: true,
722
+ mailer: "Orbital Billing",
723
+ intel: { ptr: "o12.mail.orbitalcloud.example", asn: 64505, asName: "Orbital Cloud Inc.", prefix: "203.0.113.0/26", country: "US", bimi: true },
724
+ authentication: {
725
+ ip: "203.0.113.12",
726
+ helo: "o12.mail.orbitalcloud.example",
727
+ mailFrom: "bounces@mail.orbitalcloud.example",
728
+ spf: "pass",
729
+ spfHelo: "pass",
730
+ dkim: [{ domain: "orbitalcloud.example", selector: "oc2026", result: "pass", signs: ["from", "to", "subject", "date", "message-id"] }],
731
+ dmarc: "pass",
732
+ policy: "reject",
733
+ spamScore: 0,
734
+ },
735
+ ai: {
736
+ spam: false,
737
+ spamConfidence: 0.02,
738
+ folderId: null,
739
+ folderConfidence: 0.15,
740
+ reason: "Payment receipt from a service the mailbox already pays for, fully authenticated.",
741
+ },
742
+ },
743
+ }),
744
+ ({
745
+ id: "msg_demo_19",
746
+ conversationId: "conv_demo_bank_details",
747
+ messageId: "<inv-2207-update@paydesk-mailer.example>",
748
+ from: { address: "accounts@northwind-supplies.example", name: "Northwind Supplies Accounts" },
749
+ replyTo: { address: "northwind.accounts@quickmail.example", name: "Northwind Supplies Accounts" },
750
+ to: [ilya],
751
+ subject: "Updated bank details for invoice INV-2207",
752
+ preview: "Our bank has changed. Please send the payment for INV-2207 to the new account below by end of day.",
753
+ body: "Hello,\n\nOur bank has changed after an internal audit. Please send the payment for invoice INV-2207 (€18,400.00) to the new account below by end of day, and reply to confirm once it is done.\n\nIBAN: XX00 0000 0000 0000 0000 00\nReference: INV-2207\n\nKind regards,\nAccounts team",
754
+ ageMs: 1_800_000,
755
+ checks: {
756
+ mailer: "PayDesk Mailer 3.1",
757
+ intel: { ptr: "smtp-4.paydesk-mailer.example", asn: 64506, asName: "PayDesk Mailer Ltd", prefix: "198.51.100.128/26", country: "NL", registry: "ripencc" },
758
+ authentication: {
759
+ ip: "198.51.100.140",
760
+ helo: "smtp-4.paydesk-mailer.example",
761
+ mailFrom: "bounce-2207@paydesk-mailer.example",
762
+ spf: "pass",
763
+ spfHelo: "pass",
764
+ dkim: [{ domain: "paydesk-mailer.example", selector: "pd1", result: "pass", signs: ["from", "to", "subject", "date", "reply-to"] }],
765
+ dmarc: "none",
766
+ policy: null,
767
+ spamScore: 2,
768
+ },
769
+ ai: {
770
+ spam: false,
771
+ spamConfidence: 0.58,
772
+ folderId: null,
773
+ folderConfidence: 0,
774
+ reason: "Payment redirection request whose replies go to a free-mail address; possible invoice fraud.",
775
+ },
776
+ },
777
+ }),
778
+ ];
779
+ }
780
+
781
+ if (mailboxId === "mbx_demo_support") {
782
+ return [
783
+ ({ id: "msg_demo_08", conversationId: "conv_demo_customer1", messageId: "<help1@customer.demo>", from: { address: "noah@northstar.studio", name: "Noah Williams" }, to: [support], subject: "Can we use our own sending domain?", preview: "We are evaluating the product for our studio and need mail to come from our existing domain.", body: "Hi team,\n\nWe are evaluating the product for our studio and need mail to come from our existing domain. Is that supported, and which DNS records do we need to add?\n\nThanks,\nNoah", ageMs: 900_000 }),
784
+ ({ id: "msg_demo_09", conversationId: "conv_demo_customer2", messageId: "<help2@customer.demo>", from: { address: "amina@harbor.so", name: "Amina Okafor" }, to: [support], subject: "Importing an existing mailbox", preview: "Is there a recommended path for importing a few years of historical email?", body: "Hello,\n\nIs there a recommended path for importing a few years of historical email? We have roughly 18 GB and would like to preserve threads if possible.\n\nAmina", ageMs: 7_200_000 }),
785
+ ({ id: "msg_demo_10", conversationId: "conv_demo_customer3", messageId: "<help3@customer.demo>", from: { address: "lea@atelier.fr", name: "Léa Martin" }, to: [support], subject: "Shared inbox notifications", preview: "Could each person choose which shared mailboxes should send browser notifications?", body: "Bonjour,\n\nCould each person choose which shared mailboxes should send browser notifications? We have support and press addresses, but not everyone needs both.\n\nMerci,\nLéa", ageMs: 97_200_000 }),
786
+ ({ id: "msg_demo_11", conversationId: "conv_demo_customer3", messageId: "<support-reply@demo>", direction: "outgoing", from: support, to: [{ address: "lea@atelier.fr", name: "Léa Martin" }], subject: "Re: Shared inbox notifications", preview: "Yes — notification preferences are per person and per mailbox.", body: "Hi Léa,\n\nYes — notification preferences are per person and per mailbox. You can stay in the shared inbox without receiving every browser notification.\n\nBest,\nMaya", ageMs: 90_000_000, references: ["<help3@customer.demo>"] }),
787
+ ({ id: "msg_demo_12", conversationId: "conv_demo_spam", messageId: "<promo@spam.demo>", from: { address: "offers@growth-hack.invalid", name: "Growth Network" }, to: [support], subject: "10x your pipeline overnight", preview: "One weird trick your competitors do not want you to know.", body: "One weird trick your competitors do not want you to know.", ageMs: 345_600_000,
788
+ checks: {
789
+ direct: true,
790
+ intel: {
791
+ ptr: "77-2-0-192.dyn.broadband-pool.example",
792
+ fcrdns: false,
793
+ generic: true,
794
+ asn: 64511,
795
+ asName: "Bulk Hosting Ltd",
796
+ prefix: "192.0.2.0/24",
797
+ country: "SC",
798
+ registry: "afrinic",
799
+ spamhaus: {
800
+ zen: [{ code: "127.0.0.4", list: "XBL (compromised host)", severity: "abuse" }],
801
+ dbl: [{ domain: "growth-hack.invalid", code: "127.0.1.2", list: "DBL spam domain" }],
802
+ domains: ["growth-hack.invalid"],
803
+ },
804
+ },
805
+ authentication: {
806
+ ip: "192.0.2.77",
807
+ helo: "bulk-mailer",
808
+ mailFrom: "offers@growth-hack.invalid",
809
+ spf: "softfail",
810
+ spfHelo: "none",
811
+ dkim: [{ domain: "bulk-sender.example", selector: "default", result: "pass", signs: ["from", "subject", "date"] }],
812
+ dmarc: "fail",
813
+ policy: "none",
814
+ spamScore: 5,
815
+ },
816
+ ai: {
817
+ spam: true,
818
+ spamConfidence: 0.97,
819
+ folderId: null,
820
+ folderConfidence: 0,
821
+ reason: "Unsolicited bulk promotion with exaggerated claims from a sender that fails SPF and DMARC.",
822
+ },
823
+ } }),
824
+ ({ id: "msg_demo_20", conversationId: "conv_demo_quota", messageId: "<quota-77@mail-quota-alerts.example>", from: { address: "admin@mail-quota-alerts.example", name: "Mailbox Administrator" }, to: [support], subject: "Your mailbox storage is full", preview: "Incoming messages are being held. Verify your account within 24 hours to release them.", body: "Your mailbox storage is full.\n\n14 incoming messages are being held. Verify your account within 24 hours to release them:\nhttps://mail-quota-alerts.example/verify?box=support\n\nMailbox Administrator", ageMs: 5_400_000,
825
+ checks: {
826
+ direct: true,
827
+ mailer: "PHPMailer 6.9",
828
+ intel: { ptr: null, asn: 64509, asName: "Anon VPS Hosting", prefix: "192.0.2.128/25", country: "PA", registry: "lacnic" },
829
+ authentication: {
830
+ ip: "192.0.2.201",
831
+ helo: "localhost",
832
+ mailFrom: "admin@mail-quota-alerts.example",
833
+ spf: "none",
834
+ spfHelo: "none",
835
+ dkim: [],
836
+ dmarc: "none",
837
+ policy: null,
838
+ spamScore: 4,
839
+ },
840
+ ai: {
841
+ spam: true,
842
+ spamConfidence: 0.91,
843
+ folderId: null,
844
+ folderConfidence: 0,
845
+ reason: "Credential-harvesting storage warning from an unauthenticated sender with a verification link.",
846
+ },
847
+ } }),
848
+ ];
849
+ }
850
+
851
+ return [];
852
+ }
853
+
854
+ /** A tiny self-made logo for the fictional orbitalcloud.example (SVG Tiny PS). */
855
+ const orbitalCloudLogo = '<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny-ps" viewBox="0 0 64 64"><title>Orbital Cloud</title><rect width="64" height="64" fill="#1e3a8a"/><circle cx="32" cy="32" r="11" fill="#f8fafc"/><ellipse cx="32" cy="32" rx="23" ry="8" fill="none" stroke="#7dd3fc" stroke-width="3" transform="rotate(-24 32 32)"/><circle cx="51" cy="24" r="3.5" fill="#fbbf24"/></svg>';
856
+
857
+ /**
858
+ * Earlier correspondence the demo implies but does not seed as messages, so
859
+ * known contacts do not all read as first-time senders.
860
+ */
861
+ export function demoMailboxSenders(mailboxId: string, now = Date.now()) {
862
+ const known = (address: string, messageCount: number, outboundCount: number, firstSeenDaysAgo: number) => ({
863
+ address,
864
+ domain: address.split("@")[1]!,
865
+ firstSeenAt: new Date(now - firstSeenDaysAgo * 86_400_000),
866
+ lastSeenAt: new Date(now - 7 * 86_400_000),
867
+ messageCount,
868
+ outboundCount,
869
+ lastOutboundAt: outboundCount ? new Date(now - 10 * 86_400_000) : null,
870
+ });
871
+ if (mailboxId === "mbx_demo_personal") {
872
+ return [
873
+ known("jessica@cloudflare.com", 6, 4, 210),
874
+ known("rauno@vercel.com", 9, 7, 400),
875
+ known("digest@dense-discovery.com", 40, 0, 300),
876
+ known("billing@figma.com", 12, 0, 365),
877
+ known("notifications@hireline.example", 5, 0, 60),
878
+ known("priya@northwind.example", 14, 11, 500),
879
+ known("billing@orbitalcloud.example", 9, 0, 280),
880
+ known("accounts@northwind-supplies.example", 4, 2, 150),
881
+ // Blocked before the demo starts: future mail from the domain goes to Spam.
882
+ { address: "@prize-center.example", domain: "prize-center.example", blockedAt: new Date(now - 20 * 86_400_000) },
883
+ ];
884
+ }
885
+ if (mailboxId === "mbx_demo_support") {
886
+ return [known("amina@harbor.so", 2, 1, 30)];
887
+ }
888
+ return [];
889
+ }
890
+
891
+ export function demoMailboxEmails(mailboxId: string, now = Date.now()): NewEmail[] {
892
+ return demoInputs(mailboxId).map((input) => email(input, now));
893
+ }
894
+
895
+ /** HTML bodies and originals to store in R2 before seeding, keyed as the rows reference them. */
896
+ export function demoMailboxObjects(mailboxId: string, now = Date.now()) {
897
+ return demoInputs(mailboxId).flatMap((input) => [
898
+ ...(input.html
899
+ ? [{ key: demoHtmlKey(input.id), body: input.html, contentType: "text/html; charset=utf-8" }]
900
+ : []),
901
+ ...(input.checks
902
+ ? [{ key: demoRawKey(input.id), body: demoRawMime(input, now), contentType: "message/rfc822" }]
903
+ : []),
904
+ ...(input.checks?.intel?.bimi
905
+ ? [{
906
+ key: bimiObjectKey(input.from.address.split("@")[1]!),
907
+ body: orbitalCloudLogo,
908
+ contentType: "image/svg+xml",
909
+ customMetadata: { status: "ok", checkedAt: String(now), reason: "" },
910
+ }]
911
+ : []),
912
+ ]);
913
+ }
914
+
915
+ export function demoMailboxFolders(mailboxId: string): NewFolder[] {
916
+ if (mailboxId !== "mbx_demo_personal") return [];
917
+ return [
918
+ {
919
+ id: "Product",
920
+ name: "Product",
921
+ sortOrder: 100,
922
+ },
923
+ {
924
+ id: "Pitch Decks",
925
+ name: "Pitch Decks",
926
+ sortOrder: 110,
927
+ },
928
+ ];
929
+ }
930
+
931
+ export function demoMailboxConversationState(mailboxId: string) {
932
+ // Conversations the security actions move are reset on every demo login.
933
+ const inbox = { mailboxState: "active" as const, folderId: null };
934
+ if (mailboxId === "mbx_demo_personal") {
935
+ return [
936
+ { id: "conv_demo_weekly", ...inbox },
937
+ { id: "conv_demo_password", ...inbox },
938
+ { id: "conv_demo_bank_details", ...inbox },
939
+ { id: "conv_demo_hiring", ...inbox },
940
+ { id: "conv_demo_receipt", ...inbox },
941
+ { id: "conv_demo_maya", folderId: "Product" },
942
+ { id: "conv_demo_vercel", folderId: "Pitch Decks" },
943
+ { id: "conv_demo_invoice", mailboxState: "archive" as const },
944
+ ];
945
+ }
946
+ if (mailboxId === "mbx_demo_support") {
947
+ return [
948
+ { id: "conv_demo_spam", mailboxState: "spam" as const },
949
+ { id: "conv_demo_quota", mailboxState: "spam" as const },
950
+ ];
951
+ }
952
+ return [];
953
+ }
954
+
955
+ /** One filter so the Rules tab has something to show; it applies to new mail only. */
956
+ export function demoMailboxRules(mailboxId: string): Array<MailRuleInput & { id: string }> {
957
+ if (mailboxId !== "mbx_demo_personal") return [];
958
+ return [{
959
+ id: "rul_demo_hireline",
960
+ name: "Job alerts",
961
+ enabled: true,
962
+ conditions: { from: "@hireline.example" },
963
+ actions: { archive: true, markRead: true },
964
+ }];
965
+ }
966
+
967
+ /** Message ids whose report labels a demo login clears. */
968
+ export function demoMailboxEmailIds(mailboxId: string) {
969
+ return demoInputs(mailboxId).map((input) => input.id);
970
+ }
971
+
972
+ /** A phishing report waiting in Administration → Reported messages. */
973
+ export const demoReportedMessage = {
974
+ id: "rpt_demo_quota",
975
+ mailboxId: "mbx_demo_support",
976
+ emailId: "msg_demo_20",
977
+ reportLabel: "phishing" as const,
978
+ };