@nxgt/janus-mail 0.2.1 → 0.3.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 (40) hide show
  1. package/README.md +46 -12
  2. package/dist/generated/mail.d.ts +10 -0
  3. package/dist/generated/mail.d.ts.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +16 -11
  6. package/dist/index.js.map +5 -5
  7. package/dist/notice-mails.d.ts +5 -5
  8. package/dist/notice-mails.d.ts.map +1 -1
  9. package/dist/options.d.ts.map +1 -1
  10. package/dist/templates.d.ts +1 -1
  11. package/dist/templates.d.ts.map +1 -1
  12. package/dist/types.d.ts +33 -3
  13. package/dist/types.d.ts.map +1 -1
  14. package/docs/README.md +3 -3
  15. package/docs/guide/building.md +7 -5
  16. package/docs/guide/locales.md +5 -3
  17. package/docs/guide/sending.md +65 -0
  18. package/docs/guide/templates.md +29 -9
  19. package/docs/roadmap.md +24 -1
  20. package/docs/troubleshooting.md +57 -7
  21. package/mails/en/email-changed.html +23 -13
  22. package/mails/en/password-changed.html +23 -13
  23. package/mails/en/reset-password.html +21 -12
  24. package/mails/en/sign-in-code.html +21 -10
  25. package/mails/en/two-factor-disabled.html +51 -0
  26. package/mails/en/two-factor-disabled.txt +21 -0
  27. package/mails/en/two-factor-enabled.html +51 -0
  28. package/mails/en/two-factor-enabled.txt +21 -0
  29. package/mails/en/verify-email.html +21 -12
  30. package/mails/fr/email-changed.html +24 -14
  31. package/mails/fr/password-changed.html +23 -13
  32. package/mails/fr/reset-password.html +22 -12
  33. package/mails/fr/sign-in-code.html +19 -10
  34. package/mails/fr/two-factor-disabled.html +51 -0
  35. package/mails/fr/two-factor-disabled.txt +21 -0
  36. package/mails/fr/two-factor-enabled.html +51 -0
  37. package/mails/fr/two-factor-enabled.txt +21 -0
  38. package/mails/fr/verify-email.html +22 -12
  39. package/mails/mail-manifest.json +48 -0
  40. package/package.json +8 -8
@@ -178,6 +178,33 @@ through JSON on its way to a queue holds a string — or is already past, is
178
178
  a `TypeError`, and nothing reaches the mailer: revive the date with
179
179
  `new Date(issued.expiresAt)` when the job runs, or pass `expiresIn`.
180
180
 
181
+ ### `twoFactorEnabled(to)` and `twoFactorDisabled(to)`
182
+
183
+ ```ts
184
+ // On the user events @nxgt/janus sends once the write landed:
185
+ const auth = janus({
186
+ ...config,
187
+ async events(event) {
188
+ if (event.type !== 'user.secondFactorEnabled' && event.type !== 'user.secondFactorDisabled') return;
189
+ const user = await auth.get(event.userId);
190
+ const to = { name: user.name, locale: user.locale, email: user.email };
191
+ await (event.type === 'user.secondFactorEnabled' ? mail.twoFactorEnabled(to) : mail.twoFactorDisabled(to));
192
+ },
193
+ });
194
+ ```
195
+
196
+ Two notices: two-factor authentication was turned on, or off, for the
197
+ account at `to.email`, each with a link to `links.secureAccount()` — the
198
+ user's security settings — for a user who did not make the change. Send
199
+ `twoFactorEnabled` on the `user.secondFactorEnabled` event, which
200
+ `auth.secondFactor.activate` sends once the factor is active, and
201
+ `twoFactorDisabled` on `user.secondFactorDisabled`, which
202
+ `auth.secondFactor.disable` sends only when it removed an active factor: a
203
+ `disable` on a user who had none tells nobody anything. The event names the
204
+ user by id alone, so read the name, the locale and the address from the
205
+ user. Calling them right after `activate` or `disable` answered works as
206
+ well — without the events, check `hasSecondFactor` before the `disable`.
207
+
181
208
  ## Where each e-mail goes
182
209
 
183
210
  | Method | To | Never to |
@@ -187,6 +214,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
187
214
  | `signInCode` | `issued.email` | the `email` passed to `request` |
188
215
  | `passwordChanged` | `to.email` | — |
