@framers/agentos-ext-channel-email 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview The route an unsubscribe link opens, as one function from a web `Request` to a `Response`, so a host
3
+ * mounts it on any server on Node.js that speaks them (a Next.js route handler on the Node.js runtime, Hono, a Node
4
+ * server through an adapter). The route reads the request with web APIs alone; the package it ships in loads Node's
5
+ * modules. A GET shows a page that asks and changes nothing, since link scanners follow links. A POST suppresses at
6
+ * once: a mailbox provider's one-click POST (RFC 8058 section 3.1) and the page's buttons alike, the token in the
7
+ * address the only thing it depends on. The route holds no transport, so an unsubscribe is never answered by a mail.
8
+ */
9
+ import type { RecipientCapability } from './preferences/RecipientCapability.js';
10
+ import type { SuppressionStore } from './preferences/SuppressionStore.js';
11
+ /** What the route takes: the stores the service draws in and reads, and the page's words. */
12
+ export interface UnsubscribeHandlerOptions {
13
+ /** The store the service's tokens were drawn in. */
14
+ readonly tokens: RecipientCapability;
15
+ /** The suppression record the service reads before each send. */
16
+ readonly suppressions: SuppressionStore;
17
+ /** The sender's name, shown above the page's heading. */
18
+ readonly productName?: string;
19
+ /** The words a page names each category by, such as `{ 'weekly-summary': 'weekly summary' }`; the category with its hyphens and underscores as spaces when not given. */
20
+ readonly categoryNames?: Readonly<Record<string, string>>;
21
+ /** Where a person deletes their account: an https address or a path, linked on the page of a required mail's link. */
22
+ readonly deleteAccountUrl?: string;
23
+ /** The clock the day of each suppression is read from, in epoch milliseconds: `Date.now` unless given. */
24
+ readonly now?: () => number;
25
+ /** Writes the page in place of the route's own, given what the token found and what was done. */
26
+ readonly render?: (view: UnsubscribeView) => string;
27
+ }
28
+ /**
29
+ * What a page shows. `category` is the category the link stops, or `null` for a required mail's link, which turns off
30
+ * every optional mail. `ask` carries the token, so a page of the host's own can post it back.
31
+ */
32
+ export type UnsubscribeView = {
33
+ readonly state: 'ask';
34
+ readonly category: string | null;
35
+ readonly token: string;
36
+ } | {
37
+ readonly state: 'done';
38
+ readonly category: string | null;
39
+ readonly all: boolean;
40
+ } | {
41
+ readonly state: 'invalid';
42
+ };
43
+ /**
44
+ * The route an unsubscribe link opens, as a function from a `Request` to a `Response`, mounted at the path of the
45
+ * service's `pageUrl`:
46
+ *
47
+ * - GET and HEAD show a page that asks and change nothing: for a category's link, a button that stops the category and
48
+ * one that turns off every optional mail; for a required mail's link, one button that turns off every optional mail,
49
+ * what still comes and why, and a link to delete the account when `deleteAccountUrl` is given. A token that finds
50
+ * nothing answers 404 with a page that says so.
51
+ * - POST suppresses at once, with the reason `manual` on the day `now` reads: a one-click POST, or any POST that names
52
+ * no choice, writes the link's own list and answers a blank 200, never a redirect (RFC 8058 section 3.2); the page's
53
+ * buttons post `choice=this` or `choice=all` and get the page that says what was done. A required mail's link always
54
+ * writes `OPTIONAL_MAIL_SCOPE`. A token that finds nothing writes nothing and gets the same blank 200, or the 404
55
+ * page from a button.
56
+ * - Any other method answers 405.
57
+ *
58
+ * Every answer is kept by no cache and sends no referrer. Nothing is mailed, so no unsubscribe is ever confirmed by a
59
+ * mail. An error of a store passes through as it was thrown, so the server answers 500 and a mailbox provider tries
60
+ * again.
61
+ *
62
+ * @param options The stores and the page's words; each is checked here, and a `TypeError` names the first one at fault.
63
+ */
64
+ export declare function createUnsubscribeHandler(options: UnsubscribeHandlerOptions): (request: Request) => Promise<Response>;
65
+ //# sourceMappingURL=unsubscribeHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribeHandler.d.ts","sourceRoot":"","sources":["../src/unsubscribeHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,6FAA6F;AAC7F,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,yDAAyD;IACzD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yKAAyK;IACzK,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,sHAAsH;IACtH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,0GAA0G;IAC1G,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,MAAM,CAAC;CACrD;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAkHlC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA8FpH"}
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUnsubscribeHandler = createUnsubscribeHandler;
4
+ const unsubscribe_js_1 = require("./unsubscribe.js");
5
+ /** The most bytes of a form the route reads; a longer body counts as a one-click POST. */
6
+ const MOST_FORM_BYTES = 4096;
7
+ /** Every answer: kept by no cache, sending no referrer, as the address carries the token, and kept out of search. */
8
+ const PRIVATE = {
9
+ 'Cache-Control': 'no-store',
10
+ 'Referrer-Policy': 'no-referrer',
11
+ 'X-Robots-Tag': 'noindex',
12
+ };
13
+ /** A page's answer: HTML that loads nothing, runs nothing and posts only to its own origin. */
14
+ const PAGE_HEADERS = {
15
+ ...PRIVATE,
16
+ 'Content-Type': 'text/html; charset=utf-8',
17
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
18
+ 'X-Content-Type-Options': 'nosniff',
19
+ };
20
+ /** The page's style: phone first, 16-pixel text, 44-pixel buttons, light and dark. */
21
+ const STYLE = [
22
+ ':root{color-scheme:light dark}',
23
+ 'body{margin:0;font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}',
24
+ 'main{max-width:32rem;margin:0 auto;padding:32px 16px}',
25
+ '.from{margin:0 0 8px;font-size:14px}',
26
+ 'h1{font-size:22px;line-height:1.3;margin:0 0 16px}',
27
+ 'p{margin:0 0 16px}',
28
+ 'form{margin:0 0 12px}',
29
+ 'button{font:inherit;width:100%;min-height:44px;padding:10px 16px;border-radius:8px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer}',
30
+ 'button.primary{background:#1a73e8;border-color:#1a73e8;color:#fff}',
31
+ '.note{font-size:14px}',
32
+ ].join('');
33
+ /** What still comes once optional mail is off, said on every page that turns mail off. */
34
+ const STILL_COMES = 'Mail that is part of your account still comes when it is due: sign-in codes you ask for, receipts when you pay, replies to mail you write to us, and security and legal notices.';
35
+ /** A link on the page, checked: an https address or a path that starts with one slash. */
36
+ function requireLink(value) {
37
+ if (typeof value === 'string' && !/[\s"'<>]/.test(value)) {
38
+ if (value.startsWith('/') && !value.startsWith('//'))
39
+ return value;
40
+ try {
41
+ if (new URL(value).protocol === 'https:')
42
+ return value;
43
+ }
44
+ catch {
45
+ // Refused below.
46
+ }
47
+ }
48
+ throw new TypeError('The deleteAccountUrl was refused: give an https address or a path that starts with one slash');
49
+ }
50
+ /** The day of an epoch reading in UTC, `YYYY-MM-DD`. */
51
+ function dayOf(reading) {
52
+ return new Date(reading).toISOString().slice(0, 10);
53
+ }
54
+ /** The token an address carries in `t`, or the empty string. */
55
+ function tokenOf(request) {
56
+ try {
57
+ return new URL(request.url).searchParams.get('t') ?? '';
58
+ }
59
+ catch {
60
+ return '';
61
+ }
62
+ }
63
+ /**
64
+ * What a token names when it is an unsubscribe link's: an address's hash with `OPTIONAL_MAIL_SCOPE` or a category's
65
+ * list. A token of any other scope, such as one a host drew for a purpose of its own in the same store, finds nothing,
66
+ * so a link can only ever stop mail.
67
+ */
68
+ async function found(tokens, token) {
69
+ if (token === '')
70
+ return null;
71
+ const grant = await tokens.find(token);
72
+ if (grant === null || !unsubscribe_js_1.ADDRESS_HASH.test(grant.recipient))
73
+ return null;
74
+ if (grant.scope === unsubscribe_js_1.OPTIONAL_MAIL_SCOPE)
75
+ return { addressHash: grant.recipient, scope: grant.scope, category: null };
76
+ const category = (0, unsubscribe_js_1.categoryOfScope)(grant.scope);
77
+ return category === null ? null : { addressHash: grant.recipient, scope: grant.scope, category };
78
+ }
79
+ /**
80
+ * The page's choice from a form the route can read: `all` for every optional mail, `this` for the link's own list, and
81
+ * `null` for a one-click POST or any body that is no form, longer than 4,096 bytes or names no choice.
82
+ */
83
+ async function choiceOf(request) {
84
+ const type = request.headers.get('content-type') ?? '';
85
+ if (!/^application\/x-www-form-urlencoded\b/i.test(type) || request.body === null)
86
+ return null;
87
+ const reader = request.body.getReader();
88
+ const decoder = new TextDecoder();
89
+ let form = '';
90
+ let bytes = 0;
91
+ for (;;) {
92
+ const next = await reader.read();
93
+ if (next.done)
94
+ break;
95
+ const chunk = next.value;
96
+ bytes += chunk.byteLength;
97
+ if (bytes > MOST_FORM_BYTES) {
98
+ await reader.cancel();
99
+ return null;
100
+ }
101
+ form += decoder.decode(chunk, { stream: true });
102
+ }
103
+ form += decoder.decode();
104
+ const choice = new URLSearchParams(form).get('choice');
105
+ if (choice === null)
106
+ return null;
107
+ return choice === 'all' ? 'all' : 'this';
108
+ }
109
+ /**
110
+ * The route an unsubscribe link opens, as a function from a `Request` to a `Response`, mounted at the path of the
111
+ * service's `pageUrl`:
112
+ *
113
+ * - GET and HEAD show a page that asks and change nothing: for a category's link, a button that stops the category and
114
+ * one that turns off every optional mail; for a required mail's link, one button that turns off every optional mail,
115
+ * what still comes and why, and a link to delete the account when `deleteAccountUrl` is given. A token that finds
116
+ * nothing answers 404 with a page that says so.
117
+ * - POST suppresses at once, with the reason `manual` on the day `now` reads: a one-click POST, or any POST that names
118
+ * no choice, writes the link's own list and answers a blank 200, never a redirect (RFC 8058 section 3.2); the page's
119
+ * buttons post `choice=this` or `choice=all` and get the page that says what was done. A required mail's link always
120
+ * writes `OPTIONAL_MAIL_SCOPE`. A token that finds nothing writes nothing and gets the same blank 200, or the 404
121
+ * page from a button.
122
+ * - Any other method answers 405.
123
+ *
124
+ * Every answer is kept by no cache and sends no referrer. Nothing is mailed, so no unsubscribe is ever confirmed by a
125
+ * mail. An error of a store passes through as it was thrown, so the server answers 500 and a mailbox provider tries
126
+ * again.
127
+ *
128
+ * @param options The stores and the page's words; each is checked here, and a `TypeError` names the first one at fault.
129
+ */
130
+ function createUnsubscribeHandler(options) {
131
+ if (typeof options !== 'object' || options === null)
132
+ throw new TypeError('The unsubscribe route options were refused: give them as an object');
133
+ const { tokens, suppressions } = options;
134
+ if (typeof tokens?.find !== 'function')
135
+ throw new TypeError('The unsubscribe route tokens were refused: give a store with find');
136
+ if (typeof suppressions?.suppress !== 'function')
137
+ throw new TypeError('The unsubscribe route suppressions were refused: give a store with suppress');
138
+ const productName = options.productName === undefined ? undefined : (0, unsubscribe_js_1.requireLine)('product name', options.productName, 120);
139
+ // A map, so a category no name is given for, such as `constructor`, finds no name an object inherits.
140
+ const names = new Map();
141
+ for (const [category, name] of Object.entries(options.categoryNames ?? {}))
142
+ names.set(category, (0, unsubscribe_js_1.requireLine)('category name', name, 120));
143
+ const deleteAccountUrl = options.deleteAccountUrl === undefined ? undefined : requireLink(options.deleteAccountUrl);
144
+ const now = options.now ?? Date.now;
145
+ if (typeof now !== 'function')
146
+ throw new TypeError('The unsubscribe route clock was refused: give a function or leave it out');
147
+ const render = options.render;
148
+ if (render !== undefined && typeof render !== 'function')
149
+ throw new TypeError('The unsubscribe route render was refused: give a function or leave it out');
150
+ /** The words a page names a category by. */
151
+ const nameOf = (category) => names.get(category) ?? category.replace(/[-_]+/g, ' ');
152
+ /** The route's own page for a view. */
153
+ const pageOf = (view) => {
154
+ const parts = [];
155
+ let title;
156
+ if (view.state === 'invalid') {
157
+ title = 'This link does not work';
158
+ parts.push(`<h1>${title}</h1>`, '<p>Use the unsubscribe link at the foot of a newer mail.</p>');
159
+ }
160
+ else if (view.state === 'done') {
161
+ title = 'You are unsubscribed';
162
+ const what = view.all || view.category === null ? 'Every optional mail to this address is off.' : `No more ${(0, unsubscribe_js_1.escapeHtml)(nameOf(view.category))} mail goes to this address.`;
163
+ parts.push(`<h1>${title}</h1>`, `<p>${what}</p>`, `<p class="note">${(0, unsubscribe_js_1.escapeHtml)(STILL_COMES)}</p>`);
164
+ }
165
+ else {
166
+ const action = `?t=${(0, unsubscribe_js_1.escapeHtml)(view.token)}`;
167
+ const button = (choice, words, primary) => `<form method="post" action="${action}"><input type="hidden" name="choice" value="${choice}"><button type="submit"${primary ? ' class="primary"' : ''}>${(0, unsubscribe_js_1.escapeHtml)(words)}</button></form>`;
168
+ if (view.category === null) {
169
+ title = 'Turn off optional mail';
170
+ parts.push(`<h1>${title}</h1>`, `<p>${(0, unsubscribe_js_1.escapeHtml)(STILL_COMES)}</p>`, '<p>Turn off every optional mail to this address?</p>', button('all', 'Turn off all optional mail', true));
171
+ if (deleteAccountUrl !== undefined) {
172
+ parts.push(`<p class="note">To stop that mail too, <a href="${(0, unsubscribe_js_1.escapeHtml)(deleteAccountUrl)}">delete your account</a>.</p>`);
173
+ }
174
+ }
175
+ else {
176
+ title = 'Unsubscribe';
177
+ const name = (0, unsubscribe_js_1.escapeHtml)(nameOf(view.category));
178
+ parts.push(`<h1>${title}</h1>`, `<p>Stop ${name} mail to this address?</p>`, button('this', `Unsubscribe from ${nameOf(view.category)} mail`, true), button('all', 'Unsubscribe from all optional mail', false), `<p class="note">${(0, unsubscribe_js_1.escapeHtml)(STILL_COMES)}</p>`);
179
+ }
180
+ }
181
+ const from = productName === undefined ? '' : `<p class="from">${(0, unsubscribe_js_1.escapeHtml)(productName)}</p>`;
182
+ return [
183
+ '<!doctype html>',
184
+ '<html lang="en"><head><meta charset="utf-8">',
185
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
186
+ '<meta name="robots" content="noindex">',
187
+ `<title>${(0, unsubscribe_js_1.escapeHtml)(title)}</title><style>${STYLE}</style></head>`,
188
+ `<body><main>${from}${parts.join('')}</main></body></html>`,
189
+ ].join('');
190
+ };
191
+ /** A page's answer, its body left out for HEAD. */
192
+ const page = (view, status, head) => new Response(head ? null : render === undefined ? pageOf(view) : render(view), { status, headers: PAGE_HEADERS });
193
+ /** The blank answer a one-click POST gets. */
194
+ const blank = () => new Response(null, { status: 200, headers: PRIVATE });
195
+ return async (request) => {
196
+ const method = request.method.toUpperCase();
197
+ if (method !== 'GET' && method !== 'HEAD' && method !== 'POST') {
198
+ return new Response(null, { status: 405, headers: { ...PRIVATE, Allow: 'GET, HEAD, POST' } });
199
+ }
200
+ const token = tokenOf(request);
201
+ const link = await found(tokens, token);
202
+ if (method !== 'POST') {
203
+ return link === null
204
+ ? page({ state: 'invalid' }, 404, method === 'HEAD')
205
+ : page({ state: 'ask', category: link.category, token }, 200, method === 'HEAD');
206
+ }
207
+ const choice = await choiceOf(request);
208
+ if (link === null)
209
+ return choice === null ? blank() : page({ state: 'invalid' }, 404, false);
210
+ const day = dayOf(now());
211
+ await suppressions.suppress(link.scope, link.addressHash, 'manual', day);
212
+ const all = link.category === null || choice === 'all';
213
+ if (all && link.scope !== unsubscribe_js_1.OPTIONAL_MAIL_SCOPE)
214
+ await suppressions.suppress(unsubscribe_js_1.OPTIONAL_MAIL_SCOPE, link.addressHash, 'manual', day);
215
+ return choice === null ? blank() : page({ state: 'done', category: link.category, all }, 200, false);
216
+ };
217
+ }
218
+ //# sourceMappingURL=unsubscribeHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribeHandler.js","sourceRoot":"","sources":["../src/unsubscribeHandler.ts"],"names":[],"mappings":"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"}
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
3
3
  "id": "com.framers.channels.email",
4
4
  "name": "Email Channel Extension",
5
- "version": "0.6.3",
5
+ "version": "0.6.4",
6
6
  "description": "Bidirectional email integration: send through SMTP or Resend's HTTPS API; read, search, extract verification codes, reply, move, label, flag and delete through IMAP; keep a suppression record and unsubscribe tokens on a SQL database.",
7
7
  "categories": ["channel", "communication"],
8
8
  "platforms": ["email"],
@@ -24,6 +24,7 @@
24
24
  "Extract verification codes and OTPs from email bodies",
25
25
  "Reply to email threads maintaining conversation context",
26
26
  "Read every recipient, the threading and authentication headers, bounded text and HTML parts and attachments after a UID; move, label, flag and delete one message by UID",
27
- "Keep a suppression record and opaque unsubscribe tokens, stored as hashes, on a SQL database"
27
+ "Keep a suppression record and opaque unsubscribe tokens, stored as hashes, on a SQL database",
28
+ "Put an unsubscribe link and RFC 8058 one-click fields on every mail, read the suppression record before each send, and answer the link: a GET asks, a POST suppresses"
28
29
  ]
