@wtfalch/mailer 0.4.0 → 0.6.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 +26 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/send.d.ts +6 -6
- package/dist/send.js +4 -4
- package/dist/transport.d.ts +39 -15
- package/dist/transport.js +110 -32
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @wtfalch/mailer
|
|
2
2
|
|
|
3
3
|
One `send(template, to, data)` call for the estate's transactional email,
|
|
4
|
-
over Lettermint
|
|
4
|
+
over Lettermint.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ Call `send()` with a template name, the recipient, that template's data,
|
|
|
15
15
|
and who the message is from:
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
18
|
+
import { createLettermintTransport, send } from '@wtfalch/mailer';
|
|
19
19
|
|
|
20
20
|
await send({
|
|
21
21
|
template: 'invitation',
|
|
@@ -36,15 +36,36 @@ await send({
|
|
|
36
36
|
attachments: [ // optional
|
|
37
37
|
{ filename: 'invoice.pdf', contentType: 'application/pdf', content: 'JVBERi0xLjQK...' }, // content is base64
|
|
38
38
|
],
|
|
39
|
-
transport:
|
|
39
|
+
transport: createLettermintTransport({ apiKey }), // resolved by the caller, e.g. from @wtfalch/keys
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`transport` is optional. Left out, `send()`
|
|
44
|
-
`
|
|
43
|
+
`transport` is optional. Left out, `send()` calls `defaultTransport()`,
|
|
44
|
+
which throws `MailerConfigError` unless `MAIL_SINK_URL` or `LETTERMINT_API_URL` is set -- a
|
|
45
45
|
missing transport fails loudly instead of silently no-op'ing or making a
|
|
46
46
|
live call with no key.
|
|
47
47
|
|
|
48
|
+
`MAIL_SINK_URL` (a Mailpit base URL such as `http://acme-stage-sink:8025`;
|
|
49
|
+
https, or http for `localhost`, `127.0.0.1`, `[::1]` or a single-label
|
|
50
|
+
internal host) makes the default transport post every message to
|
|
51
|
+
`<MAIL_SINK_URL>/api/v1/send` in Mailpit's send-API JSON, with no token
|
|
52
|
+
header. It wins over every setting below, `LETTERMINT_API_TOKEN` included,
|
|
53
|
+
so a stage app never reaches the real provider. A malformed URL throws
|
|
54
|
+
`MailerConfigError` when the default transport is built, before any send.
|
|
55
|
+
The same transport is `createMailpitTransport({ baseUrl })`. A non-2xx answer
|
|
56
|
+
throws a fixed message with the status, never the response body or the mail.
|
|
57
|
+
|
|
58
|
+
Otherwise two env names set that default, in every `NODE_ENV`: with
|
|
59
|
+
`LETTERMINT_API_URL` set (a base URL such as `https://sink.example.com/v1`;
|
|
60
|
+
https, or http for `localhost`, `127.0.0.1` or `[::1]` only) `send()` sends
|
|
61
|
+
through Lettermint's API shape to that server, with the token from
|
|
62
|
+
`LETTERMINT_API_TOKEN` in the `x-lettermint-token` header. This is how a
|
|
63
|
+
stage app, a production build holding real addresses, or the local stack
|
|
64
|
+
sends every mail to a sink. A malformed URL, or a URL with no token, throws
|
|
65
|
+
`MailerConfigError` when the default transport is built, before any send.
|
|
66
|
+
The same URL is available per transport as
|
|
67
|
+
`createLettermintTransport({ apiKey, baseUrl })`.
|
|
68
|
+
|
|
48
69
|
A held key: however you resolve `apiKey` for `createLettermintTransport`,
|
|
49
70
|
mailer never fetches or stores it. In the estate, the caller resolves the
|
|
50
71
|
org's Lettermint key as a held key from `@wtfalch/keys` and passes it in on
|
|
@@ -65,9 +86,6 @@ never fetches or stores it. In the estate, the caller resolves the org's
|
|
|
65
86
|
Lettermint key as a held key from `@wtfalch/keys` and passes it
|
|
66
87
|
in on every call.
|
|
67
88
|
|
|
68
|
-
- `createDevTransport({ dir })` -- writes each message to
|
|
69
|
-
`<dir>/<timestamp>-<id>.json` (default `dir`: `.mailer/dev`). Never
|
|
70
|
-
opens a network connection.
|
|
71
89
|
- `createLettermintTransport({ apiKey })` -- sends over Lettermint's
|
|
72
90
|
Sending API (EU-hosted). Needs either `apiKey` (the org's Lettermint
|
|
73
91
|
Project API token, `lm_...` -- resolve it yourself, e.g. from
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { Attachment, Brand, Locale, MailMessage, SenderIdentity, SendResult, } from './types.js';
|
|
2
2
|
export { DEFAULT_LOCALE, MailerConfigError } from './types.js';
|
|
3
|
-
export type {
|
|
4
|
-
export {
|
|
3
|
+
export type { LettermintAttachment, LettermintFetchLike, LettermintLike, LettermintSendPayload, LettermintTransportOptions, MailpitTransportOptions, OutgoingMail, Transport, } from './transport.js';
|
|
4
|
+
export { createLettermintTransport, createMailpitTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
|
|
5
5
|
export type { SendOptions } from './send.js';
|
|
6
6
|
export { send } from './send.js';
|
|
7
7
|
export type { TemplateData, TemplateName } from './templates/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { DEFAULT_LOCALE, MailerConfigError } from './types.js';
|
|
2
|
-
export {
|
|
2
|
+
export { createLettermintTransport, createMailpitTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
|
|
3
3
|
export { send } from './send.js';
|
|
4
4
|
export { templates } from './templates/index.js';
|
|
5
5
|
export { renderInvitation } from './templates/invitation.js';
|
package/dist/send.d.ts
CHANGED
|
@@ -6,17 +6,17 @@ export interface SendOptions<T extends TemplateName> {
|
|
|
6
6
|
readonly to: string;
|
|
7
7
|
readonly data: TemplateData<T>;
|
|
8
8
|
readonly from: SenderIdentity;
|
|
9
|
-
/** Defaults to
|
|
9
|
+
/** Defaults to `defaultTransport()`, which needs `LETTERMINT_API_URL` or throws. */
|
|
10
10
|
readonly transport?: Transport;
|
|
11
|
-
/** Files to attach, e.g. an invoice or credit note document.
|
|
11
|
+
/** Files to attach, e.g. an invoice or credit note document. The shipped transport accepts these. */
|
|
12
12
|
readonly attachments?: readonly Attachment[];
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* One template, rendered and handed to a transport.
|
|
16
16
|
*
|
|
17
|
-
* `transport` defaults to
|
|
18
|
-
* `MailerConfigError`
|
|
19
|
-
* instead of a silent no-op or an accidental live send
|
|
20
|
-
* `transport.ts`, `defaultTransport`).
|
|
17
|
+
* `transport` defaults to `defaultTransport()`, which throws
|
|
18
|
+
* `MailerConfigError` unless `LETTERMINT_API_URL` is set, so a missing
|
|
19
|
+
* config fails loudly instead of a silent no-op or an accidental live send
|
|
20
|
+
* (see `transport.ts`, `defaultTransport`).
|
|
21
21
|
*/
|
|
22
22
|
export declare function send<T extends TemplateName>(options: SendOptions<T>): Promise<SendResult>;
|
package/dist/send.js
CHANGED
|
@@ -10,10 +10,10 @@ function formatFrom(identity) {
|
|
|
10
10
|
/**
|
|
11
11
|
* One template, rendered and handed to a transport.
|
|
12
12
|
*
|
|
13
|
-
* `transport` defaults to
|
|
14
|
-
* `MailerConfigError`
|
|
15
|
-
* instead of a silent no-op or an accidental live send
|
|
16
|
-
* `transport.ts`, `defaultTransport`).
|
|
13
|
+
* `transport` defaults to `defaultTransport()`, which throws
|
|
14
|
+
* `MailerConfigError` unless `LETTERMINT_API_URL` is set, so a missing
|
|
15
|
+
* config fails loudly instead of a silent no-op or an accidental live send
|
|
16
|
+
* (see `transport.ts`, `defaultTransport`).
|
|
17
17
|
*/
|
|
18
18
|
export async function send(options) {
|
|
19
19
|
const render = templates[options.template];
|
package/dist/transport.d.ts
CHANGED
|
@@ -10,16 +10,6 @@ export interface OutgoingMail extends MailMessage {
|
|
|
10
10
|
export interface Transport {
|
|
11
11
|
send(message: OutgoingMail): Promise<SendResult>;
|
|
12
12
|
}
|
|
13
|
-
export interface DevTransportOptions {
|
|
14
|
-
/** Directory messages are written to. Default: `.mailer/dev`. */
|
|
15
|
-
readonly dir?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Writes every message to `<dir>/<timestamp>-<id>.json` instead of sending
|
|
19
|
-
* it. Never opens a network connection, so nothing that sends mail needs
|
|
20
|
-
* a live key to be tested.
|
|
21
|
-
*/
|
|
22
|
-
export declare function createDevTransport(options?: DevTransportOptions): Transport;
|
|
23
13
|
/**
|
|
24
14
|
* The minimal shape this package calls on `fetch` to speak Lettermint's
|
|
25
15
|
* Sending API directly -- the same no-SDK, injectable-`fetch` posture
|
|
@@ -31,6 +21,7 @@ export type LettermintFetchLike = (url: string, init: {
|
|
|
31
21
|
method: string;
|
|
32
22
|
headers: Record<string, string>;
|
|
33
23
|
body: string;
|
|
24
|
+
signal?: AbortSignal;
|
|
34
25
|
}) => Promise<{
|
|
35
26
|
ok: boolean;
|
|
36
27
|
status: number;
|
|
@@ -87,6 +78,12 @@ export interface LettermintTransportOptions {
|
|
|
87
78
|
/** Injectable `fetch`, used only when building the default client from
|
|
88
79
|
* `apiKey`. Defaults to `globalThis.fetch`. */
|
|
89
80
|
readonly fetch?: LettermintFetchLike;
|
|
81
|
+
/** The Sending API base URL, without `/send`. Default
|
|
82
|
+
* `https://api.lettermint.co/v1`. Must be https, except for a local sink
|
|
83
|
+
* host (`localhost`, `127.0.0.1`, `[::1]`), so a misconfigured value
|
|
84
|
+
* cannot send mail or the token over plain http. Ignored when `client`
|
|
85
|
+
* is given. */
|
|
86
|
+
readonly baseUrl?: string;
|
|
90
87
|
}
|
|
91
88
|
/**
|
|
92
89
|
* Thrown when Lettermint refuses to send because the `from` domain is not
|
|
@@ -121,11 +118,38 @@ export declare class LettermintDomainRefusedError extends Error {
|
|
|
121
118
|
* the network.
|
|
122
119
|
*/
|
|
123
120
|
export declare function createLettermintTransport(options: LettermintTransportOptions): Transport;
|
|
121
|
+
export interface MailpitTransportOptions {
|
|
122
|
+
/** The Mailpit base URL without `/api/v1/send`, e.g. `http://acme-stage-sink:8025`.
|
|
123
|
+
* Same rule as the Lettermint base URL: https, or http for a local or
|
|
124
|
+
* single-label internal host. A malformed value throws `MailerConfigError`. */
|
|
125
|
+
readonly baseUrl: string;
|
|
126
|
+
/** Injectable `fetch`. Defaults to `globalThis.fetch`. */
|
|
127
|
+
readonly fetch?: LettermintFetchLike;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Sends to a Mailpit server's send API (`POST <baseUrl>/api/v1/send`,
|
|
131
|
+
* https://mailpit.axllent.org/docs/api-v1/view.html#post-/api/v1/send), the
|
|
132
|
+
* stage mail sink. Sends no token header: a stage app holds no mail
|
|
133
|
+
* provider token. A non-2xx answer throws a fixed message with the status
|
|
134
|
+
* only, never the response body or the message content.
|
|
135
|
+
*/
|
|
136
|
+
export declare function createMailpitTransport(options: MailpitTransportOptions): Transport;
|
|
124
137
|
/**
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
138
|
+
* When `MAIL_SINK_URL` is set, returns a Mailpit transport to
|
|
139
|
+
* that URL. It wins over every other setting, `LETTERMINT_API_TOKEN` included,
|
|
140
|
+
* so a stage app never reaches the real provider. A malformed URL throws
|
|
141
|
+
* `MailerConfigError` here, before any send.
|
|
142
|
+
*
|
|
143
|
+
* Otherwise, when `LETTERMINT_API_URL` is set (in any `NODE_ENV`), returns a Lettermint
|
|
144
|
+
* transport that sends to that base URL with the token in
|
|
145
|
+
* `LETTERMINT_API_TOKEN`: how a stage app, a production build holding real
|
|
146
|
+
* addresses, or the local stack sends every mail to a sink server. A
|
|
147
|
+
* malformed URL, or a URL with no token, throws `MailerConfigError` here,
|
|
148
|
+
* before any send.
|
|
149
|
+
*
|
|
150
|
+
* Otherwise throws `MailerConfigError`: this package ships no stand-in
|
|
151
|
+
* transport (docs/adr/0008-remove-dev-transport.md), so `send()` must be
|
|
152
|
+
* given an explicit transport, or the caller must point `LETTERMINT_API_URL`
|
|
153
|
+
* at a sink.
|
|
130
154
|
*/
|
|
131
155
|
export declare function defaultTransport(): Transport;
|
package/dist/transport.js
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
1
|
import { MailerConfigError } from './types.js';
|
|
5
|
-
/**
|
|
6
|
-
* Writes every message to `<dir>/<timestamp>-<id>.json` instead of sending
|
|
7
|
-
* it. Never opens a network connection, so nothing that sends mail needs
|
|
8
|
-
* a live key to be tested.
|
|
9
|
-
*/
|
|
10
|
-
export function createDevTransport(options = {}) {
|
|
11
|
-
const dir = options.dir ?? '.mailer/dev';
|
|
12
|
-
return {
|
|
13
|
-
async send(message) {
|
|
14
|
-
mkdirSync(dir, { recursive: true });
|
|
15
|
-
const id = randomUUID();
|
|
16
|
-
const path = join(dir, `${Date.now()}-${id}.json`);
|
|
17
|
-
writeFileSync(path, JSON.stringify(message, null, 2));
|
|
18
|
-
return { id, transport: 'dev' };
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
2
|
/**
|
|
23
3
|
* Thrown when Lettermint refuses to send because the `from` domain is not
|
|
24
4
|
* one the sending project may use -- either unverified, or verified but
|
|
@@ -39,7 +19,33 @@ export class LettermintDomainRefusedError extends Error {
|
|
|
39
19
|
this.name = 'LettermintDomainRefusedError';
|
|
40
20
|
}
|
|
41
21
|
}
|
|
42
|
-
const
|
|
22
|
+
const LETTERMINT_BASE_URL = 'https://api.lettermint.co/v1';
|
|
23
|
+
const LETTERMINT_TIMEOUT_MS = 30_000;
|
|
24
|
+
const LOCAL_SINK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
25
|
+
/**
|
|
26
|
+
* Validates a Sending API base URL where it is read and returns it without a
|
|
27
|
+
* trailing slash. Rule: https, or http only for a local sink host. Anything
|
|
28
|
+
* else throws `MailerConfigError`, so a bad value fails at start, not at the
|
|
29
|
+
* first send.
|
|
30
|
+
*/
|
|
31
|
+
function parseBaseUrl(raw, source) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new MailerConfigError(`mailer: ${source} is not a valid URL.`);
|
|
38
|
+
}
|
|
39
|
+
// A single-label host (a container name such as `acme-stage-sink`) only resolves on the internal network.
|
|
40
|
+
const local = url.protocol === 'http:' && (LOCAL_SINK_HOSTS.has(url.hostname) || !url.hostname.includes('.'));
|
|
41
|
+
if (url.protocol !== 'https:' && !local) {
|
|
42
|
+
throw new MailerConfigError(`mailer: ${source} must be an https URL (http is allowed only for localhost, 127.0.0.1, [::1] or a single-label internal host).`);
|
|
43
|
+
}
|
|
44
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
45
|
+
throw new MailerConfigError(`mailer: ${source} must not carry credentials, a query string or a fragment.`);
|
|
46
|
+
}
|
|
47
|
+
return raw.replace(/\/+$/, '');
|
|
48
|
+
}
|
|
43
49
|
/**
|
|
44
50
|
* Matches Lettermint's confirmed domain-restriction refusal message
|
|
45
51
|
* (`LettermintDomainRefusedError`'s own doc comment has the exact quote and
|
|
@@ -57,13 +63,14 @@ function defaultLettermintFetch() {
|
|
|
57
63
|
}
|
|
58
64
|
return globalThis.fetch.bind(globalThis);
|
|
59
65
|
}
|
|
60
|
-
function createLettermintClient(apiKey, fetchImpl) {
|
|
66
|
+
function createLettermintClient(apiKey, fetchImpl, baseUrl) {
|
|
61
67
|
return {
|
|
62
68
|
async send(payload) {
|
|
63
|
-
const response = await fetchImpl(
|
|
69
|
+
const response = await fetchImpl(`${baseUrl}/send`, {
|
|
64
70
|
method: 'POST',
|
|
65
71
|
headers: { 'x-lettermint-token': apiKey, 'content-type': 'application/json' },
|
|
66
72
|
body: JSON.stringify(payload),
|
|
73
|
+
signal: AbortSignal.timeout(LETTERMINT_TIMEOUT_MS),
|
|
67
74
|
});
|
|
68
75
|
const body = (await response.json());
|
|
69
76
|
if (!response.ok) {
|
|
@@ -95,8 +102,9 @@ export function createLettermintTransport(options) {
|
|
|
95
102
|
if (!options.client && !options.apiKey) {
|
|
96
103
|
throw new MailerConfigError('createLettermintTransport needs either "apiKey" or "client".');
|
|
97
104
|
}
|
|
105
|
+
const baseUrl = options.baseUrl === undefined ? LETTERMINT_BASE_URL : parseBaseUrl(options.baseUrl, 'baseUrl');
|
|
98
106
|
const client = options.client ??
|
|
99
|
-
createLettermintClient(options.apiKey, options.fetch ?? defaultLettermintFetch());
|
|
107
|
+
createLettermintClient(options.apiKey, options.fetch ?? defaultLettermintFetch(), baseUrl);
|
|
100
108
|
return {
|
|
101
109
|
async send(message) {
|
|
102
110
|
const { message_id, status } = await client.send({
|
|
@@ -120,16 +128,86 @@ export function createLettermintTransport(options) {
|
|
|
120
128
|
},
|
|
121
129
|
};
|
|
122
130
|
}
|
|
131
|
+
/** Splits `Name <addr>` or a bare address into Mailpit's `{ Email, Name }`. */
|
|
132
|
+
function toMailpitAddress(formatted) {
|
|
133
|
+
const match = /^(.*)<([^<>]+)>\s*$/.exec(formatted);
|
|
134
|
+
if (!match)
|
|
135
|
+
return { Email: formatted.trim(), Name: '' };
|
|
136
|
+
return { Email: match[2].trim(), Name: match[1].trim() };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sends to a Mailpit server's send API (`POST <baseUrl>/api/v1/send`,
|
|
140
|
+
* https://mailpit.axllent.org/docs/api-v1/view.html#post-/api/v1/send), the
|
|
141
|
+
* stage mail sink. Sends no token header: a stage app holds no mail
|
|
142
|
+
* provider token. A non-2xx answer throws a fixed message with the status
|
|
143
|
+
* only, never the response body or the message content.
|
|
144
|
+
*/
|
|
145
|
+
export function createMailpitTransport(options) {
|
|
146
|
+
const baseUrl = parseBaseUrl(options.baseUrl, 'baseUrl');
|
|
147
|
+
const fetchImpl = options.fetch ?? defaultLettermintFetch();
|
|
148
|
+
return {
|
|
149
|
+
async send(message) {
|
|
150
|
+
const payload = {
|
|
151
|
+
From: toMailpitAddress(message.from),
|
|
152
|
+
To: [toMailpitAddress(message.to)],
|
|
153
|
+
Subject: message.subject,
|
|
154
|
+
Text: message.text,
|
|
155
|
+
HTML: message.html,
|
|
156
|
+
...(message.replyTo ? { ReplyTo: [toMailpitAddress(message.replyTo)] } : {}),
|
|
157
|
+
...(message.attachments
|
|
158
|
+
? {
|
|
159
|
+
Attachments: message.attachments.map((attachment) => ({
|
|
160
|
+
Filename: attachment.filename,
|
|
161
|
+
Content: attachment.content,
|
|
162
|
+
...(attachment.contentType ? { ContentType: attachment.contentType } : {}),
|
|
163
|
+
})),
|
|
164
|
+
}
|
|
165
|
+
: {}),
|
|
166
|
+
};
|
|
167
|
+
const response = await fetchImpl(`${baseUrl}/api/v1/send`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'content-type': 'application/json' },
|
|
170
|
+
body: JSON.stringify(payload),
|
|
171
|
+
signal: AbortSignal.timeout(LETTERMINT_TIMEOUT_MS),
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error(`mailer: the mail sink refused the message (status ${response.status}).`);
|
|
175
|
+
}
|
|
176
|
+
const body = (await response.json());
|
|
177
|
+
return { id: body.ID ?? 'unknown', transport: 'mailpit' };
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
123
181
|
/**
|
|
124
|
-
* `
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
182
|
+
* When `MAIL_SINK_URL` is set, returns a Mailpit transport to
|
|
183
|
+
* that URL. It wins over every other setting, `LETTERMINT_API_TOKEN` included,
|
|
184
|
+
* so a stage app never reaches the real provider. A malformed URL throws
|
|
185
|
+
* `MailerConfigError` here, before any send.
|
|
186
|
+
*
|
|
187
|
+
* Otherwise, when `LETTERMINT_API_URL` is set (in any `NODE_ENV`), returns a Lettermint
|
|
188
|
+
* transport that sends to that base URL with the token in
|
|
189
|
+
* `LETTERMINT_API_TOKEN`: how a stage app, a production build holding real
|
|
190
|
+
* addresses, or the local stack sends every mail to a sink server. A
|
|
191
|
+
* malformed URL, or a URL with no token, throws `MailerConfigError` here,
|
|
192
|
+
* before any send.
|
|
193
|
+
*
|
|
194
|
+
* Otherwise throws `MailerConfigError`: this package ships no stand-in
|
|
195
|
+
* transport (docs/adr/0008-remove-dev-transport.md), so `send()` must be
|
|
196
|
+
* given an explicit transport, or the caller must point `LETTERMINT_API_URL`
|
|
197
|
+
* at a sink.
|
|
129
198
|
*/
|
|
130
199
|
export function defaultTransport() {
|
|
131
|
-
|
|
132
|
-
|
|
200
|
+
const mailSinkUrl = process.env.MAIL_SINK_URL;
|
|
201
|
+
if (mailSinkUrl)
|
|
202
|
+
return createMailpitTransport({ baseUrl: mailSinkUrl });
|
|
203
|
+
const sinkUrl = process.env.LETTERMINT_API_URL;
|
|
204
|
+
if (sinkUrl) {
|
|
205
|
+
const baseUrl = parseBaseUrl(sinkUrl, 'LETTERMINT_API_URL');
|
|
206
|
+
const apiKey = process.env.LETTERMINT_API_TOKEN;
|
|
207
|
+
if (!apiKey) {
|
|
208
|
+
throw new MailerConfigError('mailer: LETTERMINT_API_URL is set but LETTERMINT_API_TOKEN is not.');
|
|
209
|
+
}
|
|
210
|
+
return createLettermintTransport({ apiKey, baseUrl });
|
|
133
211
|
}
|
|
134
|
-
|
|
212
|
+
throw new MailerConfigError('send() needs an explicit transport: pass { transport: createLettermintTransport({ apiKey }) }, or set MAIL_SINK_URL (or LETTERMINT_API_URL) to reach a sink.');
|
|
135
213
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface MailMessage {
|
|
|
60
60
|
/** What a transport returns once it has accepted a message. */
|
|
61
61
|
export interface SendResult {
|
|
62
62
|
readonly id: string;
|
|
63
|
-
readonly transport: '
|
|
63
|
+
readonly transport: 'lettermint' | 'mailpit';
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* `send()` has no usable default (no transport given, and either
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/mailer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One send(template, to, data) call for the estate's transactional email, over Lettermint
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "One send(template, to, data) call for the estate's transactional email, over Lettermint.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/wtfalch/mailer",
|