@volter/twin-gmail 0.0.0-stage → 1.0.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 (62) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -2
  3. package/dist/generated/pack-facts.json +47 -0
  4. package/dist/journeys/customer-life.json +1013 -0
  5. package/dist/journeys/decisions.json +77 -0
  6. package/dist/journeys/demand.json +43 -0
  7. package/dist/journeys/first-use.json +24 -0
  8. package/dist/journeys/vendor-examples.json +7 -0
  9. package/dist/spec/SOURCE.md +13 -0
  10. package/dist/spec/rfc5321.txt +5323 -0
  11. package/dist/spec/sources.json +25 -0
  12. package/dist/src/cli.d.ts +2 -0
  13. package/dist/src/cli.js +19 -0
  14. package/dist/src/engine/mail.d.ts +19 -0
  15. package/dist/src/engine/mail.js +89 -0
  16. package/dist/src/fetch.d.ts +2 -0
  17. package/dist/src/fetch.js +18 -0
  18. package/dist/src/generated/surface.gen.json +1 -0
  19. package/dist/src/index.d.ts +6 -0
  20. package/dist/src/index.js +11 -0
  21. package/dist/src/manifest.d.ts +2 -0
  22. package/dist/src/manifest.js +68 -0
  23. package/dist/src/screens/home.d.ts +2 -0
  24. package/dist/src/screens/home.js +3 -0
  25. package/dist/src/screens/mail.d.ts +2 -0
  26. package/dist/src/screens/mail.js +142 -0
  27. package/dist/src/semantics/around.d.ts +2 -0
  28. package/dist/src/semantics/around.js +97 -0
  29. package/dist/src/semantics/index.d.ts +2 -0
  30. package/dist/src/semantics/index.js +1 -0
  31. package/dist/src/semantics/shared.d.ts +52 -0
  32. package/dist/src/semantics/shared.js +97 -0
  33. package/dist/src/semantics/states.d.ts +4 -0
  34. package/dist/src/semantics/states.js +33 -0
  35. package/dist/src/server.d.ts +9 -0
  36. package/dist/src/server.js +23 -0
  37. package/dist/src/stream.d.ts +2 -0
  38. package/dist/src/stream.js +10 -0
  39. package/generated/pack-facts.json +47 -0
  40. package/journeys/customer-life.json +1013 -0
  41. package/journeys/decisions.json +77 -0
  42. package/journeys/demand.json +43 -0
  43. package/journeys/first-use.json +24 -0
  44. package/journeys/vendor-examples.json +7 -0
  45. package/package.json +52 -4
  46. package/spec/SOURCE.md +13 -0
  47. package/spec/rfc5321.txt +5323 -0
  48. package/spec/sources.json +25 -0
  49. package/src/cli.ts +20 -0
  50. package/src/engine/mail.ts +76 -0
  51. package/src/fetch.ts +19 -0
  52. package/src/generated/surface.gen.json +1 -0
  53. package/src/index.ts +13 -0
  54. package/src/manifest.ts +86 -0
  55. package/src/screens/home.ts +6 -0
  56. package/src/screens/mail.ts +128 -0
  57. package/src/semantics/around.ts +91 -0
  58. package/src/semantics/index.ts +6 -0
  59. package/src/semantics/shared.ts +111 -0
  60. package/src/semantics/states.ts +41 -0
  61. package/src/server.ts +19 -0
  62. package/src/stream.ts +11 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes": {
