@nxgt/janus-mail 0.4.1 → 0.5.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.
- package/README.md +78 -25
- package/dist/codes-left.d.ts +6 -0
- package/dist/codes-left.d.ts.map +1 -0
- package/dist/generated/codes-left.d.ts +3 -0
- package/dist/generated/codes-left.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +7 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +130 -21
- package/dist/index.js.map +11 -6
- package/dist/janus-mail.d.ts +1 -0
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/links.d.ts +10 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/recovery-mail.d.ts +5 -0
- package/dist/recovery-mail.d.ts.map +1 -0
- package/dist/refuse.d.ts +3 -0
- package/dist/refuse.d.ts.map +1 -0
- package/dist/template-types.d.ts +15 -2
- package/dist/template-types.d.ts.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +4 -3
- package/docs/guide/building.md +58 -9
- package/docs/guide/locales.md +13 -4
- package/docs/guide/sending.md +61 -7
- package/docs/guide/templates.md +41 -16
- package/docs/roadmap.md +15 -1
- package/docs/troubleshooting.md +114 -21
- package/mails/en/email-changed.html +5 -5
- package/mails/en/password-changed.html +5 -5
- package/mails/en/recovery-code-used.html +53 -0
- package/mails/en/recovery-code-used.txt +23 -0
- package/mails/en/reset-password.html +6 -6
- package/mails/en/sign-in-code.html +3 -3
- package/mails/en/two-factor-disabled.html +5 -5
- package/mails/en/two-factor-enabled.html +5 -5
- package/mails/en/verify-email.html +6 -6
- package/mails/en/welcome.html +5 -5
- package/mails/fr/email-changed.html +5 -5
- package/mails/fr/password-changed.html +5 -5
- package/mails/fr/recovery-code-used.html +52 -0
- package/mails/fr/recovery-code-used.txt +23 -0
- package/mails/fr/reset-password.html +6 -6
- package/mails/fr/sign-in-code.html +3 -3
- package/mails/fr/two-factor-disabled.html +5 -5
- package/mails/fr/two-factor-enabled.html +5 -5
- package/mails/fr/verify-email.html +6 -6
- package/mails/fr/welcome.html +5 -5
- package/mails/mail-manifest.json +26 -0
- package/package.json +11 -10
package/docs/guide/sending.md
CHANGED
|
@@ -27,6 +27,7 @@ export const mail = janusMail({
|
|
|
27
27
|
resetPassword: (token) => `https://acme.example/reset?token=${encodeURIComponent(token)}`,
|
|
28
28
|
secureAccount: () => 'https://acme.example/account/security',
|
|
29
29
|
getStarted: () => 'https://acme.example/',
|
|
30
|
+
recoveryCodes: () => 'https://acme.example/account/recovery-codes', // optional
|
|
30
31
|
},
|
|
31
32
|
});
|
|
32
33
|
```
|
|
@@ -47,6 +48,7 @@ time one is sent, and kept.
|
|
|
47
48
|
| `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
|
|
48
49
|
| `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
|
|
49
50
|
| `links.getStarted` | `() => string` | required | Where a new user starts — your home page, or your sign-in page for an account someone else created: the welcome's **Get started** button links to it |
|
|
51
|
+
| `links.recoveryCodes` | `() => string` | `links.secureAccount` | Where a user regenerates their recovery codes: the recovery code notice links to it, from its **Secure my account** button |
|
|
50
52
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
51
53
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
52
54
|
| `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
|
|
@@ -198,6 +200,54 @@ and the address from the user. With several user types, check
|
|
|
198
200
|
and `signUp` answers as it would have. For a welcome that must arrive, put
|
|
199
201
|
the event in a queue that retries a `MAIL_FAILED`, and send from there.
|
|
200
202
|
|
|
203
|
+
### `recoveryCodeUsed(to, { when, recoveryCodesLeft })`
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// On the user event @nxgt/janus sends once a recovery code is spent:
|
|
207
|
+
const auth = janus({
|
|
208
|
+
...config,
|
|
209
|
+
async events(event) {
|
|
210
|
+
if (event.type !== 'user.recoveryCodeUsed') return;
|
|
211
|
+
const user = await auth.get(event.userId);
|
|
212
|
+
const recoveryCodesLeft = await auth.secondFactor.recoveryCodesLeft(user); // @nxgt/janus 0.11
|
|
213
|
+
if (recoveryCodesLeft === null) return; // the factor was turned off since
|
|
214
|
+
const when = new Intl.DateTimeFormat(user.locale, {
|
|
215
|
+
dateStyle: 'long',
|
|
216
|
+
timeStyle: 'short',
|
|
217
|
+
timeZone: user.timeZone, // a field of yours, when you keep one
|
|
218
|
+
}).format(event.occurredAt);
|
|
219
|
+
await mail.recoveryCodeUsed(
|
|
220
|
+
{ name: user.name, locale: user.locale, email: user.email },
|
|
221
|
+
{ when, recoveryCodesLeft },
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A notice for the account at `to.email`: a recovery code was used — a
|
|
228
|
+
sign-in without the user's phone — at `when`, and how many codes are left,
|
|
229
|
+
with a **Secure my account** button. Send it on the `user.recoveryCodeUsed`
|
|
230
|
+
event, which `auth.secondFactor.recover` sends once the code is spent, even
|
|
231
|
+
when opening the session then fails.
|
|
232
|
+
|
|
233
|
+
- **`when` is text you write**, in the recipient's locale and time zone:
|
|
234
|
+
"28 septembre 2026 à 14:05". The event's `occurredAt` is when the code
|
|
235
|
+
was spent.
|
|
236
|
+
- **`recoveryCodesLeft` is the count, or the sentence.** The event does not
|
|
237
|
+
carry it: `auth.secondFactor.recoveryCodesLeft(user)` reads it after the
|
|
238
|
+
write, the code just spent already out, and answers `null` for a user
|
|
239
|
+
with no active factor — check it, the compiler asks you to. A count is
|
|
240
|
+
written as `@nxgt/mail-presets`' plural in the recipient's locale: "You
|
|
241
|
+
have no recovery codes left.", "You have 1 recovery code left.", "Il vous
|
|
242
|
+
reste 9 codes de récupération." — the build's own catalogue, parsed when
|
|
243
|
+
this package is built, and formatted at send time with the locale's
|
|
244
|
+
plural rules. Text is sent as it is: pass the sentence yourself for
|
|
245
|
+
another wording, or a locale the defaults are not built in, where a count
|
|
246
|
+
is a `TypeError`.
|
|
247
|
+
- **The link is `links.recoveryCodes()`**, where the user regenerates their
|
|
248
|
+
codes; without it, `links.secureAccount()`. `recoveryCodes` is optional,
|
|
249
|
+
so a `links` written for 0.4 still compiles.
|
|
250
|
+
|
|
201
251
|
## The expiry
|
|
202
252
|
|
|
203
253
|
`verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
|
|
@@ -252,6 +302,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
|
252
302
|
| `passwordChanged` | `to.email` | — |
|
|
253
303
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
254
304
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
|
305
|
+
| `recoveryCodeUsed` | `to.email` | — |
|
|
255
306
|
| `welcome` | `to.email` | — |
|
|
256
307
|
|
|
257
308
|
`issued.email` is the address the user's record holds, as they registered
|
|
@@ -307,10 +358,12 @@ retries a `MAIL_FAILED`, rather than to a promise nobody awaits.
|
|
|
307
358
|
### Retrying and tracing the mailer
|
|
308
359
|
|
|
309
360
|
Wrap the mailer before you hand it to `janusMail()`: `withRetry`, from
|
|
310
|
-
`@nxgt/mail` 0.8, retries a `MailFailure` with backoff, reusing one
|
|
311
|
-
idempotency key for every attempt, and never retries a `MailRefused
|
|
312
|
-
|
|
313
|
-
|
|
361
|
+
`@nxgt/mail` 0.8 or later, retries a `MailFailure` with backoff, reusing one
|
|
362
|
+
idempotency key for every attempt, and never retries a `MailRefused` — its
|
|
363
|
+
options are `MailRetryOptions` since 1.0, which removed the older name
|
|
364
|
+
`RetryOptions`; `withMailTelemetry`, from `@nxgt/mail/telemetry` 0.9 or
|
|
365
|
+
later (`withTelemetry` on 0.8, removed in 1.0), opens a `mail.send` span
|
|
366
|
+
per send. That entry
|
|
314
367
|
needs `@opentelemetry/api` installed (an optional peer, loaded by it alone);
|
|
315
368
|
with no OpenTelemetry SDK registered, its spans are no-ops. Put telemetry on the outside, so one
|
|
316
369
|
send is one span:
|
|
@@ -334,9 +387,10 @@ export const mail = janusMail({
|
|
|
334
387
|
which scopes a telemetry to a block. `mail.send` is recorded through
|
|
335
388
|
OpenTelemetry, not `@nxgt/telemetry`, so it does not nest under
|
|
336
389
|
`@nxgt/janus-telemetry`'s spans. `@nxgt/mail` 0.9 renamed its decorator
|
|
337
|
-
`withMailTelemetry` so the two no longer share a name
|
|
338
|
-
`withTelemetry
|
|
339
|
-
|
|
390
|
+
`withMailTelemetry` so the two no longer share a name, and 1.0 removed the
|
|
391
|
+
old `withTelemetry` — with `withRendererTelemetry`, which
|
|
392
|
+
`@nxgt/mail/telemetry` 1.0 names `withMailRendererTelemetry`. On 0.8, import
|
|
393
|
+
its `withTelemetry` under another name when a module uses both.
|
|
340
394
|
|
|
341
395
|
**SMTP ignores the idempotency key**, so a retry after an ambiguous SMTP
|
|
342
396
|
timeout can deliver an e-mail twice. If a duplicate sign-in code or reset
|
package/docs/guide/templates.md
CHANGED
|
@@ -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
|
|
4
|
+
is given and answers, replacing some of the nine, 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
|
|
25
|
+
The sign-in code e-mail is now yours; the eight others stay the defaults.
|
|
26
26
|
|
|
27
27
|
## What a template is
|
|
28
28
|
|
|
@@ -66,15 +66,34 @@ e-mail.
|
|
|
66
66
|
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
67
|
| `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
|
|
68
68
|
| `twoFactorDisabled` | `brand`, `name`, `link` | `mail.twoFactorDisabled(to)` |
|
|
69
|
+
| `recoveryCodeUsed` | `brand`, `name`, `when`, `recoveryCodesLeft`, `link` | `mail.recoveryCodeUsed(to, { when, recoveryCodesLeft })` |
|
|
69
70
|
| `welcome` | `brand`, `name`, `link` | `mail.welcome(to)` |
|
|
70
71
|
|
|
71
72
|
Every one also gets `locale`, one of `locales`. Every value is a string:
|
|
72
73
|
`brand` from the options, `name` from the recipient, `link` from `links` —
|
|
73
|
-
`links.secureAccount()` for the four notices, `links.
|
|
74
|
-
`
|
|
74
|
+
`links.secureAccount()` for the four notices, `links.recoveryCodes()` (else
|
|
75
|
+
`links.secureAccount()`) for `recoveryCodeUsed`, `links.getStarted()` for
|
|
76
|
+
`welcome` — `code`, `newEmail` and `when` from the
|
|
75
77
|
call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
|
|
76
78
|
the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
|
|
77
79
|
|
|
80
|
+
`recoveryCodesLeft` is a **sentence**, not the count: "You have 9 recovery
|
|
81
|
+
codes left." The method writes it from the count with the preset's plural in
|
|
82
|
+
the recipient's locale, or passes the send's own text — so an override for
|
|
83
|
+
a locale of its own is given what the caller wrote:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// `escape` is your HTML escaper: a template of yours escapes what it writes.
|
|
87
|
+
templates: {
|
|
88
|
+
recoveryCodeUsed: ({ name, when, recoveryCodesLeft, link }) => ({
|
|
89
|
+
subject: 'Ein Wiederherstellungscode wurde verwendet',
|
|
90
|
+
html: `<p>Hallo ${escape(name)}, am ${escape(when)}.</p><p>${escape(recoveryCodesLeft)}</p><p><a href="${escape(link)}">Konto sichern</a></p>`,
|
|
91
|
+
text: `Hallo ${name}, am ${when}.\n\n${recoveryCodesLeft}\n\n${link}`,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
// await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
|
|
95
|
+
```
|
|
96
|
+
|
|
78
97
|
`signInCode` is **never given the challenge**: it is not in its variables,
|
|
79
98
|
and the method never reads it. An override states the expiry from
|
|
80
99
|
`expiresIn`, already formatted:
|
|
@@ -105,11 +124,11 @@ methods live on its prototype and are refused — see
|
|
|
105
124
|
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
106
125
|
|
|
107
126
|
`templates` is `Partial` while `locales` stays within `en` and `fr`: give
|
|
108
|
-
any of the
|
|
109
|
-
another locale, it takes **all
|
|
127
|
+
any of the nine, and the defaults render the rest. Once `locales` holds
|
|
128
|
+
another locale, it takes **all nine** — see
|
|
110
129
|
[Adding a locale](locales.md#adding-a-locale).
|
|
111
130
|
|
|
112
|
-
`mail.templates` answers the
|
|
131
|
+
`mail.templates` answers the nine in use — yours, and the defaults for the
|
|
113
132
|
rest — frozen:
|
|
114
133
|
|
|
115
134
|
```ts
|
|
@@ -131,7 +150,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
131
150
|
});
|
|
132
151
|
```
|
|
133
152
|
|
|
134
|
-
It answers the
|
|
153
|
+
It answers the nine defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
135
154
|
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
136
155
|
to send one through something other than `janusMail()`. A default reused for
|
|
137
156
|
a wider set of locales is a compile error: it could not render the one it
|
|
@@ -157,7 +176,7 @@ them renders: that is when the package's `mails/` folder is read, once.
|
|
|
157
176
|
## What the defaults say
|
|
158
177
|
|
|
159
178
|
The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
|
|
160
|
-
0.
|
|
179
|
+
1.0.1, built with a neutral grey theme and no logo:
|
|
161
180
|
|
|
162
181
|
| Template | Built from | Subject (`en`) | Subject (`fr`) |
|
|
163
182
|
| --- | --- | --- | --- |
|
|
@@ -168,12 +187,16 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
168
187
|
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
169
188
|
| `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
|
|
170
189
|
| `twoFactorDisabled` | `two-factor-disabled` | Two-factor authentication was turned off | L'authentification à deux facteurs a été désactivée |
|
|
190
|
+
| `recoveryCodeUsed` | `recovery-code-used` | A recovery code was used on your account | Un code de récupération a été utilisé sur votre compte |
|
|
171
191
|
| `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
|
|
172
192
|
|
|
173
193
|
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
174
194
|
(but `signInCode`), a button with its link and the link again in text for a
|
|
175
|
-
client that shows no button, and a text part. The four notices add a warning:
|
|
176
|
-
*if this was not you, secure your account now*. `
|
|
195
|
+
client that shows no button, and a text part. The other four notices add a warning:
|
|
196
|
+
*if this was not you, secure your account now*. `recoveryCodeUsed` says it
|
|
197
|
+
too, after a warning banner — *a recovery code was used on your account at
|
|
198
|
+
`when`* — the codes left, and a line on generating new ones; its button says
|
|
199
|
+
**Secure my account**. `welcome` has the brand in
|
|
177
200
|
its heading — "Welcome to Acme" — and the recipient's name in its subject,
|
|
178
201
|
filled at send time like the body; its button says **Get started**.
|
|
179
202
|
|
|
@@ -197,11 +220,13 @@ mode, 9.85:1. The muted text — the footer, and the closing *if you did not
|
|
|
197
220
|
ask for this* — is a slate grey (`#5f718a`) in light mode, 4.53:1 on the
|
|
198
221
|
page, and turns `#cbd5e1` in dark mode, 12.01:1 on the dark card and
|
|
199
222
|
13.31:1 on the page. The notices' alerts keep their light ground and grey
|
|
200
|
-
text in both modes, 4.56:1 at the least
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
text in both modes, 4.56:1 at the least; the recovery code banner keeps
|
|
224
|
+
its light ground and near-black text, 17.57:1. The link under the button is
|
|
225
|
+
a blue of each mode's own: `#1d4ed8` on the light card, 6.70:1, and
|
|
226
|
+
`#93c5fd` on the dark one, 9.89:1 — `@nxgt/mail-ui` 1.0 gives the info
|
|
227
|
+
blue a dark value (`color-info-dark`), where one blue served both modes and
|
|
228
|
+
read 2.63:1 on the light card. Every text of the defaults reads at 4.5:1 or
|
|
229
|
+
more in both modes. Nothing is passed for any of it,
|
|
205
230
|
the text part has no colours, and a template of your own gets none of it. `@nxgt/mail-ui`'s
|
|
206
231
|
[Dark mode](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/dark-mode.md)
|
|
207
232
|
guide has the technique, client by client.
|
package/docs/roadmap.md
CHANGED
|
@@ -35,7 +35,7 @@ Nothing between releases.
|
|
|
35
35
|
`@nxgt/mail` answers every failure, whichever package sent the e-mail.
|
|
36
36
|
- **Retries or a queue** — a retry is the application's decision, made where
|
|
37
37
|
it can see it; hand a send to a queue that retries a `MAIL_FAILED`, or pass
|
|
38
|
-
a mailer wrapped in `@nxgt/mail` 0.8
|
|
38
|
+
a mailer wrapped in `@nxgt/mail`'s `withRetry` (0.8 or later).
|
|
39
39
|
- **Sending from inside `janus()`** — `@nxgt/janus` stays free of e-mail:
|
|
40
40
|
its flows answer what to send, and this package is called with that answer.
|
|
41
41
|
An application that sends its own way needs neither this package nor a
|
|
@@ -47,6 +47,20 @@ Nothing between releases.
|
|
|
47
47
|
|
|
48
48
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
49
49
|
|
|
50
|
+
- **The recovery code notice, and a readable link in both modes, v0.5.0.**
|
|
51
|
+
`recoveryCodeUsed(to, { when, recoveryCodesLeft })`, built from
|
|
52
|
+
`@nxgt/mail-presets` 1.0.0's `recovery-code-used` in English and French:
|
|
53
|
+
send it on `@nxgt/janus`'s `user.recoveryCodeUsed` event, with the count
|
|
54
|
+
`auth.secondFactor.recoveryCodesLeft(user)` reads (`@nxgt/janus` 0.11),
|
|
55
|
+
written as the preset's plural in the recipient's locale — "You have 1
|
|
56
|
+
recovery code left." Its button links to the new, optional
|
|
57
|
+
`links.recoveryCodes()`, else `links.secureAccount()`. Nine e-mails now,
|
|
58
|
+
and nine templates to pass for a locale beyond `en` and `fr`. The link
|
|
59
|
+
under the button is a blue of each mode's own — 6.70:1 on the light card,
|
|
60
|
+
where it read 2.63:1, and 9.89:1 on the dark one — from `@nxgt/mail-ui`
|
|
61
|
+
1.0.0's `color-info-dark`. The `@nxgt/mail` peer widens to `>=0.1.0 <2`,
|
|
62
|
+
so `@nxgt/mail` 1.0 and its transports are admitted; the manifest is still
|
|
63
|
+
format 1.
|
|
50
64
|
- **Muted text at 4.5:1 or more in both modes, v0.4.1.** Under dark mode the
|
|
51
65
|
sign-in code's box is a slate a step above the dark card with light code on
|
|
52
66
|
it, 9.85:1, and the muted text turns light, 13.31:1 on the page and
|
package/docs/troubleshooting.md
CHANGED
|
@@ -27,12 +27,13 @@ How the messages are shaped:
|
|
|
27
27
|
- [`janusMail: brand must be the name the e-mails show, as 'Acme'`](#janusmail-brand-must-be-the-name-the-e-mails-show-as-acme)
|
|
28
28
|
- [`janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`](#janusmail-links-must-be-an-object-as--verifyemail-resetpassword-secureaccount-getstarted-)
|
|
29
29
|
- [`janusMail: links.<name> must be a function`](#janusmail-linksname-must-be-a-function)
|
|
30
|
+
- [`janusMail: links.recoveryCodes must be a function, or left out`](#janusmail-linksrecoverycodes-must-be-a-function-or-left-out)
|
|
30
31
|
- [`janusMail: locales must list at least one locale, as ['en', 'fr']`](#janusmail-locales-must-list-at-least-one-locale-as-en-fr)
|
|
31
32
|
- [`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca)
|
|
32
33
|
- [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
|
|
33
34
|
- [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
|
|
34
35
|
- [`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, twoFactorEnabled, twoFactorDisabled, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-welcome)
|
|
36
|
+
- [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
|
|
36
37
|
- [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
|
|
37
38
|
- [`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
39
|
- [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
|
|
@@ -41,6 +42,8 @@ How the messages are shaped:
|
|
|
41
42
|
**Sending**
|
|
42
43
|
- [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
|
|
43
44
|
- [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
|
|
45
|
+
- [`janusMail.recoveryCodeUsed: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`](#janusmailrecoverycodeused-recoverycodesleft-must-be-a-count--a-whole-number-0-or-more--or-the-sentence-to-show)
|
|
46
|
+
- [`janusMail.recoveryCodeUsed: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`](#janusmailrecoverycodeused-recoverycodesleft-must-be-the-sentence-to-show-in-a-locale-the-default-e-mails-are-not-built-in)
|
|
44
47
|
- [`janusMail.<method>: clock.now() must answer a Date`](#janusmailmethod-clocknow-must-answer-a-date)
|
|
45
48
|
- [`janusMail.<method>: expiresAt is past — the link or code would not work`](#janusmailmethod-expiresat-is-past--the-link-or-code-would-not-work)
|
|
46
49
|
- [`This link expires in 1 hour.` — in the wrong language, or shorter than the `tokens` TTL](#this-link-expires-in-1-hour--in-the-wrong-language-or-shorter-than-the-tokens-ttl)
|
|
@@ -55,14 +58,17 @@ How the messages are shaped:
|
|
|
55
58
|
- [The button all but vanishes in dark mode](#the-button-all-but-vanishes-in-dark-mode)
|
|
56
59
|
- [The sign-in code's box stays a light slab in dark mode](#the-sign-in-codes-box-stays-a-light-slab-in-dark-mode)
|
|
57
60
|
- [The sign-in code turns unreadable in dark mode after upgrading `@nxgt/mail-ui` to 0.7](#the-sign-in-code-turns-unreadable-in-dark-mode-after-upgrading-nxgtmail-ui-to-07)
|
|
58
|
-
- [The link under the button is faint in light mode](#the-link-under-the-button-is-faint-in-light-mode)
|
|
61
|
+
- [The link under the button is faint in light mode, or in dark mode](#the-link-under-the-button-is-faint-in-light-mode-or-in-dark-mode)
|
|
59
62
|
|
|
60
63
|
**Compile errors**
|
|
61
64
|
- [`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)
|
|
62
65
|
- [`TS2345: … Types of property 'expiresAt' are incompatible. Type 'string' is not assignable to type 'Date'.`](#ts2345--types-of-property-expiresat-are-incompatible-type-string-is-not-assignable-to-type-date)
|
|
63
66
|
- [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
|
|
67
|
+
- [`TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-recoverycodeused-is-missing-in-type----but-required-in-type-janusmailtemplates)
|
|
64
68
|
- [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
|
|
65
69
|
- [`TS2741: Property 'getStarted' is missing in type '{ … }' but required in type 'JanusMailLinks'.`](#ts2741-property-getstarted-is-missing-in-type----but-required-in-type-janusmaillinks)
|
|
70
|
+
- [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
|
|
71
|
+
- [`TS2322: Type 'Date' is not assignable to type 'string'.`](#ts2322-type-date-is-not-assignable-to-type-string)
|
|
66
72
|
|
|
67
73
|
---
|
|
68
74
|
|
|
@@ -107,8 +113,8 @@ janusMail({ mailer, from, brand: 'Acme', links });
|
|
|
107
113
|
|
|
108
114
|
### `janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`
|
|
109
115
|
|
|
110
|
-
No `links`.
|
|
111
|
-
the welcome to `getStarted`.
|
|
116
|
+
No `links`. Four are required: the notices link to `secureAccount`, and
|
|
117
|
+
the welcome to `getStarted`. A fifth, `recoveryCodes`, is optional.
|
|
112
118
|
|
|
113
119
|
### `janusMail: links.<name> must be a function`
|
|
114
120
|
|
|
@@ -131,6 +137,17 @@ link the welcome e-mail added in 0.4.0: `links` written for 0.3 has no
|
|
|
131
137
|
sign-in page — even if you never send the welcome. In TypeScript the same
|
|
132
138
|
`links` is a compile error on `links`, `Property 'getStarted' is missing`.
|
|
133
139
|
|
|
140
|
+
### `janusMail: links.recoveryCodes must be a function, or left out`
|
|
141
|
+
|
|
142
|
+
`links.recoveryCodes` given as a URL, or as anything but a function. It is
|
|
143
|
+
optional — the recovery code notice links to `secureAccount` without it —
|
|
144
|
+
but when given it takes nothing and answers the page where the user
|
|
145
|
+
regenerates their codes:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
links: { …, recoveryCodes: () => 'https://acme.example/account/recovery-codes' },
|
|
149
|
+
```
|
|
150
|
+
|
|
134
151
|
### `janusMail: locales must list at least one locale, as ['en', 'fr']`
|
|
135
152
|
|
|
136
153
|
`locales: []`, `locales: 'en'`, or a list holding something that is not a
|
|
@@ -163,9 +180,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
|
|
|
163
180
|
|
|
164
181
|
`templates` given as a list or a function. Key each template by its name.
|
|
165
182
|
|
|
166
|
-
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, welcome`
|
|
183
|
+
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
167
184
|
|
|
168
|
-
A key that is not one of the
|
|
185
|
+
A key that is not one of the nine — `magicLink`, or the e-mail's file name
|
|
169
186
|
`'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
|
|
170
187
|
on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
|
|
171
188
|
|
|
@@ -199,7 +216,7 @@ as it is: fields are own properties.
|
|
|
199
216
|
### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
|
|
200
217
|
|
|
201
218
|
`locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
|
|
202
|
-
out: the defaults could not render them in that locale. Pass all
|
|
219
|
+
out: the defaults could not render them in that locale. Pass all nine — see
|
|
203
220
|
[Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
|
|
204
221
|
The message lists the templates missing.
|
|
205
222
|
|
|
@@ -238,14 +255,19 @@ number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
|
|
|
238
255
|
leave it out.
|
|
239
256
|
|
|
240
257
|
`janusMail.twoFactorEnabled: email must be a string` (or `twoFactorDisabled`,
|
|
241
|
-
or `welcome`) is the user event itself given to the
|
|
242
|
-
alone. Read the user first:
|
|
258
|
+
`recoveryCodeUsed` or `welcome`) is the user event itself given to the
|
|
259
|
+
e-mail: an event names the user by id alone. Read the user first:
|
|
243
260
|
|
|
244
261
|
```ts
|
|
245
262
|
const user = await auth.get(event.userId);
|
|
246
263
|
await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
|
|
247
264
|
```
|
|
248
265
|
|
|
266
|
+
`janusMail.recoveryCodeUsed: when must be a string` is the second argument
|
|
267
|
+
missing, or its `when` given as the event's `Date`: pass the text to show,
|
|
268
|
+
formatted in the recipient's locale and time zone —
|
|
269
|
+
`new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone }).format(event.occurredAt)`.
|
|
270
|
+
|
|
249
271
|
### `janusMail.<method>: expiresAt must be a Date`
|
|
250
272
|
|
|
251
273
|
A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the e-mail
|
|
@@ -261,6 +283,32 @@ await mail.verifyEmail(issued, { name: job.name });
|
|
|
261
283
|
await mail.verifyEmail(job.issued, { name: job.name }, { expiresIn: '1 day' });
|
|
262
284
|
```
|
|
263
285
|
|
|
286
|
+
### `janusMail.recoveryCodeUsed: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`
|
|
287
|
+
|
|
288
|
+
A `TypeError`, before anything is rendered or sent. `recoveryCodesLeft` was
|
|
289
|
+
`null`, negative, a fraction, `NaN`, or missing. The usual cause is
|
|
290
|
+
`auth.secondFactor.recoveryCodesLeft(user)`'s answer passed unchecked: it is
|
|
291
|
+
`null` for a user with no active factor — turned off since the code was
|
|
292
|
+
spent — who has no codes to count. Check it, and send nothing then:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const recoveryCodesLeft = await auth.secondFactor.recoveryCodesLeft(user);
|
|
296
|
+
if (recoveryCodesLeft !== null) {
|
|
297
|
+
await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft });
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `janusMail.recoveryCodeUsed: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`
|
|
302
|
+
|
|
303
|
+
A `TypeError`, before anything is sent: the recipient's locale is one of
|
|
304
|
+
yours beyond `en` and `fr`, and `recoveryCodesLeft` is a count. The plural
|
|
305
|
+
of the codes left is in the build's catalogue, in `en` and `fr` only, so the
|
|
306
|
+
count cannot be written for that locale. Pass the sentence as text:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
|
|
310
|
+
```
|
|
311
|
+
|
|
264
312
|
### `janusMail.<method>: clock.now() must answer a Date`
|
|
265
313
|
|
|
266
314
|
A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the
|
|
@@ -320,6 +368,7 @@ A `TypeError` from a call, before anything is rendered or sent
|
|
|
320
368
|
(`mailer.attempts` stays 0) — `janusMail.verifyEmail: links.verifyEmail(token)
|
|
321
369
|
must answer a string`, `janusMail.emailChanged: links.secureAccount() must
|
|
322
370
|
answer a string`, `janusMail.welcome: links.getStarted() must answer a
|
|
371
|
+
string`, `janusMail.recoveryCodeUsed: links.recoveryCodes() must answer a
|
|
323
372
|
string`. The `links` function answered something other than a
|
|
324
373
|
string: a `URL` object, `undefined`, or a promise — an `async` function.
|
|
325
374
|
Answer `url.href`, and compute the link synchronously:
|
|
@@ -379,7 +428,7 @@ try {
|
|
|
379
428
|
```
|
|
380
429
|
|
|
381
430
|
This package retries nothing: a retry is your decision. To retry in
|
|
382
|
-
process, pass a mailer wrapped in `@nxgt/mail`
|
|
431
|
+
process, pass a mailer wrapped in `@nxgt/mail`'s `withRetry` (0.8 or later); once its
|
|
383
432
|
attempts run out it rejects with the last `MailFailure`, `attempts` on it —
|
|
384
433
|
see [Retrying and tracing the mailer](guide/sending.md#retrying-and-tracing-the-mailer).
|
|
385
434
|
|
|
@@ -396,7 +445,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
|
|
|
396
445
|
|
|
397
446
|
The default templates read `mails/` with `node:fs`, through
|
|
398
447
|
`@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
|
|
399
|
-
|
|
448
|
+
nine templates yourself; an inlined build is on the [roadmap](roadmap.md).
|
|
400
449
|
|
|
401
450
|
### An e-mail stays light in dark mode
|
|
402
451
|
|
|
@@ -466,17 +515,33 @@ the code at 1.86:1 once rebuilt with `@nxgt/mail-ui` 0.7.0. Through 0.6.0,
|
|
|
466
515
|
no one value suits a mid-slate box: set a dark `color-muted-dark` and a
|
|
467
516
|
light `color-muted-foreground-dark`, as in the entry above.
|
|
468
517
|
|
|
469
|
-
### The link under the button is faint in light mode
|
|
518
|
+
### The link under the button is faint in light mode, or in dark mode
|
|
470
519
|
|
|
471
520
|
The link repeated in text under the button, for a client that shows no
|
|
472
|
-
button, is `@nxgt/mail-ui`'s `NxLink`, coloured with its info blue
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
521
|
+
button, is `@nxgt/mail-ui`'s `NxLink`, coloured with its info blue. Through
|
|
522
|
+
0.4, the defaults kept `@nxgt/mail-ui`'s `#54a2ff` in both modes: 2.63:1 on
|
|
523
|
+
the light card, 6.78:1 on the dark one. Since 0.5.0 they are built with
|
|
524
|
+
`@nxgt/mail-ui` 1.0, whose info blue has a dark twin, and set one blue per
|
|
525
|
+
mode: `#1d4ed8` on the light card, 6.70:1, and `#93c5fd` on the dark card,
|
|
526
|
+
9.89:1. Upgrade to read it.
|
|
527
|
+
|
|
528
|
+
A template of your own built with `@nxgt/mail-ui` 1.0.0 or later sets the
|
|
529
|
+
same pair in its `theme` — a light `color-info` dark enough for the white
|
|
530
|
+
card, and a `color-info-dark` light enough for the dark one; no single
|
|
531
|
+
value reaches 4.5:1 on both:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
ui({
|
|
535
|
+
brand: { name: 'Acme' },
|
|
536
|
+
theme: {
|
|
537
|
+
'color-info': '#1d4ed8', // the link on the light card, 6.70:1
|
|
538
|
+
'color-info-dark': '#93c5fd', // the link on the dark card, 9.89:1
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Before 1.0, colour the link with the primary instead, which has a dark
|
|
544
|
+
twin: `NxLink` merges a `class` over its own `text-info`, and
|
|
480
545
|
`nx-dark-text-primary` follows `color-primary-dark` in dark mode:
|
|
481
546
|
|
|
482
547
|
```vue
|
|
@@ -511,7 +576,18 @@ whole.
|
|
|
511
576
|
### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
|
|
512
577
|
|
|
513
578
|
`locales` holds a locale beyond `en` and `fr`, and `templates` does not give
|
|
514
|
-
all
|
|
579
|
+
all nine. With five or more missing, the code is `TS2740` and the list ends
|
|
580
|
+
`…, and 4 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
|
|
581
|
+
|
|
582
|
+
### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
583
|
+
|
|
584
|
+
`templates` written for 0.4, with a locale beyond `en` and `fr`: since
|
|
585
|
+
0.5.0 there are nine templates, and with such a locale every one is yours.
|
|
586
|
+
Add `recoveryCodeUsed` — it is given `brand`, `name`, `when`,
|
|
587
|
+
`recoveryCodesLeft` (a sentence) and `link` — even if you never send it; in
|
|
588
|
+
JavaScript the same `templates` is
|
|
589
|
+
[`janusMail: the default templates are built in en and fr only — …; recoveryCodeUsed missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
|
|
590
|
+
See [Templates](guide/templates.md#what-each-template-is-given).
|
|
515
591
|
|
|
516
592
|
### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
|
|
517
593
|
|
|
@@ -533,3 +609,20 @@ links: {
|
|
|
533
609
|
getStarted: () => 'https://acme.example/',
|
|
534
610
|
},
|
|
535
611
|
```
|
|
612
|
+
|
|
613
|
+
### `TS2322: Type 'number | null' is not assignable to type 'string | number'.`
|
|
614
|
+
|
|
615
|
+
On `recoveryCodesLeft`: `auth.secondFactor.recoveryCodesLeft(user)`'s
|
|
616
|
+
answer, passed without checking it for `null`. A user with no active factor
|
|
617
|
+
has no codes to count; check it first, as in
|
|
618
|
+
[`recoveryCodesLeft must be a count`](#janusmailrecoverycodeused-recoverycodesleft-must-be-a-count--a-whole-number-0-or-more--or-the-sentence-to-show).
|
|
619
|
+
|
|
620
|
+
### `TS2322: Type 'Date' is not assignable to type 'string'.`
|
|
621
|
+
|
|
622
|
+
On `when`: the event's `occurredAt` passed as it is. `when` is the text the
|
|
623
|
+
e-mail shows — format it in the recipient's locale and time zone first:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
const when = new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
|
|
627
|
+
.format(event.occurredAt);
|
|
628
|
+
```
|
|
@@ -10,11 +10,11 @@
|
|
|
10
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
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-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
|
|
12
12
|
[data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
|
|
13
|
-
[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[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-
|
|
14
|
-
|
|
15
|
-
{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display: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;}
|
|
13
|
+
[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[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-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
|
|
14
|
+
!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
|
|
15
|
+
!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display: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;}
|
|
16
16
|
@media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!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
|
|
17
|
-
!important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
|
|
17
|
+
!important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
|
|
18
18
|
</style>
|
|
19
19
|
</head>
|
|
20
20
|
<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. ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
@@ -42,7 +42,7 @@ background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{backgroun
|
|
|
42
42
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
43
43
|
<![endif]--> <span style="mso-text-raise:10px;">Undo this change</span>
|
|
44
44
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
45
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" style="color:#
|
|
45
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:underline;">{{ link }}</a></p>
|
|
46
46
|
</td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><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>
|
|
47
47
|
</td></tr></table></div>
|
|
48
48
|
<!--[if mso]>
|
|
@@ -10,11 +10,11 @@
|
|
|
10
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
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-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
|
|
12
12
|
[data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
|
|
13
|
-
[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[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-
|
|
14
|
-
|
|
15
|
-
{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display: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;}
|
|
13
|
+
[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[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-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
|
|
14
|
+
!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
|
|
15
|
+
!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display: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;}
|
|
16
16
|
@media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!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
|
|
17
|
-
!important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
|
|
17
|
+
!important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
|
|
18
18
|
</style>
|
|
19
19
|
</head>
|
|
20
20
|
<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. ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
@@ -42,7 +42,7 @@ background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{backgroun
|
|
|
42
42
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
43
43
|
<![endif]--> <span style="mso-text-raise:10px;">Secure my account</span>
|
|
44
44
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
45
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" style="color:#
|
|
45
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:underline;">{{ link }}</a></p>
|
|
46
46
|
</td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><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>
|
|
47
47
|
</td></tr></table></div>
|
|
48
48
|
<!--[if mso]>
|