29
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@framers/agentos-ext-channel-email",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Email channel adapter for AgentOS: send through SMTP or Resend's HTTPS API; read, search, extract codes, reply, move, label, flag and delete through IMAP; keep a suppression record and unsubscribe tokens on a SQL database.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,11 +22,12 @@
22
22
  "nodemailer": "^10.0.16"
23
23
  },
24
24
  "devDependencies": {
25
- "@framers/sql-storage-adapter": "^0.8.3",
25
+ "@framers/sql-storage-adapter": "^0.9.2",
26
26
  "@types/node": "^20.0.0",
27
+ "parse5": "^8.0.1",
27
28
  "typescript": "^6.0.3",
28
29
  "vitest": "^3.2.7",
29
- "@framers/agentos": "^0.15.77"
30
+ "@framers/agentos": "^0.15.105"
30
31
  },
31
32
  "repository": {
32
33
  "type": "git",
@@ -37,7 +38,7 @@
37
38
  "bugs": {
38
39
  "url": "https://github.com/framerslab/agentos-extensions/issues"
39
40
  },
40
- "gitHead": "ca945fe0c72b6e3fe8599ddd2396ab1813681b04",
41
+ "gitHead": "fabef244aaa44de2e031567f3bdaafb162bfa32c",
41
42
  "scripts": {
42
43
  "build": "tsc",
43
44
  "dev": "tsc --watch",