3
+ "status": 200,
4
+ "checked": "2026-11-11",
5
+ "quotes": {
6
+ "gsmtp (Google SMTP) is added to all errors": true,
7
+ "The email account that you tried to reach does not exist.": true,
8
+ "Your message exceeded Google's message size limits.": true
9
+ }
10
+ },
11
+ "https://www.rfc-editor.org/rfc/rfc2047": {
12
+ "status": 200,
13
+ "checked": "2026-10-03",
14
+ "quotes": {
15
+ "encoded-word = \"=?\" charset \"?\" encoding \"?\" encoded-text \"?=\"": true
16
+ }
17
+ },
18
+ "https://www.rfc-editor.org/rfc/rfc5322": {
19
+ "status": 200,
20
+ "checked": "2026-10-03",
21
+ "quotes": {
22
+ "Unfolding is accomplished by simply removing any CRLF that is immediately followed by WSP": true
23
+ }
24
+ }
25
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // world-gmail: serve the gmail twin (its webmail, and its receiving SMTP server on the side port the World allocates).
3
+ import { hasFlag, optionValue } from '@volter/world-core/args';
4
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
5
+ import { createGmailTwinServer } from './server.ts';
6
+
7
+ const [cmd, ...rest] = process.argv.slice(2);
8
+ const port = Number(optionValue(rest, '--port', '0')) || undefined;
9
+ const smtpPort = Number(optionValue(rest, '--smtp-port', '0')) || undefined;
10
+ const root = optionValue(rest, '--root') || undefined;
11
+ const readOnly = hasFlag(rest, '--read-only');
12
+
13
+ if (cmd === 'serve' || cmd === undefined) {
14
+ const s = await createGmailTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(smtpPort ? { smtpPort } : {}) });
15
+ process.stdout.write(`gmail twin${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
16
+ process.stdout.write(`gmail twin receiving mail on smtp://127.0.0.1:${s.smtpPort}\n`);
17
+ await keepProcessAlive();
18
+ } else {
19
+ process.stdout.write('Usage: world-gmail serve [--port N] [--smtp-port N] [--root DIR] [--read-only]\n');
20
+ }
@@ -0,0 +1,76 @@
1
+ // A message as RFC 5322 and MIME make of it (its headers, its text and HTML, its attachments), from the bytes Gmail's
2
+ // receiving server accepted: pure, so the server's front keeps it in each recipient's mailbox (semantics/shared.ts
3
+ // `deliver`). The data arrives with its dot-stuffing undone (../stream.ts); the World keeps the bytes as sent.
4
+ type Part = { headers: Record<string, string>; body: string };
5
+ type Attachment = { filename: string | null; contentType: string; size: number; contentId: string | null; base64: string };
6
+ type Parsed = { headers: Record<string, string>; subject: string; from: string; to: string[]; cc: string[]; text: string | null; html: string | null; attachments: Attachment[] };
7
+
8
+ /** Headers and body split at the first empty line; folded headers unfolded, names lowercased (RFC 5322 §2.2.3). */
9
+ // source: https://www.rfc-editor.org/rfc/rfc5322 "Unfolding is accomplished by simply removing any CRLF that is immediately followed by WSP"
10
+ function split(raw: string): Part {
11
+ const at = raw.search(/\r?\n\r?\n/);
12
+ const head = at < 0 ? raw : raw.slice(0, at);
13
+ const body = at < 0 ? '' : raw.slice(at).replace(/^\r?\n\r?\n/, '');
14
+ const headers: Record<string, string> = {};
15
+ for (const line of head.replace(/\r?\n[ \t]+/g, ' ').split(/\r?\n/)) {
16
+ const i = line.indexOf(':');
17
+ if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] ??= utf8(bytesOf(line.slice(i + 1).trim()));
18
+ }
19
+ return { headers, body };
20
+ }
21
+
22
+ const param = (value: string, name: string): string | undefined => {
23
+ const m = new RegExp(`;\\s*${name}\\*?=\\s*(?:"([^"]*)"|([^;\\s]+))`, 'i').exec(value);
24
+ const v = m?.[1] ?? m?.[2];
25
+ return v?.startsWith("utf-8''") || v?.startsWith("UTF-8''") ? decodeURIComponent(v.slice(7)) : v;
26
+ };
27
+
28
+ const bytesOf = (s: string): Uint8Array => Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff);
29
+ const utf8 = (b: Uint8Array): string => new TextDecoder().decode(b);
30
+ const textFor = (b: Uint8Array, charset: string | undefined): string => { try { return new TextDecoder(charset ?? 'utf-8').decode(b); } catch { return utf8(b); } };
31
+
32
+ /** A part's body decoded by its Content-Transfer-Encoding: base64 and quoted-printable to bytes (RFC 2045 §6). */
33
+ function decoded(part: Part): Uint8Array {
34
+ const enc = (part.headers['content-transfer-encoding'] ?? '7bit').toLowerCase();
35
+ if (enc === 'base64') { try { return bytesOf(atob(part.body.replace(/\s+/g, ''))); } catch { return bytesOf(part.body); } }
36
+ if (enc === 'quoted-printable') return bytesOf(part.body.replace(/=\r?\n/g, '').replace(/=([0-9A-Fa-f]{2})/g, (_, h: string) => String.fromCharCode(Number.parseInt(h, 16))).split('').map((c) => (c.charCodeAt(0) > 0xff ? '?' : c)).join(''));
37
+ return bytesOf(part.body);
38
+ }
39
+
40
+ /** Encoded words in a header (`=?UTF-8?B?…?=`, `=?UTF-8?Q?…?=`), decoded (RFC 2047). */
41
+ // source: https://www.rfc-editor.org/rfc/rfc2047 "encoded-word = \"=?\" charset \"?\" encoding \"?\" encoded-text \"?=\""
42
+ function words(value: string): string {
43
+ return value.replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=(\s+(?==\?))?/g, (_, _cs: string, e: string, t: string) => {
44
+ try { return textFor(e.toUpperCase() === 'B' ? bytesOf(atob(t)) : bytesOf(t.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_m, h: string) => String.fromCharCode(Number.parseInt(h, 16)))), _cs); } catch { return t; }
45
+ });
46
+ }
47
+
48
+ const addresses = (v: string | undefined): string[] => (v ? words(v).split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/).map((a) => (/<([^>]+)>/.exec(a)?.[1] ?? a).trim()).filter(Boolean) : []);
49
+
50
+ /** The message's parts: a multipart's each (recursively), else the one. */
51
+ function leaves(part: Part): Part[] {
52
+ const type = part.headers['content-type'] ?? 'text/plain';
53
+ const boundary = /^multipart\//i.test(type) ? param(type, 'boundary') : undefined;
54
+ if (!boundary) return [part];
55
+ const pieces = part.body.split(new RegExp(`\\r?\\n?--${boundary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:--)?[ \\t]*\\r?\\n?`));
56
+ return pieces.slice(1, -1).filter((p) => p.trim()).flatMap((p) => leaves(split(p)));
57
+ }
58
+
59
+ export function parse(data: string): Parsed {
60
+ const top = split(data);
61
+ let text: string | null = null;
62
+ let html: string | null = null;
63
+ const attachments: Attachment[] = [];
64
+ for (const leaf of leaves(top)) {
65
+ const type = (leaf.headers['content-type'] ?? 'text/plain').split(';')[0]!.trim().toLowerCase();
66
+ const disposition = leaf.headers['content-disposition'] ?? '';
67
+ const filename = param(disposition, 'filename') ?? param(leaf.headers['content-type'] ?? '', 'name');
68
+ const bytes = decoded(leaf);
69
+ if (!/^attachment/i.test(disposition) && !filename && type === 'text/plain' && text === null) { text = textFor(bytes, param(leaf.headers['content-type'] ?? '', 'charset')); continue; }
70
+ if (!/^attachment/i.test(disposition) && !filename && type === 'text/html' && html === null) { html = textFor(bytes, param(leaf.headers['content-type'] ?? '', 'charset')); continue; }
71
+ let bin = '';
72
+ for (const b of bytes) bin += String.fromCharCode(b);
73
+ attachments.push({ filename: filename ? words(filename) : null, contentType: type, size: bytes.length, contentId: leaf.headers['content-id']?.replace(/^<|>$/g, '') ?? null, base64: btoa(bin) });
74
+ }
75
+ return { headers: top.headers, subject: words(top.headers.subject ?? ''), from: addresses(top.headers.from)[0] ?? '', to: addresses(top.headers.to), cc: addresses(top.headers.cc), text, html, attachments };
76
+ }
package/src/fetch.ts ADDED
@@ -0,0 +1,19 @@
1
+ // GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
2
+ // the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
3
+ import { createPackFetch, type DerivedFetch, type DerivedSurface, type PackFetchOptions } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from './manifest.ts';
6
+ import { around } from './semantics/around.ts';
7
+ import { gmailSemantics } from './semantics/index.ts';
8
+ import { screen as screen_home } from './screens/home.ts';
9
+ import { screen as screen_mail } from './screens/mail.ts';
10
+
11
+ export function createGmailFetch(options: PackFetchOptions = {}): DerivedFetch {
12
+ return createPackFetch({
13
+ manifest,
14
+ surface: surface as unknown as DerivedSurface,
15
+ handlers: gmailSemantics,
16
+ around,
17
+ screens: { "home": screen_home, "mail": screen_mail },
18
+ }, options);
19
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"rfc5321.txt","format":"command-reply","version":"RFC 5321","anyCommand":[500,501,421],"commands":[{"id":"CONNECTION ESTABLISHMENT","intermediate":[],"success":[220],"error":[554]},{"id":"EHLO","intermediate":[],"success":[250],"error":[504,550,502]},{"id":"HELO","intermediate":[],"success":[250],"error":[504,550,502]},{"id":"MAIL","intermediate":[],"success":[250],"error":[552,451,452,550,553,503,455,555]},{"id":"RCPT","intermediate":[],"success":[250,251],"error":[550,551,552,553,450,451,452,503,455,555]},{"id":"DATA","intermediate":[354],"success":[],"error":[503,554],"then":{"intermediate":[],"success":[250],"error":[552,554,451,452,450,550]}},{"id":"RSET","intermediate":[],"success":[250],"error":[]},{"id":"VRFY","intermediate":[],"success":[250,251,252],"error":[550,551,553,502,504]},{"id":"EXPN","intermediate":[],"success":[250,252],"error":[550,500,502,504]},{"id":"HELP","intermediate":[],"success":[211,214],"error":[502,504]},{"id":"NOOP","intermediate":[],"success":[250],"error":[]},{"id":"QUIT","intermediate":[],"success":[221],"error":[]}]}
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // @volter/twin-gmail — the gmail twin (Protocol 3). A fixed file (create-pack): the descriptor is the manifest's, and the
2
+ // kernel derives the vendor-backed half from the generated surface, unless the manifest declares none (vendorBacked.none;
3
+ // architecture, "The real-system adapters").
4
+ import { packOf, registerPack, type DerivedSurface } from '@volter/world-core';
5
+ import { manifest } from './manifest.ts';
6
+ import surface from './generated/surface.gen.json' with { type: 'json' };
7
+
8
+ export { createGmailFetch } from './fetch.ts';
9
+ export { createGmailTwinServer } from './server.ts';
10
+ export { createGmailTwinStream } from './stream.ts';
11
+ export { manifest };
12
+
13
+ export const pack = registerPack(packOf(manifest, surface as unknown as DerivedSurface));
@@ -0,0 +1,86 @@
1
+ // Gmail's manifest (ADR 0032 §4): the mail half of a Google account. A message sent to an address at gmail.com reaches
2
+ // Gmail's receiving server (gmail.com's MX) over SMTP, which accepts it for an account Google holds and refuses it for
3
+ // any other; the person reads it in Gmail's webmail at mail.google.com, signed in to Google.
4
+ //
5
+ // THE SURFACE is generated (./generated/surface.gen.json): RFC 5321's command-reply table, the receiving server's, served
6
+ // on the pack's SMTP side port. The front (./semantics/around.ts) runs each command a connection's byte-stream door
7
+ // (../stream.ts) hands it. The Gmail API, IMAP and submission are later cards (ADR 0032, Staging).
8
+ //
9
+ // THE STATE is each mailbox's messages (`message`: its mailbox, its envelope, its bytes and what they parse to). The
10
+ // mailboxes are Google's accounts: googleoauth owns them and Google's sign-in, and this pack reads them through the
11
+ // kernel's cross-pack read (`ownerReads`, architecture A3), never writes them: a person who signed up (`_person`, keyed
12
+ // `person:<email>`, with the account's `sub`), the account (`account` <sub>) and the browser's Google session
13
+ // (`_web_session`, keyed `websession:<the SID cookie>`).
14
+ //
15
+ // THE ADDRESSING is a static vendor fact: the descriptor's host rules name gmail.com, and its side port's scheme is
16
+ // `smtp`, so the kernel's `ctx.mail` hands a gmail.com recipient's mail here (world-core smtp.ts, ADR 0032 §3).
17
+ import type { DerivedManifest } from '@volter/world-core';
18
+ import { states } from './semantics/states.ts';
19
+
20
+ export const manifest: DerivedManifest = {
21
+ vendor: 'gmail',
22
+ service: 'gmail',
23
+ discovery: {
24
+ twinOf: "Gmail: its receiving SMTP server (gmail.com's MX) and its webmail at mail.google.com",
25
+ identity: "The SMTP side port takes mail from any sender, as an MX does, for gmail.com accounts Google holds. Webmail reads the person's Google session (googleoauth's SID cookie).",
26
+ stores: "each Google account's mailbox: the messages it received, with their envelope, bytes, headers, bodies and attachments",
27
+ },
28
+ body: { json: 'always' },
29
+ ids: { template: '{letters:16}' },
30
+ time: 'iso',
31
+ // the screens' and the wire's refusals; the wire answers in SMTP replies (./semantics/shared.ts)
32
+ error: { error: '{message}' },
33
+ readOnly: { status: 403, message: 'the twin is read-only' },
34
+ notFound: { status: 404, message: 'Not Found' },
35
+ list: { style: 'envelope', envelope: { messages: '{data}' }, limit: { param: 'limit', default: 50, max: 500 } },
36
+ deleted: {},
37
+ vendorBacked: { none: 'Mail reaches Gmail over SMTP from a sending twin inside the World; there is no derived HTTP perform or vendor resource refresh (the Gmail API is a later card).' },
38
+ unmodeled: ['EXPN', 'HELP'],
39
+ // who a message's records name (ADR 0036, "Replicating an issue"): its mailbox, the Google account it was kept for,
40
+ // and its envelope's addresses; the recipient as Gmail delivers it, where `local+tag@gmail.com` is the mailbox
41
+ // `local@gmail.com` labelled (shared.ts accountOf), so a pseudonymized pull keeps the tag on the mailbox's pseudonym (`email+`)
42
+ privacy: {
43
+ identifiers: {
44
+ message: { mailbox: 'email', 'envelope.to': 'email+', 'envelope.from': 'email', receivedAt: 'time' },
45
+ },
46
+ },
47
+ ownerReads: [
48
+ { owner: 'googleoauth', resource: '_person' },
49
+ { owner: 'googleoauth', resource: 'account' },
50
+ { owner: 'googleoauth', resource: '_web_session' },
51
+ ],
52
+ resources: {
53
+ Session: { storedAs: '_session', refresh: { none: 'Connection-local state, not a vendor resource.' }, idPrefix: 'session', ...states.Session },
54
+ message: { refresh: { none: 'The Gmail API, which would read a mailbox back, is a later card.' }, idPrefix: '', ids: '{letters:16}' },
55
+ },
56
+ screens: [{
57
+ // Gmail's webmail (screens/mail.ts): the signed-in person's inbox, a message with its links, and its attachments
58
+ id: 'mail', kind: 'workspace', host: 'mail.google.com', path: '/mail',
59
+ demand: "a person reads the mail an application sent them (Rallly's invitation with its invite.ics, Dub's links) and follows its link", status: 'done',
60
+ controls: ['Inbox', 'Download'], source: 'https://support.google.com/mail/answer/8494',
61
+ }, {
62
+ // gmail.com itself, which sends a visitor to the webmail
63
+ id: 'home', kind: 'flow', host: 'gmail.com', path: '/',
64
+ demand: 'a person who opens gmail.com reaches their inbox', status: 'done',
65
+ source: 'https://support.google.com/mail/answer/8494',
66
+ }],
67
+ descriptor: {
68
+ protocol: '3',
69
+ transport: 'rest',
70
+ archetype: 'crud',
71
+ bin: 'world-gmail',
72
+ resources: ['message'],
73
+ specSource: "spec/rfc5321.txt (RFC 5321): the command-reply table of Gmail's receiving server (spec/SOURCE.md)",
74
+ description: "Gmail twin — gmail.com's receiving SMTP server, which keeps mail for the World's Google accounts and refuses other addresses, and the webmail at mail.google.com where the signed-in person reads it and downloads its attachments.",
75
+ adoption: { sdks: [], envStems: [] },
76
+ // gmail.com: the domain whose mail this server receives (the kernel's ctx.mail routes by it), and its site; the
77
+ // webmail's host
78
+ hosts: [{ host: 'mail.google.com' }, { host: 'gmail.com' }],
79
+ endpointEnvNone: 'no application reads Gmail by a base URL: mail reaches it from the World\'s senders (ctx.mail), and a person opens its webmail',
80
+ // a gmail.com address is a Google account, made at Google's own sign-up (googleoauth's accounts.google.com/signup,
81
+ // which leaves the person signed in): the door the World's generated seed calls for each declared person whose
82
+ // address this pack's mail domain is (ADR 0033 §3)
83
+ seed: { signUp: 'https://accounts.google.com/signup' },
84
+ sidePorts: [{ name: 'smtp', option: 'smtpPort', flag: '--smtp-port', scheme: 'smtp', note: "Gmail's receiving SMTP server (gmail-smtp-in.l.google.com), without TLS: mail for gmail.com's accounts, handed on by the World's senders through ctx.mail" }],
85
+ },
86
+ };
@@ -0,0 +1,6 @@
1
+ // gmail.com: the site sends its visitor to the webmail, which signs them in first when it must (./mail.ts).
2
+ import type { HandlerContext } from '@volter/world-core';
3
+
4
+ export async function screen(_ctx: HandlerContext): Promise<Response> {
5
+ return new Response(null, { status: 301, headers: { location: 'https://mail.google.com/mail/' } });
6
+ }
@@ -0,0 +1,128 @@
1
+ // Gmail's webmail (mail.google.com/mail): the signed-in person's inbox, one message with its links, and its attachments,
2
+ // under the person's Google session (googleoauth's SID cookie, read through the kernel's cross-pack read: ADR 0032 §2).
3
+ // A browser with no Google session is sent to Google's sign-in, which returns it here.
4
+ //
5
+ // GET /mail, /mail/u/0/ the inbox, newest first
6
+ // GET /mail/u/0/inbox/<id> one message: its sender, recipients, subject and body, links followable
7
+ // GET /mail/u/0/inbox/<id>/attachments/<index> one attachment's bytes, downloaded
8
+ //
9
+ // Where the documentation stops: Gmail draws its pages in the browser and names a message in the page's fragment
10
+ // (`#inbox/<id>`); the twin renders each on the server at a path of its own, so a person (or a walk) opens and clicks it
11
+ // as a page. A message's HTML is sanitized on the server by the kernel's HTML grammar and shown inline in the message
12
+ // body, as Gmail shows it, its links opening a new tab; the body is the containing boundary of everything it draws.
13
+ // Composing, replying, labels, search and the other folders are not built.
14
+ import { html, type HandlerContext } from '@volter/world-core';
15
+ import { MESSAGE, type Row, signedInAccount } from '../semantics/shared.ts';
16
+
17
+ const esc = (v: unknown): string => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!);
18
+
19
+ type HtmlNode = Awaited<ReturnType<typeof html.parseFragment>>[number];
20
+ type HtmlElement = Extract<HtmlNode, { type: 'element' }>;
21
+
22
+ // What Gmail keeps of a received message (ADR 0032 §4): these elements go with their content; these are kept; any other
23
+ // element is unwrapped, its children kept. Only these attributes stay, so no received class or id poses as the
24
+ // webmail's own element.
25
+ const DROPPED = new Set(['script', 'style', 'link', 'meta', 'base', 'title', 'head', 'iframe', 'frame', 'frameset', 'object', 'embed', 'applet', 'form', 'input', 'button', 'select', 'textarea', 'option', 'template', 'noscript', 'svg', 'math', 'video', 'audio', 'source', 'track', 'canvas']);
26
+ const KEPT = new Set(['a', 'abbr', 'address', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'wbr']);
27
+ const ATTRIBUTES = new Set(['href', 'src', 'alt', 'title', 'width', 'height', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'bgcolor', 'color', 'face', 'size', 'dir', 'lang', 'style']);
28
+
29
+ /** A style attribute as Gmail keeps it (ADR 0032 §4): without a remote load, a script or positioning, for an ordinary
30
+ * spelling. This is fidelity with Gmail, not the boundary: the message body contains whatever a style draws (BODY) and
31
+ * the page's CSP refuses every load, so an escaped spelling this misses stays inside the body and loads nothing. */
32
+ const style = (value: string): string => value.split(';').map((declaration) => declaration.trim()).filter((declaration) => {
33
+ if (!declaration) return false;
34
+ const lower = declaration.toLowerCase();
35
+ if (lower.includes('url(') || lower.includes('expression(') || lower.includes('@import')) return false;
36
+ const property = lower.slice(0, lower.indexOf(':') === -1 ? lower.length : lower.indexOf(':')).trim();
37
+ return property !== 'position' && property !== 'behavior' && property !== '-moz-binding';
38
+ }).join('; ');
39
+
40
+ // The message body is the containing boundary of what the message draws: paint containment makes it the containing
41
+ // block of every positioned descendant, `position: fixed` included, and clips what they paint to it (CSS Containment
42
+ // Level 2 §3.3, https://www.w3.org/TR/css-contain-2/#containment-paint); its own stacking context keeps the message's
43
+ // z-index inside it.
44
+ const BODY = 'contain:paint;overflow:hidden;isolation:isolate;position:relative';
45
+
46
+ const scheme = (value: string): string => { try { return new URL(value).protocol; } catch { return ''; } };
47
+
48
+ function keep(nodes: HtmlNode[]): HtmlNode[] {
49
+ const out: HtmlNode[] = [];
50
+ for (const node of nodes) {
51
+ if (node.type === 'text') { out.push(node); continue; }
52
+ if (DROPPED.has(node.name)) continue;
53
+ const children = keep(node.children);
54
+ if (!KEPT.has(node.name)) { out.push(...children); continue; }
55
+ const attributes: HtmlElement['attributes'] = [];
56
+ for (const { name, value, namespace } of node.attributes) {
57
+ // a namespaced attribute (xlink:href, xml:lang) is no attribute the message's HTML keeps
58
+ if (namespace || !ATTRIBUTES.has(name)) continue;
59
+ if (name === 'href' && !['http:', 'https:', 'mailto:'].includes(scheme(value))) continue;
60
+ if (name === 'src' && !(node.name === 'img' && scheme(value) === 'data:')) continue;
61
+ if (name === 'style') { const kept = style(value); if (kept) attributes.push({ name, value: kept }); continue; }
62
+ attributes.push({ name, value });
63
+ }
64
+ if (node.name === 'a') attributes.push({ name: 'target', value: '_blank' }, { name: 'rel', value: 'noreferrer noopener' });
65
+ out.push({ type: 'element', name: node.name, namespace: node.namespace, attributes, children });
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** A received message's HTML as Gmail shows it (ADR 0032 §4): sanitized on the server and shown inline in its body. */
71
+ async function sanitized(source: string): Promise<string> {
72
+ return html.serialize(keep(await html.parseFragment(source)));
73
+ }
74
+
75
+ /** A plain-text body with its addresses followable, as Gmail links them. */
76
+ const linked = (text: string): string => esc(text).replace(/https?:\/\/[^\s<>"']+/g, (url) => `<a href="${url}" target="_blank" rel="noreferrer">${url}</a>`);
77
+
78
+ const when = (iso: unknown): string => {
79
+ const d = new Date(String(iso));
80
+ return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' });
81
+ };
82
+
83
+ // the inbox is Gmail's, at its own host, however the request reached the twin: a person's browser reaches it there
84
+ // through the World's DNS, so the links it writes carry that host (ADR 0036 §9)
85
+ const MAIL = 'https://mail.google.com';
86
+
87
+ function page(title: string, email: string, body: string, status = 200): Response {
88
+ return new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)}</title><style>body{margin:0;font:14px/1.45 Roboto,Arial,sans-serif;background:#f6f8fc;color:#1f1f1f}header{display:flex;justify-content:space-between;align-items:center;padding:12px 20px}header .brand{font-size:22px;color:#444746}header .who{color:#444746}nav{position:absolute;width:220px;padding:8px 12px}nav a{display:block;padding:6px 16px;border-radius:16px;background:#d3e3fd;color:#001d35;font-weight:600;text-decoration:none}main{margin-left:244px;margin-right:16px;background:#fff;border-radius:16px;min-height:70vh;padding:8px 0}.row{display:grid;grid-template-columns:200px 1fr 120px;gap:12px;padding:10px 20px;border-bottom:1px solid #f1f3f4;color:inherit;text-decoration:none}.row.unread{font-weight:700}.row .date{text-align:right;color:#5e5e5e}.msg{padding:12px 24px}.msg h2.hP{font-size:22px;font-weight:400;margin:8px 0 16px}.meta{color:#5e5e5e;margin-bottom:16px}.body{border-top:1px solid #f1f3f4;padding-top:16px;overflow-wrap:anywhere;${BODY}}.body pre{white-space:pre-wrap;font:inherit}.files{margin-top:24px;display:flex;gap:12px;flex-wrap:wrap}.files a{border:1px solid #dadce0;border-radius:8px;padding:10px 14px;color:#1f1f1f;text-decoration:none}.empty{padding:24px}</style></head><body><header><span class="brand">Gmail</span><span class="who">${esc(email)}</span></header><nav><a href="${MAIL}/mail/u/0/" aria-current="page">Inbox</a></nav><main>${body}</main></body></html>`, { status, headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'" } });
89
+ }
90
+
91
+ export async function screen(ctx: HandlerContext): Promise<Response> {
92
+ const request = ctx.call.request;
93
+ if (!['GET', 'HEAD'].includes(request.method)) return new Response('Method Not Allowed', { status: 405, headers: { allow: 'GET, HEAD' } });
94
+ const url = new URL(request.url);
95
+ const who = signedInAccount(ctx);
96
+ if (!who) {
97
+ // to Google's sign-in, which returns the person to the page they asked for
98
+ const back = `${MAIL}${url.pathname}${url.search}`;
99
+ return new Response(null, { status: 302, headers: { location: `https://accounts.google.com/ServiceLogin?${new URLSearchParams({ service: 'mail', continue: back })}` } });
100
+ }
101
+ const mine = ctx.rowsRaw(MESSAGE).filter((m) => m.mailbox === who && m.deleted !== true)
102
+ .sort((a, b) => String(b.receivedAt).localeCompare(String(a.receivedAt)) || String(b.id).localeCompare(String(a.id)));
103
+ const path = url.pathname.replace(/\/+$/, '');
104
+ const opened = /^\/mail\/u\/0\/inbox\/([^/]+)(?:\/attachments\/(\d+))?$/.exec(path);
105
+ if (!opened) {
106
+ if (path !== '/mail' && path !== '/mail/u/0') return page('Gmail', who, '<p class="empty" role="alert">Temporary Error (404)</p>', 404);
107
+ const rows = mine.map((m) => `<a class="row${(m.labelIds as string[] | undefined)?.includes('UNREAD') ? ' unread' : ''}" href="${MAIL}/mail/u/0/inbox/${encodeURIComponent(String(m.id))}"><span class="from">${esc(m.from || (m.envelope as Row | undefined)?.from)}</span><span class="subject">${esc(m.subject || '(no subject)')}</span><span class="date">${esc(when(m.receivedAt))}</span></a>`).join('');
108
+ return page(`Inbox - ${who} - Gmail`, who, rows || '<p class="empty">Your Primary tab is empty.</p>');
109
+ }
110
+ let id = '';
111
+ try { id = decodeURIComponent(opened[1]!); } catch { /* no such message */ }
112
+ const m = mine.find((x) => x.id === id);
113
+ if (!m) return page('Gmail', who, '<p class="empty" role="alert">That message could not be found.</p>', 404);
114
+ const files = (m.attachments as Row[] | undefined) ?? [];
115
+ if (opened[2] !== undefined) {
116
+ const index = Number(opened[2]);
117
+ const key = (m._attachments as string[] | undefined)?.[index];
118
+ const meta = files[index];
119
+ const bytes = key === undefined ? undefined : await ctx.blobs.get(key);
120
+ if (!meta || !bytes) return page('Gmail', who, '<p class="empty" role="alert">That attachment could not be found.</p>', 404);
121
+ const name = String(meta.filename ?? `attachment-${index}`).replace(/["\r\n\\]/g, '');
122
+ return new Response(new Uint8Array(bytes), { headers: { 'content-type': String(meta.contentType ?? 'application/octet-stream'), 'content-disposition': `attachment; filename="${name}"` } });
123
+ }
124
+ const body = typeof m.html === 'string' && m.html ? await sanitized(m.html) : `<pre>${linked(String(m.text ?? ''))}</pre>`;
125
+ const list = files.map((f, i) => `<a href="${MAIL}/mail/u/0/inbox/${encodeURIComponent(String(m.id))}/attachments/${i}" download="${esc(f.filename ?? '')}">${esc(f.filename ?? `attachment ${i + 1}`)} <small>Download</small></a>`).join('');
126
+ return page(`${String(m.subject ?? '(no subject)')} - ${who} - Gmail`, who,
127
+ `<article class="msg"><h2 class="hP">${esc(m.subject || '(no subject)')}</h2><div class="meta"><strong>${esc(m.from)}</strong> to ${esc(((m.to as string[] | undefined) ?? []).join(', ') || who)} · ${esc(when(m.receivedAt))}</div><div class="body a3s">${body}</div>${list ? `<div class="files" aria-label="Attachments">${list}</div>` : ''}</article>`);
128
+ }
@@ -0,0 +1,91 @@
1
+ // GMAIL'S RECEIVING SERVER'S FRONT: each command a connection sends, run as RFC 5321 orders it and Gmail's MX answers
2
+ // it. The byte-stream door (../stream.ts) frames a connection's bytes and posts each command (or a message's whole data)
3
+ // here with the session it carries; the answer is the reply lines, the moved session, and what the stream reads next.
4
+ // Every other request is the webmail's (the screens).
5
+ //
6
+ // The server takes mail from any sender, as an MX does, and refuses a recipient it does not hold: an address at another
7
+ // domain (it relays for no one) or a gmail.com address that is no Google account (550 5.1.1). It asks no authentication
8
+ // and has no certificate here, so it advertises neither AUTH nor STARTTLS (ADR 0032 §4).
9
+ import { smtpServer, type HandlerContext } from '@volter/world-core';
10
+ import {
11
+ accountOf, BRIDGE, BYE, deliver, GO_AHEAD, GREETING, HOST, MAIL_DOMAIN, MAX_SIZE, NO_ACCOUNT, NO_TLS, NOT_OURS, OK, OK_MAIL, OK_RCPT,
12
+ type Session, SYNTAX, TOO_BIG, UNRECOGNIZED, unsupported, type WireAnswer,
13
+ } from './shared.ts';
14
+
15
+ const { smtpCleared, smtpCommand, smtpDomain: domainOf, smtpEhlo, smtpFront, smtpMailFrom, smtpOrder, smtpRcptTo } = smtpServer;
16
+
17
+ const cmd = (session: Session, ...replies: string[]): WireAnswer => ({ replies, session, mode: 'command' });
18
+
19
+ /** The machine's word on a command from where the session stands: a refusal's reply, or undefined to go on. */
20
+ const order = (ctx: HandlerContext, s: Session, op: string, to?: string): string | undefined => smtpOrder(ctx, 'Session', s.stage, op, to);
21
+
22
+ /** A transaction's state cleared, the greeting kept (RSET, a new EHLO, the end of DATA). */
23
+ const cleared = (s: Session, stage: string): Session => smtpCleared(s, stage, {});
24
+
25
+ function command(ctx: HandlerContext, s: Session, line: string): WireAnswer {
26
+ const { verb, arg } = smtpCommand(line);
27
+ switch (verb) { default: return cmd(s, UNRECOGNIZED);
28
+ case 'EHLO': case 'HELO': {
29
+ if (!arg.trim()) return cmd(s, `501 5.5.4 Empty ${verb} argument not allowed, closing connection. - gsmtp`);
30
+ order(ctx, s, verb, verb.toLowerCase());
31
+ const next = cleared(s, verb.toLowerCase());
32
+ if (verb === 'HELO') return cmd(next, `250 ${HOST} at your service`);
33
+ return { replies: smtpEhlo([`${HOST} at your service`, `SIZE ${MAX_SIZE}`, '8BITMIME', 'ENHANCEDSTATUSCODES', 'PIPELINING', 'SMTPUTF8']), session: next, mode: 'command' };
34
+ }
35
+ // source: spec:4.1.1.2 "This command clears the reverse-path buffer, the forward-path buffer, and the mail data buffer"
36
+ case 'MAIL': {
37
+ const refused = order(ctx, s, 'MAIL', 'mail');
38
+ if (refused) return cmd(s, refused);
39
+ const mail = smtpMailFrom(arg);
40
+ if ('refused' in mail) return cmd(s, 'parameter' in mail ? unsupported(mail.parameter) : SYNTAX);
41
+ if (mail.size !== undefined && mail.size > MAX_SIZE) return cmd(s, TOO_BIG);
42
+ return cmd({ ...s, stage: 'mail', from: mail.from.toLowerCase(), rcpt: [], ...(mail.smtputf8 ? { smtputf8: true } : {}) }, OK_MAIL);
43
+ }
44
+ // source: spec:4.1.1.3 "This command is used to identify an individual recipient of the mail data; multiple recipients are specified by multiple uses of this command."
45
+ case 'RCPT': {
46
+ const refused = order(ctx, s, 'RCPT', 'rcpt');
47
+ if (refused) return cmd(s, refused);
48
+ const rcpt = smtpRcptTo(arg);
49
+ if ('refused' in rcpt) return cmd(s, 'parameter' in rcpt ? unsupported(rcpt.parameter) : SYNTAX);
50
+ const to = rcpt.to.toLowerCase();
51
+ if (domainOf(to) !== MAIL_DOMAIN) return cmd(s, NOT_OURS);
52
+ if (!accountOf(ctx, to)) return cmd(s, NO_ACCOUNT);
53
+ return cmd({ ...s, stage: 'rcpt', rcpt: [...s.rcpt, to] }, OK_RCPT);
54
+ }
55
+ // source: spec:4.1.1.4 "The receiver normally sends a 354 response to DATA"
56
+ case 'DATA': {
57
+ const refused = order(ctx, s, 'DATA', 'data');
58
+ if (refused) return cmd(s, refused);
59
+ return { replies: [GO_AHEAD], session: { ...s, stage: 'data' }, mode: 'data' };
60
+ }
61
+ // source: spec:4.1.1.5 "This command specifies that the current mail transaction will be aborted."
62
+ case 'RSET': {
63
+ const back = s.greeted ?? 'fresh';
64
+ if (s.stage === 'mail' || s.stage === 'rcpt') order(ctx, s, 'RSET', back);
65
+ return cmd(cleared(s, s.stage === 'mail' || s.stage === 'rcpt' ? back : s.stage), OK);
66
+ }
67
+ // source: spec:4.1.1.9 "This command does not affect any parameters or previously entered commands."
68
+ case 'NOOP': return cmd(s, OK);
69
+ // source: spec:4.1.1.10 "The QUIT command may be issued at any time."
70
+ case 'QUIT': order(ctx, s, 'QUIT', 'closed'); return { replies: [BYE], session: { ...s, stage: 'closed' }, mode: 'command', close: true };
71
+ case 'STARTTLS': return cmd(s, NO_TLS);
72
+ }
73
+ }
74
+
75
+ /** The end of a message's data: its size held against the limit, the message kept in each recipient's mailbox. */
76
+ async function endOfData(ctx: HandlerContext, s: Session, data: string): Promise<WireAnswer> {
77
+ const back = s.greeted ?? 'ehlo';
78
+ order(ctx, s, 'DATA', back);
79
+ const next = cleared(s, back);
80
+ if (data.length > MAX_SIZE) return cmd(next, TOO_BIG);
81
+ await deliver(ctx, { from: s.from ?? '', to: s.rcpt, data, smtputf8: s.smtputf8 === true });
82
+ return cmd(next, OK);
83
+ }
84
+
85
+ export async function around(ctx: HandlerContext, next: (request?: Request) => Promise<Response>): Promise<Response> {
86
+ return smtpFront(ctx, next, BRIDGE, {
87
+ open: () => ({ replies: [GREETING], session: BRIDGE.fresh(), mode: 'command' }),
88
+ command: (s, line) => command(ctx, s, line),
89
+ data: (s, data) => endOfData(ctx, s, data),
90
+ });
91
+ }
@@ -0,0 +1,6 @@
1
+ // GENERATED by twin-standard create --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+
5
+ export const gmailSemantics: Record<string, Handler> = {
6
+ };
@@ -0,0 +1,111 @@
1
+ // What Gmail's receiving server, its byte-stream door and its webmail share: the server's replies, a session's shape, the
2
+ // key the stream proves itself with, the mailboxes (Google's accounts, read from googleoauth: architecture A3) and the
3
+ // delivery of a message into them.
4
+ import { browserSessionToken, privateTransportKey, type HandlerContext, type smtpServer } from '@volter/world-core';
5
+ import { parse } from '../engine/mail.ts';
6
+
7
+ export type Row = Record<string, unknown>;
8
+
9
+ export const MESSAGE = 'message';
10
+
11
+ /** The domain whose accounts this server receives mail for: the descriptor's host rule names it, so the kernel's
12
+ * ctx.mail hands its mail here. */
13
+ export const MAIL_DOMAIN = 'gmail.com';
14
+
15
+ /** A session as its connection carries it between commands. */
16
+ export type Session = { stage: string; from?: string; rcpt: string[]; smtputf8?: boolean; greeted?: 'helo' | 'ehlo' };
17
+
18
+ /** What the front answers the stream: the reply lines, the session, and whether the stream reads a message next. */
19
+ export type WireAnswer = smtpServer.SmtpAnswer<Session>;
20
+
21
+ // Gmail's receiving server's replies. Google publishes its refusals in "Gmail SMTP errors and codes", each ending
22
+ // `gsmtp`; where the documentation stops: the page lists no successes and no refusal of a recipient at a domain Google
23
+ // does not receive for, so those replies' words are unverified (each is said so below), and the queue ids Gmail adds
24
+ // are left out.
25
+ // source: https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes "gsmtp (Google SMTP) is added to all errors"
26
+ export const HOST = 'mx.google.com';
27
+ export const GREETING = `220 ${HOST} ESMTP - gsmtp`;
28
+ // the largest message the server takes, as its EHLO advertises SIZE (recalled, as above)
29
+ export const MAX_SIZE = 157_286_400;
30
+ export const OK_MAIL = '250 2.1.0 OK - gsmtp';
31
+ export const OK_RCPT = '250 2.1.5 OK - gsmtp';
32
+ export const OK = '250 2.0.0 OK - gsmtp';
33
+ export const GO_AHEAD = '354 Go ahead - gsmtp';
34
+ export const BYE = '221 2.0.0 closing connection - gsmtp';
35
+ export const UNRECOGNIZED = '502 5.5.1 Unrecognized command. - gsmtp';
36
+ // an address at gmail.com that is no account
37
+ // source: https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes "The email account that you tried to reach does not exist."
38
+ export const NO_ACCOUNT = '550 5.1.1 The email account that you tried to reach does not exist. - gsmtp';
39
+ // an address at a domain this server does not receive for: an MX relays for no one. Unverified: Google's error page has
40
+ // no row for it, no published transcript of mx.google.com's reply to it was found, and a session to its MX from a
41
+ // residential connection could not be made (port 25 closed); no wording is claimed for it, the words below only stand in
42
+ export const NOT_OURS = '550 5.7.1 Relaying denied. - gsmtp';
43
+ // source: https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes "Your message exceeded Google's message size limits."
44
+ export const TOO_BIG = '552 5.3.4 Your message exceeded Google\'s message size limits. - gsmtp';
45
+ // no certificate on the World's side port (ADR 0032 §4: IMAP and submission, which need TLS, are later cards)
46
+ export const NO_TLS = '454 4.7.0 TLS not available - gsmtp';
47
+ // source: spec:4.1.1.11 "command, it will return code 555."
48
+ export const unsupported = (arg: string): string => `555 5.5.4 Unsupported option: ${arg} - gsmtp`;
49
+ export const SYNTAX = '501 5.5.2 Syntax error. - gsmtp';
50
+ // The host cannot persist a message: a temporary failure, as RFC 5321 names it.
51
+ // source: spec:4.2.3 "451 Requested action aborted: local error in processing"
52
+ const queueFailure = (session: Session): WireAnswer => ({ replies: ['451 4.3.0 Local error in processing - gsmtp'], session, mode: 'command', close: true });
53
+
54
+ /** The bridge between a connection and the server's front (world-core's SMTP server machine): the header the stream
55
+ * signs its requests with, a key made once per process so a request from outside it (the webmail's port) cannot speak
56
+ * for a connection, a fresh session, and the server's local-error reply. */
57
+ export const BRIDGE: smtpServer.SmtpBridge<Session> = { header: 'x-volter-gmail-wire', key: () => privateTransportKey('gmail'), fresh: () => ({ stage: 'fresh', rcpt: [] }), failure: queueFailure };
58
+
59
+
60
+ type Reader = Pick<HandlerContext, 'ownerRow'>;
61
+
62
+ /** The mailbox an address names: the address of the Google account whose sign-up recorded its person (googleoauth's
63
+ * `_person`, keyed `person:<email>`, with the account's `sub`, whose `account` stands); a `+tag` reaches the same
64
+ * mailbox, as Gmail delivers it. Where the
65
+ * documentation stops: Gmail also ignores dots in the local part; the twin matches the address as signed up. */
66
+ export function accountOf(ctx: Reader, address: string): string | undefined {
67
+ const lower = address.toLowerCase();
68
+ const plain = lower.replace(/\+[^@]*@/, '@');
69
+ for (const email of lower === plain ? [lower] : [lower, plain]) {
70
+ const person = ctx.ownerRow('googleoauth', '_person', `person:${email}`);
71
+ const sub = typeof person?.sub === 'string' && person.deleted !== true ? person.sub : undefined;
72
+ const account = sub ? ctx.ownerRow('googleoauth', 'account', sub) : undefined;
73
+ if (sub && account && account.deleted !== true) return email;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ /** A message accepted: kept once in each recipient's mailbox, with its envelope, its bytes and what they parse to. */
79
+ export async function deliver(ctx: HandlerContext, e: { from: string; to: string[]; data: string; smtputf8: boolean }): Promise<void> {
80
+ const p = parse(e.data);
81
+ for (const rcpt of e.to) {
82
+ const mailbox = accountOf(ctx, rcpt);
83
+ if (!mailbox) continue;
84
+ const id = ctx.mint(MESSAGE);
85
+ const rawKey = `${id}/raw`;
86
+ await ctx.blobs.put(rawKey, Uint8Array.from(e.data, (c) => c.charCodeAt(0) & 0xff));
87
+ const attachmentKeys: string[] = [];
88
+ for (let i = 0; i < p.attachments.length; i++) {
89
+ const key = `${id}/attachments/${i}`;
90
+ await ctx.blobs.put(key, Uint8Array.from(atob(p.attachments[i]!.base64), (c) => c.charCodeAt(0)));
91
+ attachmentKeys.push(key);
92
+ }
93
+ await ctx.write(MESSAGE, id, {
94
+ id, mailbox, envelope: { from: e.from, to: rcpt, smtputf8: e.smtputf8 }, size: e.data.length, receivedAt: ctx.occurredAt,
95
+ headers: p.headers, subject: p.subject, from: p.from, to: p.to, cc: p.cc, text: p.text, html: p.html,
96
+ attachments: p.attachments.map(({ base64: _b, ...a }) => a), labelIds: ['INBOX', 'UNREAD'], _raw: rawKey, _attachments: attachmentKeys,
97
+ }, 'message.deliver');
98
+ }
99
+ }
100
+
101
+ /** The SID cookie Google's sign-in sets on .google.com (googleoauth's contract). */
102
+ const SESSION_COOKIE = 'SID';
103
+ /** The mailbox of the Google account a request's browser is signed in to: its `SID` cookie names a session googleoauth
104
+ * recorded (`_web_session`, keyed `websession:<token>`), whose person is the account's. */
105
+ export function signedInAccount(ctx: HandlerContext): string | undefined {
106
+ const token = browserSessionToken(ctx.call.request, SESSION_COOKIE);
107
+ if (!token) return undefined;
108
+ const session = ctx.ownerRow('googleoauth', '_web_session', `websession:${token}`);
109
+ if (!session || session.token !== token || typeof session.email !== 'string') return undefined;
110
+ return accountOf(ctx, session.email);
111
+ }