189
216
  | `emailChanged` | `to.formerEmail` | `to.newEmail` |
217
+ | `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
190
218
 
191
219
  `issued.email` is the address the user's record holds, as they registered
192
220
  it; what the visitor typed may differ in case or spacing. The recipient is a
@@ -238,6 +266,43 @@ advises sending *after* answering, so the time an answer takes does not tell
238
266
  whether the address exists: hand the send to a queue that awaits it and
239
267
  retries a `MAIL_FAILED`, rather than to a promise nobody awaits.
240
268
 
269
+ ### Retrying and tracing the mailer
270
+
271
+ From `@nxgt/mail` 0.8, wrap the mailer before you hand it to `janusMail()`:
272
+ `withRetry` retries a `MailFailure` with backoff, reusing one idempotency
273
+ key for every attempt, and never retries a `MailRefused`; `withTelemetry`
274
+ from `@nxgt/mail/telemetry` opens a `mail.send` span per send. That entry
275
+ needs `@opentelemetry/api` installed (an optional peer, loaded by it alone);
276
+ with no OpenTelemetry SDK registered, its spans are no-ops. Put telemetry on the outside, so one
277
+ send is one span:
278
+
279
+ ```ts
280
+ import { janusMail } from '@nxgt/janus-mail';
281
+ import { withRetry } from '@nxgt/mail';
282
+ import { withTelemetry as withMailTelemetry } from '@nxgt/mail/telemetry';
283
+ import { createSmtpMailer } from '@nxgt/mail-smtp';
284
+
285
+ export const mail = janusMail({
286
+ mailer: withMailTelemetry(withRetry(createSmtpMailer({ transporter })), { transport: 'smtp' }),
287
+ from: 'noreply@acme.example',
288
+ brand: 'Acme',
289
+ links, // transporter and links: as in the example at the top
290
+ });
291
+ ```
292
+
293
+ **Alias it.** `@nxgt/telemetry` — which `@nxgt/janus-telemetry` peers — also
294
+ exports a `withTelemetry`, `withTelemetry(telemetry, fn)`, which scopes a
295
+ telemetry to a block. The two are unrelated: `mail.send` is recorded
296
+ through OpenTelemetry, not `@nxgt/telemetry`, so it does not nest under
297
+ `@nxgt/janus-telemetry`'s spans. Import `@nxgt/mail`'s under another name,
298
+ as above, when a module uses both.
299
+
300
+ **SMTP ignores the idempotency key**, so a retry after an ambiguous SMTP
301
+ timeout can deliver an e-mail twice. If a duplicate sign-in code or reset
302
+ link is not acceptable, leave an SMTP mailer unretried (`{ attempts: 1 }`,
303
+ or no `withRetry`). See `@nxgt/mail`'s
304
+ [sending guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/sending.md#retrying--withretry).
305
+
241
306
  ## Testing
242
307
 
243
308
  `createMemoryMailer()` from `@nxgt/mail` keeps an outbox, and can be told to
@@ -1,7 +1,7 @@
1
1
  # Templates
2
2
 
3
3
  This page is for changing what an e-mail says or looks like: what a template
4
- is given and answers, replacing some of the five, using the defaults on their
4
+ is given and answers, replacing some of the seven, using the defaults on their
5
5
  own, and what the defaults say.
6
6
 
7
7
  ```ts
@@ -22,7 +22,7 @@ const mail = janusMail({
22
22
  });
23
23
  ```
24
24
 
25
- The sign-in code e-mail is now yours; the four others stay the defaults.
25
+ The sign-in code e-mail is now yours; the six others stay the defaults.
26
26
 
27
27
  ## What a template is
28
28
 
@@ -64,10 +64,12 @@ e-mail.
64
64
  | `signInCode` | `brand`, `code`, `expiresIn` | `mail.signInCode(issued, to?, options?)` |
65
65
  | `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
66
66
  | `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
67
+ | `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
68
+ | `twoFactorDisabled` | `brand`, `name`, `link` | `mail.twoFactorDisabled(to)` |
67
69
 
68
70
  Every one also gets `locale`, one of `locales`. Every value is a string:
69
71
  `brand` from the options, `name` from the recipient, `link` from `links` —
70
- `links.secureAccount()` for the two notices — `code` and `newEmail` from the
72
+ `links.secureAccount()` for the four notices — `code` and `newEmail` from the
71
73
  call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
