@ultimat3/mail 22.5.1 → 22.6.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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ setMailTransform(async (rendered, meta) =>
58
58
  | Every colour is a token | `MAIL_TOKENS` in `layout.ts` holds light + dark hexes; templates never see a hex |
59
59
  | Every date takes an IANA zone | `options.tz`, else `ctx.tz`, else `UTC` |
60
60
  | No CR/LF in a header-bound field | checked in `renderMessage` and again in `sendMailJob`, so every driver refuses the same message (`X_MAIL_HEADER_INVALID`). `mime.ts` keeps its own gate for the headers the SMTP transport mints itself |
61
- | `unsubscribeUrl` is one-click unless you say otherwise | it emits `List-Unsubscribe: <url>` plus `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058), a promise that a POST to that URL unsubscribes. When the URL is a confirm page — GET shows a button and must never unsubscribe, because scanners prefetch — pass `unsubscribeOneClick: false`: the `-Post` line goes, `List-Unsubscribe` and the footer link stay |
61
+ | `unsubscribeUrl` is one-click, and one-click is the supported path | it emits `List-Unsubscribe: <url>` plus `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058), and the footer links the SAME url. Make that url one page that answers both: `GET` renders a confirm button and never unsubscribes (scanners prefetch), and `POST` — the mail client's one-click, or the button's own form — runs the action: `defineRoute({ render: 'ssr', …, post: 'unsubscribe' })` (`@ultimat3/render`) binds it, with the url's query (`?t=<token>`) merged into the input. `unsubscribeOneClick: false` drops the `-Post` line for a url that still cannot take a POST; it is the fallback, not the path |
62
62
  | Sending is a job | `retry: { attempts: 5, backoff: 'exponential' }`, idempotency key `mail:<mailId>:<hash(recipients + rendered)>` — 128 bits, ASCII, under Resend's 256-character limit at any recipient count — or `(mailId, your key)` when you pass one (digested if it is not a short ASCII token) — a caller's key is scoped to its mail so two templates cannot dedupe each other away |
63
63
 
64
64
  ## Drivers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/mail",
3
- "version": "22.5.1",
3
+ "version": "22.6.1",
4
4
  "description": "Transactional email as data: one template renders HTML and text, sent through a job.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/core": "22.5.1",
35
- "@ultimat3/i18n": "22.5.1",
36
- "@ultimat3/jobs": "22.5.1",
37
- "@ultimat3/schema": "22.5.1",
38
- "@ultimat3/time": "22.5.1"
34
+ "@ultimat3/core": "22.6.1",
35
+ "@ultimat3/i18n": "22.6.1",
36
+ "@ultimat3/jobs": "22.6.1",
37
+ "@ultimat3/schema": "22.6.1",
38
+ "@ultimat3/time": "22.6.1"
39
39
  }
40
40
  }
package/src/driver.ts CHANGED
@@ -28,9 +28,11 @@ export interface MailMessage {
28
28
  readonly bcc?: readonly string[] | undefined;
29
29
  readonly unsubscribeUrl?: string | undefined;
30
30
  /**
31
- * `false` drops `List-Unsubscribe-Post` and keeps the GET-only `List-Unsubscribe`: for an
32
- * `unsubscribeUrl` that is a confirm page, which cannot honour RFC 8058's promise that a POST to
33
- * it unsubscribes. Absent means `true` — one-click, what Gmail and Yahoo require of bulk senders.
31
+ * Absent means `true` — one-click, what Gmail and Yahoo require of bulk senders, and the
32
+ * supported path: the url is a page whose `GET` confirms and whose `POST` unsubscribes
33
+ * (`defineRoute({ post: '<action>' })` in `@ultimat3/render`). `false` drops
34
+ * `List-Unsubscribe-Post` and keeps the GET-only `List-Unsubscribe`, for a url that cannot yet
35
+ * honour RFC 8058's promise that a POST to it unsubscribes.
34
36
  */
35
37
  readonly unsubscribeOneClick?: boolean | undefined;
36
38
  readonly idempotencyKey?: string | undefined;
package/src/mail.ts CHANGED
@@ -46,8 +46,10 @@ export interface SendOptions {
46
46
  readonly unsubscribeUrl?: string | undefined;
47
47
  /**
48
48
  * Default `true`: `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058) rides with
49
- * `List-Unsubscribe`. `false` when `unsubscribeUrl` is a GET confirm page that cannot take the
50
- * one-click POST — the header and the footer link stay, the POST promise goes.
49
+ * `List-Unsubscribe`, and it is the supported path — one url for the header and the footer, a
50
+ * page whose `GET` confirms and whose `POST` unsubscribes (`defineRoute({ post: '<action>' })`).
51
+ * `false` is the fallback for a url that cannot take the POST: the header and the footer link
52
+ * stay, the POST promise goes.
51
53
  */
52
54
  readonly unsubscribeOneClick?: boolean | undefined;
53
55
  readonly idempotencyKey?: string | undefined;