@nxgt/janus-mail 0.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/generated/locales.d.ts +5 -0
- package/dist/generated/locales.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +29 -0
- package/dist/generated/mail.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +14 -0
- package/dist/janus-mail.d.ts +24 -0
- package/dist/janus-mail.d.ts.map +1 -0
- package/dist/mails.d.ts +26 -0
- package/dist/mails.d.ts.map +1 -0
- package/dist/options.d.ts +27 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/templates.d.ts +15 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/README.md +34 -0
- package/docs/guide/building.md +130 -0
- package/docs/guide/locales.md +113 -0
- package/docs/guide/sending.md +242 -0
- package/docs/guide/templates.md +183 -0
- package/docs/roadmap.md +52 -0
- package/docs/troubleshooting.md +313 -0
- package/mails/en/email-changed.html +42 -0
- package/mails/en/email-changed.txt +21 -0
- package/mails/en/password-changed.html +42 -0
- package/mails/en/password-changed.txt +21 -0
- package/mails/en/reset-password.html +40 -0
- package/mails/en/reset-password.txt +22 -0
- package/mails/en/sign-in-code.html +36 -0
- package/mails/en/sign-in-code.txt +15 -0
- package/mails/en/verify-email.html +40 -0
- package/mails/en/verify-email.txt +22 -0
- package/mails/fr/email-changed.html +42 -0
- package/mails/fr/email-changed.txt +21 -0
- package/mails/fr/password-changed.html +41 -0
- package/mails/fr/password-changed.txt +21 -0
- package/mails/fr/reset-password.html +39 -0
- package/mails/fr/reset-password.txt +22 -0
- package/mails/fr/sign-in-code.html +36 -0
- package/mails/fr/sign-in-code.txt +15 -0
- package/mails/fr/verify-email.html +40 -0
- package/mails/fr/verify-email.txt +22 -0
- package/mails/mail-manifest.json +128 -0
- package/package.json +71 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Sending
|
|
2
|
+
|
|
3
|
+
This page is for wiring `janusMail()` into an application: what it takes,
|
|
4
|
+
what each method sends and to whom, what happens when a send fails, and how
|
|
5
|
+
to test it.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { janus, createMemoryStores, scryptHasher } from '@nxgt/janus';
|
|
9
|
+
import { janusMail } from '@nxgt/janus-mail';
|
|
10
|
+
import { createSmtpMailer } from '@nxgt/mail-smtp'; // or any @nxgt/mail transport
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
export const auth = janus({
|
|
14
|
+
user: z.object({ email: z.email(), name: z.string(), locale: z.string().nullable() }),
|
|
15
|
+
password: { login: 'email' },
|
|
16
|
+
store: createMemoryStores(),
|
|
17
|
+
hasher: scryptHasher(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const mail = janusMail({
|
|
21
|
+
mailer: createSmtpMailer({ transporter }), // transporter: nodemailer.createTransport(…)
|
|
22
|
+
from: { name: 'Acme', address: 'noreply@acme.example' },
|
|
23
|
+
replyTo: 'support@acme.example',
|
|
24
|
+
brand: 'Acme',
|
|
25
|
+
links: {
|
|
26
|
+
verifyEmail: (token) => `https://acme.example/verify?token=${encodeURIComponent(token)}`,
|
|
27
|
+
resetPassword: (token) => `https://acme.example/reset?token=${encodeURIComponent(token)}`,
|
|
28
|
+
secureAccount: () => 'https://acme.example/account/security',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Create it once, at start-up, beside `janus()`. It reads nothing when called:
|
|
34
|
+
the default e-mails are read from the package's `mails/` folder the first
|
|
35
|
+
time one is sent, and kept.
|
|
36
|
+
|
|
37
|
+
## Options
|
|
38
|
+
|
|
39
|
+
| Option | Type | Default | Effect |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| `mailer` | `Mailer` | required | The transport every e-mail is handed to |
|
|
42
|
+
| `from` | `Address` | required | The sender: `'noreply@acme.example'` or `{ name, address }` |
|
|
43
|
+
| `replyTo` | `Address` | none | Where a reply goes, when not to `from` |
|
|
44
|
+
| `brand` | `string` | required | The name the e-mails show, in the header, the body and the footer. Text: escaped in the HTML |
|
|
45
|
+
| `links.verifyEmail` | `(token) => string` | required | The page that confirms an address, given the one-time token |
|
|
46
|
+
| `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
|
|
47
|
+
| `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
|
|
48
|
+
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
49
|
+
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
50
|
+
| `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
|
|
51
|
+
|
|
52
|
+
Every link is called at send time, and must answer an absolute `http:`,
|
|
53
|
+
`https:` or `mailto:` URL: a default template refuses anything else with
|
|
54
|
+
`MailRefused`, before the mailer is called. `mailto:` is accepted on purpose:
|
|
55
|
+
`secureAccount: () => 'mailto:security@acme.example'` points a user who made
|
|
56
|
+
no change at your support desk. A link must be answered synchronously, as a
|
|
57
|
+
string — an `async` function or a `URL` object is a compile error in
|
|
58
|
+
TypeScript, and a `TypeError` naming the call at send time in JavaScript,
|
|
59
|
+
`janusMail.verifyEmail: links.verifyEmail(token) must answer a string`.
|
|
60
|
+
Encode a token you put in a query string yourself (`encodeURIComponent`).
|
|
61
|
+
|
|
62
|
+
`links`, `from` and `replyTo` are copied and frozen when `janusMail()` is
|
|
63
|
+
called: what was checked then is what every send uses, and changing your
|
|
64
|
+
object afterwards changes nothing.
|
|
65
|
+
|
|
66
|
+
A wrong option is a bare `TypeError` when `janusMail()` is called, naming the
|
|
67
|
+
option and never its value — every message is in
|
|
68
|
+
[troubleshooting](../troubleshooting.md#wiring--janusmail).
|
|
69
|
+
|
|
70
|
+
## The methods
|
|
71
|
+
|
|
72
|
+
Each method renders its e-mail with its template, hands it to the mailer,
|
|
73
|
+
and answers the mailer's `SentMail` (`{ messageId }`).
|
|
74
|
+
|
|
75
|
+
### `verifyEmail(issued, to)`
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const issued = await auth.verifyEmail.send(user); // IssuedToken: { token, email, expiresAt }
|
|
79
|
+
await mail.verifyEmail(issued, { name: user.name, locale: user.locale });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Sent to `issued.email`, with the link `links.verifyEmail(issued.token)`.
|
|
83
|
+
|
|
84
|
+
### `resetPassword(issued, to)`
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const issued = await auth.resetPassword.request(email); // (IssuedToken & { user }) | null
|
|
88
|
+
if (issued !== null) {
|
|
89
|
+
await mail.resetPassword(issued, { name: issued.user.name, locale: issued.user.locale });
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`request` answers `null` for an address nobody holds, and the compiler
|
|
94
|
+
refuses `null` here: check it first, and answer the visitor the same either
|
|
95
|
+
way. Sent to `issued.email`, with `links.resetPassword(issued.token)`.
|
|
96
|
+
|
|
97
|
+
### `signInCode(issued, to?)`
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const issued = await auth.signInCode.request(email); // IssuedCode: { code, challenge, email, expiresAt, user } | null
|
|
101
|
+
if (issued !== null) await mail.signInCode(issued, { locale: issued.user.locale });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It reads `issued.code` and `issued.email`, **and nothing else**: the
|
|
105
|
+
challenge is the visitor's secret, and neither the default template nor an
|
|
106
|
+
override is ever given it. `to` is optional — the e-mail greets nobody by
|
|
107
|
+
name — and only its `locale` is read.
|
|
108
|
+
|
|
109
|
+
### `passwordChanged(to)`
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const changed = await auth.changePassword(user, { current, next });
|
|
113
|
+
await mail.passwordChanged({ name: changed.name, locale: changed.locale, email: changed.email });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A notice: the password of the account at `to.email` changed, with a link to
|
|
117
|
+
`links.secureAccount()` for a user who did not change it. Send it after
|
|
118
|
+
`changePassword`, `setPassword` and `resetPassword.confirm`.
|
|
119
|
+
|
|
120
|
+
### `emailChanged(to)`
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const formerEmail = user.email;
|
|
124
|
+
const updated = await auth.update(user, { email: next });
|
|
125
|
+
await mail.emailChanged({ name: updated.name, locale: updated.locale, formerEmail, newEmail: updated.email });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Sent to **`formerEmail`**: the owner of the old address is the one to warn,
|
|
129
|
+
since whoever changed it already controls the new one. It names `newEmail`,
|
|
130
|
+
and links to `links.secureAccount()`. Read the former address before the
|
|
131
|
+
update: the user `update` answers already holds the new one.
|
|
132
|
+
|
|
133
|
+
## Where each e-mail goes
|
|
134
|
+
|
|
135
|
+
| Method | To | Never to |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `verifyEmail` | `issued.email` | what the visitor typed |
|
|
138
|
+
| `resetPassword` | `issued.email` | the `email` passed to `request` |
|
|
139
|
+
| `signInCode` | `issued.email` | the `email` passed to `request` |
|
|
140
|
+
| `passwordChanged` | `to.email` | — |
|
|
141
|
+
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
142
|
+
|
|
143
|
+
`issued.email` is the address the user's record holds, as they registered
|
|
144
|
+
it; what the visitor typed may differ in case or spacing. The recipient is a
|
|
145
|
+
bare address: `to.name` is used in the greeting, not in the `To` header.
|
|
146
|
+
|
|
147
|
+
Only `subject`, `html` and `text` are read from what a template answers. A
|
|
148
|
+
template cannot add a recipient or a header: `to`, `from`, `replyTo` and
|
|
149
|
+
`headers` are this package's alone.
|
|
150
|
+
|
|
151
|
+
## When a send fails
|
|
152
|
+
|
|
153
|
+
**Every method rejects with the error it met, untouched.** This package
|
|
154
|
+
defines no error class and wraps nothing:
|
|
155
|
+
|
|
156
|
+
| Error | From | Means | Answer |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `MailFailure` (`MAIL_FAILED`) | the mailer | The transport could not hand the e-mail over. Nothing is known to have been sent | A `503`, or a retry from a queue |
|
|
159
|
+
| `MailRefused` (`MAIL_REFUSED`) | the renderer, or the mailer | The e-mail itself is wrong: a link that is not `http:`, `https:` or `mailto:`, an address that is not one | A bug to fix; sending it again fails again |
|
|
160
|
+
| `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string` | A bug to fix |
|
|
161
|
+
| `Error` from `createMailRenderer` | the renderer | `mails/` is missing where the package runs: a bundler inlined `@nxgt/janus-mail`, or a deploy kept `dist/` only | Keep `@nxgt/janus-mail` external to your bundle and deploy its `mails/` with it; see [troubleshooting](../troubleshooting.md#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder) |
|
|
162
|
+
|
|
163
|
+
`instanceof` holds against the classes of your own `@nxgt/mail`, since it is
|
|
164
|
+
a peer: one copy defines them.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { MailError } from '@nxgt/mail';
|
|
168
|
+
import { JanusError } from '@nxgt/janus';
|
|
169
|
+
|
|
170
|
+
export async function sendVerification(user: User): Promise<Response> {
|
|
171
|
+
try {
|
|
172
|
+
await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name, locale: user.locale });
|
|
173
|
+
return new Response(null, { status: 202 });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof MailError && error.code === 'MAIL_FAILED') {
|
|
176
|
+
return Response.json({ code: 'MAIL_FAILED' }, { status: 503 }); // offer to send it again
|
|
177
|
+
}
|
|
178
|
+
if (error instanceof JanusError && error.code === 'STORE_FAILED') {
|
|
179
|
+
return Response.json({ code: 'STORE_FAILED' }, { status: 503 });
|
|
180
|
+
}
|
|
181
|
+
throw error; // MAIL_REFUSED and TypeError are bugs: a 500
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Never fire and forget.** `void mail.signInCode(issued)` turns a failure
|
|
187
|
+
into an unhandled rejection. The [sign-in code guide of
|
|
188
|
+
`@nxgt/janus`](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/sign-in-code.md)
|
|
189
|
+
advises sending *after* answering, so the time an answer takes does not tell
|
|
190
|
+
whether the address exists: hand the send to a queue that awaits it and
|
|
191
|
+
retries a `MAIL_FAILED`, rather than to a promise nobody awaits.
|
|
192
|
+
|
|
193
|
+
## Testing
|
|
194
|
+
|
|
195
|
+
`createMemoryMailer()` from `@nxgt/mail` keeps an outbox, and can be told to
|
|
196
|
+
fail:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { expect, it } from 'bun:test';
|
|
200
|
+
import { createMemoryMailer, MailFailure } from '@nxgt/mail';
|
|
201
|
+
import { janusMail } from '@nxgt/janus-mail';
|
|
202
|
+
|
|
203
|
+
const links = {
|
|
204
|
+
verifyEmail: (token: string) => `https://acme.example/verify?token=${token}`,
|
|
205
|
+
resetPassword: (token: string) => `https://acme.example/reset?token=${token}`,
|
|
206
|
+
secureAccount: () => 'https://acme.example/account/security',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
it('sends the code, and never the challenge', async () => {
|
|
210
|
+
const mailer = createMemoryMailer();
|
|
211
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
212
|
+
const issued = { code: '042817', challenge: 'secret', email: 'ada@example.com' };
|
|
213
|
+
|
|
214
|
+
await mail.signInCode(issued, { locale: 'fr-CA' });
|
|
215
|
+
|
|
216
|
+
const [sent] = mailer.sent;
|
|
217
|
+
expect(sent?.to).toBe('ada@example.com');
|
|
218
|
+
expect(sent?.subject).toBe('Votre code de connexion : 042817');
|
|
219
|
+
expect(`${sent?.html}${sent?.text}`).not.toContain('secret');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('says so when the mailer is down', async () => {
|
|
223
|
+
const mailer = createMemoryMailer();
|
|
224
|
+
mailer.failNext();
|
|
225
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
226
|
+
|
|
227
|
+
const error = await mail
|
|
228
|
+
.signInCode({ code: '042817', email: 'ada@example.com' })
|
|
229
|
+
.then(() => null, (e: unknown) => e); // settled where it is created
|
|
230
|
+
|
|
231
|
+
expect(error).toBeInstanceOf(MailFailure);
|
|
232
|
+
expect(mailer.attempts).toBe(1); // tried once: nothing retried in secret
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## See also
|
|
237
|
+
|
|
238
|
+
- [Templates](templates.md) — replacing an e-mail.
|
|
239
|
+
- [Locales](locales.md) — how the locale is picked.
|
|
240
|
+
- `@nxgt/mail`'s
|
|
241
|
+
[sending guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/sending.md)
|
|
242
|
+
— the `Mailer` port and its errors.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Templates
|
|
2
|
+
|
|
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
|
|
5
|
+
own, and what the defaults say.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { janusMail } from '@nxgt/janus-mail';
|
|
9
|
+
|
|
10
|
+
const mail = janusMail({
|
|
11
|
+
mailer,
|
|
12
|
+
from: 'noreply@acme.example',
|
|
13
|
+
brand: 'Acme',
|
|
14
|
+
links,
|
|
15
|
+
templates: {
|
|
16
|
+
signInCode: ({ brand, code, locale }) => ({
|
|
17
|
+
subject: locale === 'fr' ? `Votre code ${brand} : ${code}` : `Your ${brand} code: ${code}`,
|
|
18
|
+
html: `<p style="font-size:24px;letter-spacing:4px">${code}</p>`,
|
|
19
|
+
text: code,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The sign-in code e-mail is now yours; the four others stay the defaults.
|
|
26
|
+
|
|
27
|
+
## What a template is
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import type { Rendered } from '@nxgt/mail';
|
|
31
|
+
|
|
32
|
+
type JanusMailTemplate<V, L extends string = 'en' | 'fr'> = (
|
|
33
|
+
variables: V & { readonly locale: L },
|
|
34
|
+
) => Rendered | PromiseLike<Rendered>;
|
|
35
|
+
|
|
36
|
+
interface Rendered {
|
|
37
|
+
readonly subject: string;
|
|
38
|
+
readonly html: string;
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A template is a function: its variables and the locale picked for the
|
|
44
|
+
recipient in, the three parts of the e-mail out — synchronously or as a
|
|
45
|
+
promise. Anything that answers that shape fits: React Email's `render`, a
|
|
46
|
+
template string, another Maizzle build read with `@nxgt/mail/renderer`.
|
|
47
|
+
|
|
48
|
+
**Escaping is then the template's job.** The defaults escape every value in
|
|
49
|
+
the HTML; a template of yours that writes `name` into HTML must escape it
|
|
50
|
+
itself — a user's name is free text.
|
|
51
|
+
|
|
52
|
+
Only `subject`, `html` and `text` are read from what it answers: a `to`, a
|
|
53
|
+
`from` or `headers` it adds are ignored, so a template can never redirect an
|
|
54
|
+
e-mail.
|
|
55
|
+
|
|
56
|
+
## What each template is given
|
|
57
|
+
|
|
58
|
+
`JanusMailVariables`, by template:
|
|
59
|
+
|
|
60
|
+
| Template | Variables | Sent by |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `verifyEmail` | `brand`, `name`, `link` | `mail.verifyEmail(issued, to)` |
|
|
63
|
+
| `resetPassword` | `brand`, `name`, `link` | `mail.resetPassword(issued, to)` |
|
|
64
|
+
| `signInCode` | `brand`, `code` | `mail.signInCode(issued, to?)` |
|
|
65
|
+
| `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
|
|
66
|
+
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
|
+
|
|
68
|
+
Every one also gets `locale`, one of `locales`. Every value is a string:
|
|
69
|
+
`brand` from the options, `name` from the recipient, `link` from `links` —
|
|
70
|
+
`links.secureAccount()` for the two notices — `code` and `newEmail` from the
|
|
71
|
+
call.
|
|
72
|
+
|
|
73
|
+
`signInCode` is **never given the challenge**: it is not in its variables,
|
|
74
|
+
and the method never reads it. `expiresAt` is not given either — the flows
|
|
75
|
+
answer it, so a template that states the lifetime takes it from a closure:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const mail = janusMail({
|
|
79
|
+
mailer, from, brand: 'Acme', links,
|
|
80
|
+
templates: {
|
|
81
|
+
// The code lives ten minutes, @nxgt/janus's default for tokens.signInCode.
|
|
82
|
+
signInCode: ({ code, locale }) =>
|
|
83
|
+
locale === 'fr'
|
|
84
|
+
? { subject: `Code : ${code}`, html: `<p>${code} — valable 10 minutes.</p>`, text: `${code} — valable 10 minutes.` }
|
|
85
|
+
: { subject: `Code: ${code}`, html: `<p>${code} — valid for 10 minutes.</p>`, text: `${code} — valid for 10 minutes.` },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A template that reads a variable its e-mail does not have is a compile error:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// @ts-expect-error — Property 'name' does not exist: the sign-in code e-mail greets nobody
|
|
94
|
+
templates: { signInCode: ({ name }) => rendered },
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Some templates, or all of them
|
|
98
|
+
|
|
99
|
+
`templates` is a plain object, or a class with template fields: a class's
|
|
100
|
+
methods live on its prototype and are refused — see
|
|
101
|
+
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
102
|
+
|
|
103
|
+
`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
|
+
[Adding a locale](locales.md#adding-a-locale).
|
|
107
|
+
|
|
108
|
+
`mail.templates` answers the five in use — yours, and the defaults for the
|
|
109
|
+
rest — frozen:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const rendered = await mail.templates.verifyEmail({ brand: 'Acme', name: 'Ada', link, locale: 'fr' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## The defaults alone — `janusTemplates()`
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { janusTemplates } from '@nxgt/janus-mail';
|
|
119
|
+
|
|
120
|
+
const defaults = janusTemplates();
|
|
121
|
+
const { subject, html, text } = await defaults.resetPassword({
|
|
122
|
+
brand: 'Acme',
|
|
123
|
+
name: 'Ada',
|
|
124
|
+
link: 'https://acme.example/reset?token=abc',
|
|
125
|
+
locale: 'fr',
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It answers the five defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
130
|
+
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
131
|
+
to send one through something other than `janusMail()`. A default reused for
|
|
132
|
+
a wider set of locales is a compile error: it could not render the one it
|
|
133
|
+
lacks.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { janusTemplates, type JanusMailTemplates } from '@nxgt/janus-mail';
|
|
137
|
+
|
|
138
|
+
const defaults = janusTemplates();
|
|
139
|
+
|
|
140
|
+
// Wrapping a default: the same e-mail, with a line added to its text part.
|
|
141
|
+
const templates: Partial<JanusMailTemplates> = {
|
|
142
|
+
verifyEmail: async (variables) => {
|
|
143
|
+
const rendered = await defaults.verifyEmail(variables);
|
|
144
|
+
return { ...rendered, text: `${rendered.text}\n\nAcme — 1 Example Street` };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The default templates share one renderer, created the first time one of
|
|
150
|
+
them renders: that is when the package's `mails/` folder is read, once.
|
|
151
|
+
|
|
152
|
+
## What the defaults say
|
|
153
|
+
|
|
154
|
+
The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
|
|
155
|
+
0.1.0, built with a neutral grey theme and no logo:
|
|
156
|
+
|
|
157
|
+
| Template | Built from | Subject (`en`) | Subject (`fr`) |
|
|
158
|
+
| --- | --- | --- | --- |
|
|
159
|
+
| `verifyEmail` | `verify-email` | Confirm your e-mail address | Confirmez votre adresse e-mail |
|
|
160
|
+
| `resetPassword` | `reset-password` | Reset your password | Réinitialisez votre mot de passe |
|
|
161
|
+
| `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
|
|
162
|
+
| `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
|
|
163
|
+
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
164
|
+
|
|
165
|
+
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
166
|
+
(but `signInCode`), a button with its link and the link again in text for a
|
|
167
|
+
client that shows no button, and a text part. The two notices add a warning:
|
|
168
|
+
*if this was not you, secure your account now*.
|
|
169
|
+
|
|
170
|
+
**They do not state how long a link or a code lasts.** Replace the template
|
|
171
|
+
when yours must: see above.
|
|
172
|
+
|
|
173
|
+
**The brand is text.** It is written in the header, the body and the footer,
|
|
174
|
+
escaped — no logo and no link, which would need absolute URLs known when the
|
|
175
|
+
package is built. A logo takes a template of your own; the
|
|
176
|
+
[roadmap](../roadmap.md) has a build of your own brand.
|
|
177
|
+
|
|
178
|
+
## See also
|
|
179
|
+
|
|
180
|
+
- [Sending](sending.md) — what each method passes its template.
|
|
181
|
+
- [Locales](locales.md) — the `locale` a template is given.
|
|
182
|
+
- `@nxgt/mail-presets`' [e-mails guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-presets/docs/guide/emails.md)
|
|
183
|
+
— every message of the defaults, in both locales.
|
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Where `@nxgt/janus-mail` is heading. A direction, not a commitment: there are
|
|
4
|
+
no dates here, and the version something shipped in is the only number.
|
|
5
|
+
|
|
6
|
+
## Now
|
|
7
|
+
|
|
8
|
+
Nothing between releases.
|
|
9
|
+
|
|
10
|
+
## Next
|
|
11
|
+
|
|
12
|
+
- **The other presets** — `magic-link`, `new-sign-in`, `welcome` and
|
|
13
|
+
`invitation` from `@nxgt/mail-presets`, each once `@nxgt/janus` has a flow
|
|
14
|
+
that sends it: a sign-in link, a sign-in from a new device, a user created,
|
|
15
|
+
an invitation to a user type.
|
|
16
|
+
- **The expiry in the e-mail** — "the link expires in 24 hours", from the
|
|
17
|
+
flow's `expiresAt`, once the presets' messages take it.
|
|
18
|
+
|
|
19
|
+
## Later
|
|
20
|
+
|
|
21
|
+
- **An inlined build, for edge runtimes** — the default e-mails compiled into
|
|
22
|
+
the JavaScript itself rather than read from `mails/` with `node:fs`, so the
|
|
23
|
+
package runs where there is no file system.
|
|
24
|
+
- **More built-in languages** — each one a catalogue of the presets'
|
|
25
|
+
messages, so a locale beyond `en` and `fr` no longer requires every
|
|
26
|
+
template.
|
|
27
|
+
- **Your brand at build time** — a logo, a link and your colours, from a
|
|
28
|
+
Maizzle build of your own over the same presets, read by the same
|
|
29
|
+
renderer.
|
|
30
|
+
|
|
31
|
+
## Not planned
|
|
32
|
+
|
|
33
|
+
- **An error class of its own** — a send rejects with the mailer's
|
|
34
|
+
`MailFailure` or `MailRefused`, untouched, so one `instanceof` against
|
|
35
|
+
`@nxgt/mail` answers every failure, whichever package sent the e-mail.
|
|
36
|
+
- **Retries or a queue** — a retry is the application's decision, made where
|
|
37
|
+
it can see it; hand a send to a queue that retries a `MAIL_FAILED`.
|
|
38
|
+
- **Sending from inside `janus()`** — `@nxgt/janus` stays free of e-mail:
|
|
39
|
+
its flows answer what to send, and this package is called with that answer.
|
|
40
|
+
An application that sends its own way needs neither this package nor a
|
|
41
|
+
hook to turn off.
|
|
42
|
+
- **The challenge in an e-mail** — a sign-in code e-mail carries the code and
|
|
43
|
+
never the challenge, not even through an override.
|
|
44
|
+
|
|
45
|
+
## Shipped
|
|
46
|
+
|
|
47
|
+
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
48
|
+
|
|
49
|
+
- **The first release, v0.1.0.** The five e-mails of `@nxgt/janus`'s flows,
|
|
50
|
+
in English and French, over any `@nxgt/mail` transport: `janusMail()` and
|
|
51
|
+
`janusTemplates()`. Built with Maizzle when the package is built, shipped
|
|
52
|
+
in `mails/`, and only filled in at send time, every value escaped.
|