72
74
  the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
73
75
 
@@ -101,11 +103,11 @@ methods live on its prototype and are refused — see
101
103
  [troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
102
104
 
103
105
  `templates` is `Partial` while `locales` stays within `en` and `fr`: give
104
- any of the five, and the defaults render the rest. Once `locales` holds
105
- another locale, it takes **all five** — see
106
+ any of the seven, and the defaults render the rest. Once `locales` holds
107
+ another locale, it takes **all seven** — see
106
108
  [Adding a locale](locales.md#adding-a-locale).
107
109
 
108
- `mail.templates` answers the five in use — yours, and the defaults for the
110
+ `mail.templates` answers the seven in use — yours, and the defaults for the
109
111
  rest — frozen:
110
112
 
111
113
  ```ts
@@ -127,7 +129,7 @@ const { subject, html, text } = await defaults.resetPassword({
127
129
  });
128
130
  ```
129
131
 
130
- It answers the five defaults, typed for `'en' | 'fr'`, without a mailer:
132
+ It answers the seven defaults, typed for `'en' | 'fr'`, without a mailer:
131
133
  to preview an e-mail, to wrap one — add a line to the default text, say — or
132
134
  to send one through something other than `janusMail()`. A default reused for
133
135
  a wider set of locales is a compile error: it could not render the one it
@@ -153,7 +155,7 @@ them renders: that is when the package's `mails/` folder is read, once.
153
155
  ## What the defaults say
154
156
 
155
157
  The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
156
- 0.3.0, built with a neutral grey theme and no logo:
158
+ 0.4.1, built with a neutral grey theme and no logo:
157
159
 
158
160
  | Template | Built from | Subject (`en`) | Subject (`fr`) |
159
161
  | --- | --- | --- | --- |
@@ -162,16 +164,34 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
162
164
  | `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
163
165
  | `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
164
166
  | `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
167
+ | `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
168
+ | `twoFactorDisabled` | `two-factor-disabled` | Two-factor authentication was turned off | L'authentification à deux facteurs a été désactivée |
165
169
 
166
170
  Each has a header and a footer with the brand, a heading, a greeting by name
167
171
  (but `signInCode`), a button with its link and the link again in text for a
168
- client that shows no button, and a text part. The two notices add a warning:
172
+ client that shows no button, and a text part. The four notices add a warning:
169
173
  *if this was not you, secure your account now*.
170
174
 
171
175
  `verifyEmail`, `resetPassword` and `signInCode` also say how long the link
172
176
  or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
173
177
  10 minutes." The text part keeps each paragraph on one line.
174
178
 
179
+ **The HTML follows dark mode.** Each HTML part declares
180
+ `color-scheme: light dark`. A client that reads `prefers-color-scheme`, or
181
+ Outlook's `[data-ogsc]`, shows a dark page, a dark card and light text to a
182
+ reader in dark mode; Gmail always shows the light e-mail. The dark palette
183
+ is `@nxgt/mail-ui`'s default, a deep navy, but for the primary: the button
184
+ is near-black (`#27272a`) with light text in light mode, and near-white
185
+ (`#fafafa`) with near-black text (`#18181b`) in dark mode — 17:1 against the
186
+ dark card, where the light primary would be 1.2:1. The page behind the card
187
+ follows it, 5% of the dark primary over the dark background, so it sits
188
+ close to the card, which its border outlines. The sign-in code's box is
189
+ `@nxgt/mail-ui`'s muted grey, which has no dark value: it stays a light box
190
+ with dark text in both modes. Nothing is passed for any of it, the text part
191
+ has no colours, and a template of your own gets none of it. `@nxgt/mail-ui`'s
192
+ [Dark mode](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/dark-mode.md)
193
+ guide has the technique, client by client.
194
+
175
195
  **The brand is text.** It is written in the header, the body and the footer,
176
196
  escaped — no logo and no link, which would need absolute URLs known when the
177
197
  package is built. A logo takes a template of your own; the
package/docs/roadmap.md CHANGED
@@ -13,6 +13,9 @@ Nothing between releases.
13
13
  `invitation` from `@nxgt/mail-presets`, each once `@nxgt/janus` has a flow
14
14
  that sends it: a sign-in link, a sign-in from a new device, a user created,
15
15
  an invitation to a user type.
16
+ - **The other presets of `@nxgt/mail-presets` 0.4.0** — `account-deleted`,
17
+ after a user is deleted, once `@nxgt/janus` has a deletion a link can
18
+ undo; and `invitation-accepted`, once it has invitations.
16
19
 
17
20
  ## Later
18
21
 
@@ -32,7 +35,8 @@ Nothing between releases.
32
35
  `MailFailure` or `MailRefused`, untouched, so one `instanceof` against
33
36
  `@nxgt/mail` answers every failure, whichever package sent the e-mail.
34
37
  - **Retries or a queue** — a retry is the application's decision, made where
35
- it can see it; hand a send to a queue that retries a `MAIL_FAILED`.
38
+ it can see it; hand a send to a queue that retries a `MAIL_FAILED`, or pass
39
+ a mailer wrapped in `@nxgt/mail` 0.8's `withRetry`.
36
40
  - **Sending from inside `janus()`** — `@nxgt/janus` stays free of e-mail:
37
41
  its flows answer what to send, and this package is called with that answer.
38
42
  An application that sends its own way needs neither this package nor a
@@ -44,6 +48,25 @@ Nothing between releases.
44
48
 
45
49
  Newest first; from the first release on, the package's CHANGELOG holds every one.
46
50
 
51
+ - **A button that shows in dark mode, v0.3.1.** Under dark mode the button
52
+ is near-white with near-black text, no longer a near-black one that all but
53
+ vanished on the dark card; light mode, the text parts, subjects and
54
+ variables are unchanged. Built from `@nxgt/mail-ui` 0.5.0,
55
+ `@nxgt/mail-presets` 0.4.1 and `@nxgt/mail-i18n` 0.6.0; the manifest is
56
+ still format 1, and the `@nxgt/mail` peer stays `>=0.1.0 <1`.
57
+ - **The two-factor notices, v0.3.0.** `twoFactorEnabled(to)` and
58
+ `twoFactorDisabled(to)`, built from `@nxgt/mail-presets` 0.4.0's
59
+ `two-factor-enabled` and `two-factor-disabled` in English and French, each
60
+ linking to `links.secureAccount()`: send them on `@nxgt/janus` 0.9's
61
+ `user.secondFactorEnabled` and `user.secondFactorDisabled` events. Seven
62
+ e-mails now, and seven templates to pass for a locale beyond `en` and `fr`.
63
+ - **Dark mode, v0.3.0.** The e-mails follow the reader's dark mode in
64
+ every client that supports it, Gmail excepted: a dark page, a dark card and
65
+ light text. Built from `@nxgt/mail-ui` 0.4.0, `@nxgt/mail-presets` 0.4.0
66
+ and `@nxgt/mail-i18n` 0.5.0; the text parts, subjects and variables are
67
+ unchanged, the manifest is still format 1, and the `@nxgt/mail` peer stays
68
+ `>=0.1.0 <1`.
69
+
47
70
  - **The expiry in the e-mail, v0.2.0.** The verification, reset and sign-in
48
71
  code e-mails say how long the link or code lasts — "1 hour", "1 heure" —
49
72
  derived from the flow's `expiresAt` in the recipient's locale, measured
@@ -32,7 +32,7 @@ How the messages are shaped:
32
32
  - [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
33
33
  - [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
34
34
  - [`janusMail: templates must be an object of functions, as { verifyEmail: (variables) => rendered }`](#janusmail-templates-must-be-an-object-of-functions-as--verifyemail-variables--rendered-)
35
- - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged)
35
+ - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled)
36
36
  - [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
37
37
  - [`janusMail: templates.<name> is not an own enumerable property — pass a plain object, as { <name>: (variables) => rendered }`](#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-)
38
38
  - [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
@@ -51,6 +51,8 @@ How the messages are shaped:
51
51
  - [`MAIL_FAILED` — `MailFailure`](#mail_failed--mailfailure)
52
52
  - [`createMailRenderer: …/mails/mail-manifest.json cannot be read — run maizzle build, and deploy its output folder`](#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder)
53
53
  - [`Could not resolve "node:fs"` on an edge runtime](#could-not-resolve-nodefs-on-an-edge-runtime)
54
+ - [An e-mail stays light in dark mode](#an-e-mail-stays-light-in-dark-mode)
55
+ - [The button all but vanishes in dark mode](#the-button-all-but-vanishes-in-dark-mode)
54
56
 
55
57
  **Compile errors**
56
58
  - [`TS2345: Argument of type '(IssuedToken & { user: … }) | null' is not assignable to parameter of type 'IssuedToken'.`](#ts2345-argument-of-type-issuedtoken---user----null-is-not-assignable-to-parameter-of-type-issuedtoken)
@@ -149,9 +151,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
149
151
 
150
152
  `templates` given as a list or a function. Key each template by its name.
151
153
 
152
- ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged`
154
+ ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled`
153
155
 
154
- A key that is not one of the five — `welcome`, or the e-mail's file name
156
+ A key that is not one of the seven — `welcome`, or the e-mail's file name
155
157
  `'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
156
158
  on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
157
159
 
@@ -185,7 +187,7 @@ as it is: fields are own properties.
185
187
  ### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
186
188
 
187
189
  `locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
188
- out: the defaults could not render them in that locale. Pass all five — see
190
+ out: the defaults could not render them in that locale. Pass all seven — see
189
191
  [Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
190
192
  The message lists the templates missing.
191
193
 
@@ -223,6 +225,15 @@ string` is the third argument's `expiresIn` given as something else — a
223
225
  number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
224
226
  leave it out.
225
227
 
228
+ `janusMail.twoFactorEnabled: email must be a string` (or `twoFactorDisabled`)
229
+ is the user event itself given to the notice: an event names the user by id
230
+ alone. Read the user first:
231
+
232
+ ```ts
233
+ const user = await auth.get(event.userId);
234
+ await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
235
+ ```
236
+
226
237
  ### `janusMail.<method>: expiresAt must be a Date`
227
238
 
228
239
  A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the e-mail
@@ -354,7 +365,10 @@ try {
354
365
  }
355
366
  ```
356
367
 
357
- This package retries nothing: a retry is your decision.
368
+ This package retries nothing: a retry is your decision. To retry in
369
+ process, pass a mailer wrapped in `@nxgt/mail` 0.8's `withRetry`; once its
370
+ attempts run out it rejects with the last `MailFailure`, `attempts` on it —
371
+ see [Retrying and tracing the mailer](guide/sending.md#retrying-and-tracing-the-mailer).
358
372
 
359
373
  ### `createMailRenderer: …/mails/mail-manifest.json cannot be read — run maizzle build, and deploy its output folder`
360
374
 
@@ -369,7 +383,43 @@ avoids the read altogether. (In this repository: run `bun run build`.)
369
383
 
370
384
  The default templates read `mails/` with `node:fs`, through
371
385
  `@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
372
- five templates yourself; an inlined build is on the [roadmap](roadmap.md).
386
+ seven templates yourself; an inlined build is on the [roadmap](roadmap.md).
387
+
388
+ ### An e-mail stays light in dark mode
389
+
390
+ Two causes, neither an error:
391
+
392
+ - **Gmail.** Its web and mobile clients cannot be targeted from CSS and
393
+ always show the light e-mail. Every other client that reads
394
+ `prefers-color-scheme`, or Outlook's `[data-ogsc]`, shows the dark one.
395
+ - **A template of your own.** The dark rules are in the default HTML, built
396
+ by `@nxgt/mail-ui`. An override that returns its own `html` has none of
397
+ them; build it with `@nxgt/mail-ui` 0.4.0 or later to follow dark mode:
398
+
399
+ ```ts
400
+ janusMail({ mailer, from, brand, links, templates: { verifyEmail: myVerifyEmail } });
401
+ // myVerifyEmail's html needs its own color-scheme and dark rules
402
+ ```
403
+
404
+ ### The button all but vanishes in dark mode
405
+
406
+ A template of your own, built with a near-black primary, shows a button
407
+ that melts into the dark card for a reader in dark mode; the defaults turn
408
+ theirs near-white there since 0.3.1. `@nxgt/mail-ui`'s primary keeps its
409
+ light value under dark mode unless the build gives it a dark one — `#27272a`
410
+ on the dark card is 1.2:1. Build the override with `@nxgt/mail-ui` 0.5.0 or
411
+ later and set the dark pair in its `theme`, as the defaults do:
412
+
413
+ ```ts
414
+ ui({
415
+ brand: { name: 'Acme' },
416
+ theme: {
417
+ 'color-primary': '#27272a',
418
+ 'color-primary-dark': '#fafafa', // 17:1 on the dark card
419
+ 'color-primary-foreground-dark': '#18181b', // the button's text, in dark mode
420
+ },
421
+ });
422
+ ```
373
423
 
374
424
  ## Compile errors
375
425
 
@@ -399,7 +449,7 @@ whole.
399
449
  ### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
400
450
 
401
451
  `locales` holds a locale beyond `en` and `fr`, and `templates` does not give
402
- all five. See [Adding a locale](guide/locales.md#adding-a-locale).
452
+ all seven. See [Adding a locale](guide/locales.md#adding-a-locale).
403
453
 
404
454
  ### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
405
455
 
@@ -1,37 +1,47 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
2
+ <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="color-scheme:light dark;">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="x-apple-disable-message-reformatting">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <meta name="color-scheme" content="light">
8
- <meta name="supported-color-schemes" content="light">
7
+ <meta name="color-scheme" content="light dark">
8
+ <meta name="supported-color-schemes" content="light dark">
9
+ <style>[data-ogsc] .nx-dark-bg-paper{background-color:#0f1523!important;}[data-ogsb] .nx-dark-bg-paper{background-color:#0f1523!important;}[data-ogsc] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsb] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsc] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686
10
+ -8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
11
+ [data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}
12
+ [data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#dfdfdf!important;}[data-ogsb] .nx-dark-bg-primary-15{
13
+ background-color:#dfdfdf!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#d4d4d4!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#d4d4d4!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#a9a9aa!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#a9a9aa!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#939395!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#939395!important;}[data-ogsc] .nx-light-only{display:
14
+ none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
15
+ @media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#0f1523!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
16
+ !important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
17
+ </style>
9
18
  </head>
10
- <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;"><div style="display:none;">Your account now signs in with a new address.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
19
+ <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">Your account now signs in with a new address.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
11
20
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
12
21
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
13
22
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
14
23
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
15
24
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
16
25
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
17
- &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &nbsp;</div><span style="display:none">
26
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &nbsp;</div><span style="display:none">
18
27
  <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings><w:WordDocument><w:DontUseAdvancedTypographyReadingMail/></w:WordDocument></xml>
19
28
  <![endif]--></span><div role="article" aria-roledescription="email" lang="en" dir="ltr" style="font-size:medium;font-size:max(16px,1rem)">
20
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
29
+ <table class="nx-dark-bg-paper" role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
21
30
  <!--[if mso]>
22
31
  <table role="none" cellpadding="0" cellspacing="0" style="width:600px" align="center"><tr><td>
23
32
  <![endif]--><div style="margin:0 auto;max-width:600px;">
24
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
25
- </td></tr><tr><td style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);"><h1 style="margin:0 0 16px;font-size:24px;line-height:32px;
26
- font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your e-mail address was changed</h1><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Hello {{ name }},</p><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your {{ brand }} account now uses {{ newEmail }}. We send this notice to your former address.</p>
27
- <table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:16px;width:100%;"><tr><td style="border-width:0;border-left-style:solid;border-left-width:8px;border-style:solid;border-color:#f05100;border-color:lab(57.1026% 64.2584 89.8886);background-color:#fef6f2;padding:16px;">
28
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;"><tr><td style="vertical-align:top;"><p style="margin:0;font-size:14px;line-height:20px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If this was not you, secure your account now.</p>
33
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span class="nx-dark-text-foreground" style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
34
+ </td></tr><tr><td class="nx-dark-border-border nx-dark-bg-card nx-dark-text-card-foreground" style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,
35
+ 0,0,.1);"><h1 class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your e-mail address was changed</h1><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Hello {{ name }},</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686
36
+ -8.51797);">Your {{ brand }} account now uses {{ newEmail }}. We send this notice to your former address.</p>
37
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="margin-bottom:16px;width:100%;"><tr><td style="border-width:0;border-left-style:solid;border-left-width:8px;border-style:solid;border-color:#f05100;border-color:lab(57.1026% 64.2584 89.8886);background-color:#fef6f2;padding:16px;">
38
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="vertical-align:top;"><p style="margin:0;font-size:14px;line-height:20px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If this was not you, secure your account now.</p>
29
39
  </td></tr></table>
30
- </td></tr></table><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}">
40
+ </td></tr></table><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}" class="nx-dark-bg-primary nx-dark-text-primary-foreground">
31
41
  <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
32
42
  <![endif]--> <span style="mso-text-raise:10px;">Undo this change</span>
33
43
  <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
34
- <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
44
+ <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
35
45
  </td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
36
46
  </td></tr></table></div>
37
47
  <!--[if mso]>
@@ -1,37 +1,47 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
2
+ <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="color-scheme:light dark;">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="x-apple-disable-message-reformatting">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <meta name="color-scheme" content="light">
8
- <meta name="supported-color-schemes" content="light">
7
+ <meta name="color-scheme" content="light dark">
8
+ <meta name="supported-color-schemes" content="light dark">
9
+ <style>[data-ogsc] .nx-dark-bg-paper{background-color:#0f1523!important;}[data-ogsb] .nx-dark-bg-paper{background-color:#0f1523!important;}[data-ogsc] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsb] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsc] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686
10
+ -8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
11
+ [data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}
12
+ [data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#dfdfdf!important;}[data-ogsb] .nx-dark-bg-primary-15{
13
+ background-color:#dfdfdf!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#d4d4d4!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#d4d4d4!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#a9a9aa!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#a9a9aa!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#939395!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#939395!important;}[data-ogsc] .nx-light-only{display:
14
+ none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
15
+ @media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#0f1523!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
16
+ !important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
17
+ </style>
9
18
  </head>
10
- <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;"><div style="display:none;">The password of your account was just changed.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
19
+ <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">The password of your account was just changed.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
11
20
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
12
21
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
13
22
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
14
23
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
15
24
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
16
25
  &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
17
- &#8199;&#65279;&#847; &#8199;&#65279;&#847; &nbsp;</div><span style="display:none">
26
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &nbsp;</div><span style="display:none">
18
27
  <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings><w:WordDocument><w:DontUseAdvancedTypographyReadingMail/></w:WordDocument></xml>
19
28
  <![endif]--></span><div role="article" aria-roledescription="email" lang="en" dir="ltr" style="font-size:medium;font-size:max(16px,1rem)">
20
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
29
+ <table class="nx-dark-bg-paper" role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
21
30
  <!--[if mso]>
22
31
  <table role="none" cellpadding="0" cellspacing="0" style="width:600px" align="center"><tr><td>
23
32
  <![endif]--><div style="margin:0 auto;max-width:600px;">
24
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
25
- </td></tr><tr><td style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);"><h1 style="margin:0 0 16px;font-size:24px;line-height:32px;
26
- font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your password was changed</h1><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Hello {{ name }},</p><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">The password of your {{ brand }} account was just changed.</p>
27
- <table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:16px;width:100%;"><tr><td style="border-width:0;border-left-style:solid;border-left-width:8px;border-style:solid;border-color:#f05100;border-color:lab(57.1026% 64.2584 89.8886);background-color:#fef6f2;padding:16px;">
28
- <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;"><tr><td style="vertical-align:top;"><p style="margin:0;font-size:14px;line-height:20px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If this was not you, secure your account now.</p>
33
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span class="nx-dark-text-foreground" style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
34
+ </td></tr><tr><td class="nx-dark-border-border nx-dark-bg-card nx-dark-text-card-foreground" style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,
35
+ 0,0,.1);"><h1 class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your password was changed</h1><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Hello {{ name }},</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686
36
+ -8.51797);">The password of your {{ brand }} account was just changed.</p>
37
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="margin-bottom:16px;width:100%;"><tr><td style="border-width:0;border-left-style:solid;border-left-width:8px;border-style:solid;border-color:#f05100;border-color:lab(57.1026% 64.2584 89.8886);background-color:#fef6f2;padding:16px;">
38
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="vertical-align:top;"><p style="margin:0;font-size:14px;line-height:20px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If this was not you, secure your account now.</p>
29
39
  </td></tr></table>
30
- </td></tr></table><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}">
40
+ </td></tr></table><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}" class="nx-dark-bg-primary nx-dark-text-primary-foreground">
31
41
  <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
32
42
  <![endif]--> <span style="mso-text-raise:10px;">Secure my account</span>
33
43
  <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
34
- <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
44
+ <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
35
45
  </td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
36
46
  </td></tr></table></div>
37
47
  <!--[if mso]>