@volter/twin-gmail 0.0.0-stage → 1.0.0

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,97 @@
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 } from '@volter/world-core';
5
+ import { parse } from "../engine/mail.js";
6
+ export const MESSAGE = 'message';
7
+ /** The domain whose accounts this server receives mail for: the descriptor's host rule names it, so the kernel's
8
+ * ctx.mail hands its mail here. */
9
+ export const MAIL_DOMAIN = 'gmail.com';
10
+ // Gmail's receiving server's replies. Google publishes its refusals in "Gmail SMTP errors and codes", each ending
11
+ // `gsmtp`; where the documentation stops: the page lists no successes and no refusal of a recipient at a domain Google
12
+ // does not receive for, so those replies' words are unverified (each is said so below), and the queue ids Gmail adds
13
+ // are left out.
14
+ // source: https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes "gsmtp (Google SMTP) is added to all errors"
15
+ export const HOST = 'mx.google.com';
16
+ export const GREETING = `220 ${HOST} ESMTP - gsmtp`;
17
+ // the largest message the server takes, as its EHLO advertises SIZE (recalled, as above)
18
+ export const MAX_SIZE = 157_286_400;
19
+ export const OK_MAIL = '250 2.1.0 OK - gsmtp';
20
+ export const OK_RCPT = '250 2.1.5 OK - gsmtp';
21
+ export const OK = '250 2.0.0 OK - gsmtp';
22
+ export const GO_AHEAD = '354 Go ahead - gsmtp';
23
+ export const BYE = '221 2.0.0 closing connection - gsmtp';
24
+ export const UNRECOGNIZED = '502 5.5.1 Unrecognized command. - gsmtp';
25
+ // an address at gmail.com that is no account
26
+ // 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."
27
+ export const NO_ACCOUNT = '550 5.1.1 The email account that you tried to reach does not exist. - gsmtp';
28
+ // an address at a domain this server does not receive for: an MX relays for no one. Unverified: Google's error page has
29
+ // 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
30
+ // residential connection could not be made (port 25 closed); no wording is claimed for it, the words below only stand in
31
+ export const NOT_OURS = '550 5.7.1 Relaying denied. - gsmtp';
32
+ // source: https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes "Your message exceeded Google's message size limits."
33
+ export const TOO_BIG = '552 5.3.4 Your message exceeded Google\'s message size limits. - gsmtp';
34
+ // no certificate on the World's side port (ADR 0032 §4: IMAP and submission, which need TLS, are later cards)
35
+ export const NO_TLS = '454 4.7.0 TLS not available - gsmtp';
36
+ // source: spec:4.1.1.11 "command, it will return code 555."
37
+ export const unsupported = (arg) => `555 5.5.4 Unsupported option: ${arg} - gsmtp`;
38
+ export const SYNTAX = '501 5.5.2 Syntax error. - gsmtp';
39
+ // The host cannot persist a message: a temporary failure, as RFC 5321 names it.
40
+ // source: spec:4.2.3 "451 Requested action aborted: local error in processing"
41
+ const queueFailure = (session) => ({ replies: ['451 4.3.0 Local error in processing - gsmtp'], session, mode: 'command', close: true });
42
+ /** The bridge between a connection and the server's front (world-core's SMTP server machine): the header the stream
43
+ * signs its requests with, a key made once per process so a request from outside it (the webmail's port) cannot speak
44
+ * for a connection, a fresh session, and the server's local-error reply. */
45
+ export const BRIDGE = { header: 'x-volter-gmail-wire', key: () => privateTransportKey('gmail'), fresh: () => ({ stage: 'fresh', rcpt: [] }), failure: queueFailure };
46
+ /** The mailbox an address names: the address of the Google account whose sign-up recorded its person (googleoauth's
47
+ * `_person`, keyed `person:<email>`, with the account's `sub`, whose `account` stands); a `+tag` reaches the same
48
+ * mailbox, as Gmail delivers it. Where the
49
+ * documentation stops: Gmail also ignores dots in the local part; the twin matches the address as signed up. */
50
+ export function accountOf(ctx, address) {
51
+ const lower = address.toLowerCase();
52
+ const plain = lower.replace(/\+[^@]*@/, '@');
53
+ for (const email of lower === plain ? [lower] : [lower, plain]) {
54
+ const person = ctx.ownerRow('googleoauth', '_person', `person:${email}`);
55
+ const sub = typeof person?.sub === 'string' && person.deleted !== true ? person.sub : undefined;
56
+ const account = sub ? ctx.ownerRow('googleoauth', 'account', sub) : undefined;
57
+ if (sub && account && account.deleted !== true)
58
+ return email;
59
+ }
60
+ return undefined;
61
+ }
62
+ /** A message accepted: kept once in each recipient's mailbox, with its envelope, its bytes and what they parse to. */
63
+ export async function deliver(ctx, e) {
64
+ const p = parse(e.data);
65
+ for (const rcpt of e.to) {
66
+ const mailbox = accountOf(ctx, rcpt);
67
+ if (!mailbox)
68
+ continue;
69
+ const id = ctx.mint(MESSAGE);
70
+ const rawKey = `${id}/raw`;
71
+ await ctx.blobs.put(rawKey, Uint8Array.from(e.data, (c) => c.charCodeAt(0) & 0xff));
72
+ const attachmentKeys = [];
73
+ for (let i = 0; i < p.attachments.length; i++) {
74
+ const key = `${id}/attachments/${i}`;
75
+ await ctx.blobs.put(key, Uint8Array.from(atob(p.attachments[i].base64), (c) => c.charCodeAt(0)));
76
+ attachmentKeys.push(key);
77
+ }
78
+ await ctx.write(MESSAGE, id, {
79
+ id, mailbox, envelope: { from: e.from, to: rcpt, smtputf8: e.smtputf8 }, size: e.data.length, receivedAt: ctx.occurredAt,
80
+ headers: p.headers, subject: p.subject, from: p.from, to: p.to, cc: p.cc, text: p.text, html: p.html,
81
+ attachments: p.attachments.map(({ base64: _b, ...a }) => a), labelIds: ['INBOX', 'UNREAD'], _raw: rawKey, _attachments: attachmentKeys,
82
+ }, 'message.deliver');
83
+ }
84
+ }
85
+ /** The SID cookie Google's sign-in sets on .google.com (googleoauth's contract). */
86
+ const SESSION_COOKIE = 'SID';
87
+ /** The mailbox of the Google account a request's browser is signed in to: its `SID` cookie names a session googleoauth
88
+ * recorded (`_web_session`, keyed `websession:<token>`), whose person is the account's. */
89
+ export function signedInAccount(ctx) {
90
+ const token = browserSessionToken(ctx.call.request, SESSION_COOKIE);
91
+ if (!token)
92
+ return undefined;
93
+ const session = ctx.ownerRow('googleoauth', '_web_session', `websession:${token}`);
94
+ if (!session || session.token !== token || typeof session.email !== 'string')
95
+ return undefined;
96
+ return accountOf(ctx, session.email);
97
+ }
@@ -0,0 +1,4 @@
1
+ import type { StateField } from '@volter/world-core';
2
+ export declare const states: Record<string, {
3
+ state: Record<string, StateField>;
4
+ }>;
@@ -0,0 +1,33 @@
1
+ const RFC5321 = (section, quote) => `spec:${section} "${quote}"`;
2
+ // Where the documentation stops: Gmail's words for a command out of order, recalled from its server's replies
3
+ const badSequence = (message) => ({ status: 503, code: '5.5.1', message });
4
+ const ORDER = 'Once started, a mail transaction consists of a transaction beginning command, one or more RCPT commands, and a DATA command, in that order.';
5
+ // fresh (nothing yet), helo or ehlo (greeted), mail (a sender), rcpt (recipients), data (the message's lines), closed
6
+ const stage = [
7
+ { operation: 'EHLO', from: '*', to: 'ehlo', source: RFC5321('4.1.4', 'If it is issued after the session begins and the EHLO command is acceptable to the SMTP server, the SMTP server MUST clear all buffers and reset the state exactly as if a RSET command had been issued.') },
8
+ { operation: 'HELO', from: '*', to: 'helo', source: RFC5321('4.1.1.1', 'These commands are used to identify the SMTP client to the SMTP server.') },
9
+ {
10
+ operation: 'MAIL', from: ['helo', 'ehlo'], to: 'mail',
11
+ refusals: { fresh: badSequence('EHLO/HELO first. - gsmtp'), mail: badSequence('Nested MAIL command. - gsmtp'), rcpt: badSequence('Nested MAIL command. - gsmtp') },
12
+ source: RFC5321('4.1.4', 'MAIL (or SEND, SOML, or SAML) MUST NOT be sent if a mail transaction is already open'),
13
+ },
14
+ {
15
+ operation: 'RCPT', from: ['mail', 'rcpt'], to: 'rcpt',
16
+ refusals: { fresh: badSequence('MAIL first. - gsmtp'), helo: badSequence('MAIL first. - gsmtp'), ehlo: badSequence('MAIL first. - gsmtp') },
17
+ source: RFC5321('4.1.4', ORDER),
18
+ },
19
+ {
20
+ operation: 'DATA', from: ['rcpt'], to: 'data',
21
+ refusals: { fresh: badSequence('MAIL first. - gsmtp'), helo: badSequence('MAIL first. - gsmtp'), ehlo: badSequence('MAIL first. - gsmtp'), mail: badSequence('RCPT first. - gsmtp') },
22
+ source: RFC5321('4.1.4', ORDER),
23
+ },
24
+ // the end-of-data dot closes the transaction, back to the greeting the session had
25
+ { operation: 'DATA', from: ['data'], to: 'ehlo', source: RFC5321('4.1.4', 'or if the previous one successfully concluded with a successful DATA command') },
26
+ { operation: 'DATA', from: ['data'], to: 'helo', source: RFC5321('4.1.4', 'or if the previous one successfully concluded with a successful DATA command') },
27
+ { operation: 'RSET', from: ['mail', 'rcpt'], to: 'ehlo', source: RFC5321('4.1.1.5', 'This command specifies that the current mail transaction will be aborted.') },
28
+ { operation: 'RSET', from: ['mail', 'rcpt'], to: 'helo', source: RFC5321('4.1.1.5', 'This command specifies that the current mail transaction will be aborted.') },
29
+ { operation: 'QUIT', from: '*', to: 'closed', source: RFC5321('4.1.1.10', 'The QUIT command may be issued at any time.') },
30
+ ];
31
+ export const states = {
32
+ Session: { state: { stage: { initial: 'fresh', transitions: stage } } },
33
+ };
@@ -0,0 +1,9 @@
1
+ import { type PackFetchOptions } from '@volter/world-core';
2
+ export declare function createGmailTwinServer(options?: PackFetchOptions & {
3
+ port?: number;
4
+ smtpPort?: number;
5
+ }): Promise<{
6
+ port: number;
7
+ smtpPort: number;
8
+ stop: () => Promise<void>;
9
+ }>;
@@ -0,0 +1,23 @@
1
+ // FETCH-FIRST (runtime contract R12b) with a wire of the pack's own: the webmail on the main port (the pack's fetch) and
2
+ // Gmail's receiving SMTP server on its side port (`smtpPort`, the manifest's sidePorts: the kernel's `serveStream`
3
+ // carrying the byte-stream door's connections), where the World's senders hand gmail.com's mail (ctx.mail). A listener
4
+ // that fails to open releases the one already open; stop closes both and settles once both have.
5
+ import { serveHttp, serveStream } from '@volter/world-core';
6
+ import { createGmailFetch } from "./fetch.js";
7
+ import { createGmailTwinStream } from "./stream.js";
8
+ export async function createGmailTwinServer(options = {}) {
9
+ const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createGmailFetch(options) });
10
+ let smtp;
11
+ try {
12
+ smtp = await serveStream({ port: options.smtpPort ?? 0, stream: createGmailTwinStream(options) });
13
+ }
14
+ catch (error) {
15
+ await server.stop(true);
16
+ throw error;
17
+ }
18
+ return {
19
+ port: server.port ?? options.port ?? 0,
20
+ smtpPort: smtp.port,
21
+ stop: async () => { await Promise.all([smtp.stop(), server.stop(true)]); },
22
+ };
23
+ }
@@ -0,0 +1,2 @@
1
+ import { type PackFetchOptions, type TwinStream } from '@volter/world-core';
2
+ export declare function createGmailTwinStream(options?: PackFetchOptions): TwinStream;
@@ -0,0 +1,10 @@
1
+ // GMAIL'S RECEIVING SERVER'S BYTE-STREAM DOOR (world-core's TwinStream): a connection's bytes framed and carried to the
2
+ // server's front (./semantics/around.ts) by the kernel's SMTP server machine (`smtpServer.smtpStream`, RFC 5321
3
+ // §4.1.1.4 and §4.5.2). The host carries the bytes: a local listener's socket on the pack's SMTP side port (./server.ts),
4
+ // or the World's sender in process (a journey walk's companion).
5
+ import { smtpServer } from '@volter/world-core';
6
+ import { createGmailFetch } from "./fetch.js";
7
+ import { BRIDGE } from "./semantics/shared.js";
8
+ export function createGmailTwinStream(options = {}) {
9
+ return smtpServer.smtpStream(createGmailFetch(options), BRIDGE);
10
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "//": "GENERATED by twin-standard facts from the pack descriptor; no prior-artifact entries are retained.",
3
+ "packs": {
4
+ "gmail": {
5
+ "adoption": {
6
+ "sdks": [],
7
+ "envStems": []
8
+ },
9
+ "hosts": [
10
+ {
11
+ "host": "mail.google.com"
12
+ },
13
+ {
14
+ "host": "gmail.com"
15
+ }
16
+ ],
17
+ "archetype": "crud",
18
+ "protocol": {
19
+ "declared": "3",
20
+ "major": 3,
21
+ "standing": "current"
22
+ },
23
+ "transport": "rest",
24
+ "resources": [
25
+ "message"
26
+ ],
27
+ "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",
28
+ "requires": [
29
+ "googleoauth"
30
+ ],
31
+ "sidePorts": [
32
+ {
33
+ "name": "smtp",
34
+ "option": "smtpPort",
35
+ "flag": "--smtp-port",
36
+ "scheme": "smtp",
37
+ "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"
38
+ }
39
+ ],
40
+ "seed": {
41
+ "signUp": "https://accounts.google.com/signup"
42
+ },
43
+ "serveExport": "createGmailTwinServer"
44
+ }
45
+ },
46
+ "undeclared": []
47
+ }