@dotpress/mailer 0.0.0-stage → 2.0.0-alpha.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 +491 -2
- package/dist/aws-ses.d.ts +30 -0
- package/dist/aws-ses.js +108 -0
- package/dist/aws-ses.js.map +1 -0
- package/dist/brevo.d.ts +23 -0
- package/dist/brevo.js +115 -0
- package/dist/brevo.js.map +1 -0
- package/dist/chunk-2ZABFOZQ.js +15 -0
- package/dist/chunk-2ZABFOZQ.js.map +1 -0
- package/dist/chunk-FRMKUAF4.js +88 -0
- package/dist/chunk-FRMKUAF4.js.map +1 -0
- package/dist/chunk-GOAXQ2GE.js +28 -0
- package/dist/chunk-GOAXQ2GE.js.map +1 -0
- package/dist/chunk-QYVN5N3B.js +221 -0
- package/dist/chunk-QYVN5N3B.js.map +1 -0
- package/dist/http-B35l9W1d.d.ts +9 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +649 -0
- package/dist/index.js.map +1 -0
- package/dist/mailer-CmQCJ0qc.d.ts +339 -0
- package/dist/mailgun.d.ts +31 -0
- package/dist/mailgun.js +100 -0
- package/dist/mailgun.js.map +1 -0
- package/dist/mailjet.d.ts +28 -0
- package/dist/mailjet.js +104 -0
- package/dist/mailjet.js.map +1 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.js +21 -0
- package/dist/plugin.js.map +1 -0
- package/dist/postmark.d.ts +27 -0
- package/dist/postmark.js +149 -0
- package/dist/postmark.js.map +1 -0
- package/dist/resend.d.ts +31 -0
- package/dist/resend.js +92 -0
- package/dist/resend.js.map +1 -0
- package/dist/scaleway.d.ts +31 -0
- package/dist/scaleway.js +113 -0
- package/dist/scaleway.js.map +1 -0
- package/dist/sendgrid.d.ts +26 -0
- package/dist/sendgrid.js +121 -0
- package/dist/sendgrid.js.map +1 -0
- package/dist/smtp.d.ts +45 -0
- package/dist/smtp.js +61 -0
- package/dist/smtp.js.map +1 -0
- package/package.json +139 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Renaud Guyon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,492 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dotpress/mailer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, multilingual email templates for [Dotpress](https://dotpress.dev), written in [MJML](https://mjml.io) or HTML, with shared layouts, and interchangeable providers to send them. Works without a Dotpress app: in a script, a worker or a Lambda function.
|
|
4
|
+
|
|
5
|
+
> 🚧 Dotpress V2 is under active development and not published yet.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @dotpress/mailer zod
|
|
9
|
+
pnpm add mjml # for MJML templates
|
|
10
|
+
pnpm add @dotpress/core # for mailerPlugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
createConsoleProvider,
|
|
18
|
+
createMailer,
|
|
19
|
+
defineLayout,
|
|
20
|
+
defineTemplate,
|
|
21
|
+
} from '@dotpress/mailer'
|
|
22
|
+
import { z } from 'zod'
|
|
23
|
+
|
|
24
|
+
export const baseLayout = defineLayout({
|
|
25
|
+
translations: {
|
|
26
|
+
footer: { en: 'Sent by Acme.', fr: 'Envoyé par Acme.' },
|
|
27
|
+
},
|
|
28
|
+
mjml: ({ t, content, locale }) => `
|
|
29
|
+
<mjml lang="${locale}">
|
|
30
|
+
<mj-body background-color="#f6f6f6">
|
|
31
|
+
<mj-section>
|
|
32
|
+
<mj-column><mj-image src="https://cdn.acme.com/logo.png" width="40px" /></mj-column>
|
|
33
|
+
</mj-section>
|
|
34
|
+
${content}
|
|
35
|
+
<mj-section>
|
|
36
|
+
<mj-column><mj-text font-size="12px">${t('footer')}</mj-text></mj-column>
|
|
37
|
+
</mj-section>
|
|
38
|
+
</mj-body>
|
|
39
|
+
</mjml>`,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const resetPassword = defineTemplate({
|
|
43
|
+
name: 'resetPassword',
|
|
44
|
+
layout: baseLayout,
|
|
45
|
+
schema: z.object({ firstName: z.string(), resetUrl: z.url() }),
|
|
46
|
+
subject: {
|
|
47
|
+
en: 'Reset your password, {{firstName}}',
|
|
48
|
+
fr: 'Réinitialisez votre mot de passe, {{firstName}}',
|
|
49
|
+
},
|
|
50
|
+
translations: {
|
|
51
|
+
intro: {
|
|
52
|
+
en: 'Click the button below to set a new password.',
|
|
53
|
+
fr: 'Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe.',
|
|
54
|
+
},
|
|
55
|
+
cta: { en: 'Reset my password', fr: 'Changer mon mot de passe' },
|
|
56
|
+
},
|
|
57
|
+
mjml: ({ t }) => `
|
|
58
|
+
<mj-section background-color="#fff">
|
|
59
|
+
<mj-column>
|
|
60
|
+
<mj-text>Hello {{firstName}},<br /><br />${t('intro')}</mj-text>
|
|
61
|
+
<mj-button href="{{{resetUrl}}}">${t('cta')}</mj-button>
|
|
62
|
+
</mj-column>
|
|
63
|
+
</mj-section>`,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const mailer = createMailer({
|
|
67
|
+
provider: createConsoleProvider(), // prints messages, for development
|
|
68
|
+
from: 'Acme <noreply@acme.com>',
|
|
69
|
+
defaultLocale: 'en',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await mailer.send(resetPassword, {
|
|
73
|
+
to: 'Ada Lovelace <ada@example.com>',
|
|
74
|
+
locale: 'fr-CA', // fr-CA → fr → defaultLocale
|
|
75
|
+
data: { firstName: 'Ada', resetUrl: 'https://app.acme.com/reset?token=…' },
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How a template is rendered
|
|
80
|
+
|
|
81
|
+
1. **Translations**: the source function runs with `t()`, which returns the texts of the rendered locale. The layout function then receives the result as `content`, with its own `t()`.
|
|
82
|
+
2. **MJML** compiles the result to HTML. This step only depends on the template and the locale, so its result is cached.
|
|
83
|
+
3. **Mustache** inserts the data (`{{firstName}}`) in the HTML, the subject and the text versions.
|
|
84
|
+
|
|
85
|
+
Since translations are inserted before Mustache, they can use the data too: `{ en: 'Hello {{firstName}}' }`. They are inserted as-is: they may contain HTML (`<br />`, `<strong>`).
|
|
86
|
+
|
|
87
|
+
## Templates
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
defineTemplate({
|
|
91
|
+
name: 'welcome', // used in errors and logs
|
|
92
|
+
layout: baseLayout, // optional
|
|
93
|
+
schema: z.object({ … }), // optional: without a schema, the template accepts no data
|
|
94
|
+
subject: 'Welcome', // or { en: 'Welcome', fr: 'Bienvenue' }
|
|
95
|
+
translations: { … }, // optional
|
|
96
|
+
mjml: ({ t, locale }) => `…`, // or html, a string or a function
|
|
97
|
+
text: ({ t, locale }) => `…`, // optional, a string or a function
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Templates are checked when they are defined: unknown options, a missing source, texts that do not have the same locales, or a layout without translations in a locale of the template throw a `TypeError`. TypeScript reports most of these mistakes too.
|
|
102
|
+
|
|
103
|
+
### HTML templates
|
|
104
|
+
|
|
105
|
+
Use `html` instead of `mjml` for HTML written by hand or produced by another tool. MJML is then never loaded. A layout and its templates must use the same format.
|
|
106
|
+
|
|
107
|
+
### Layouts
|
|
108
|
+
|
|
109
|
+
A layout is a function that receives the template source as `content`. In MJML, place `content` inside `<mj-body>`: templates provide `<mj-section>` elements, so each section can have its own background.
|
|
110
|
+
|
|
111
|
+
Layouts and templates have separate translations: the `t()` of a layout only reads the layout translations. A layout without translations can be used in any locale.
|
|
112
|
+
|
|
113
|
+
Reusable blocks are plain functions:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const button = (href: string, label: string) =>
|
|
117
|
+
`<mj-button href="${href}" background-color="#1992d4">${label}</mj-button>`
|
|
118
|
+
|
|
119
|
+
mjml: ({ t }) => `<mj-section><mj-column>${button('{{{url}}}', t('cta'))}</mj-column></mj-section>`
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`mj-include` is ignored: templates do not read files.
|
|
123
|
+
|
|
124
|
+
### Locales
|
|
125
|
+
|
|
126
|
+
The locales of a template are those of its `subject` and `translations`; every text needs all of them. A template without translations takes the locales of its layout. A template and layout without translations ignore the locale.
|
|
127
|
+
|
|
128
|
+
The rendered locale is the first available one among:
|
|
129
|
+
|
|
130
|
+
1. the requested locale (`fr-CA`), compared case-insensitively;
|
|
131
|
+
2. its base language (`fr`);
|
|
132
|
+
3. `defaultLocale`, then its base language.
|
|
133
|
+
|
|
134
|
+
Otherwise, rendering fails. `t()` with an unknown key fails too.
|
|
135
|
+
|
|
136
|
+
### Data
|
|
137
|
+
|
|
138
|
+
The data is validated by the schema (`safeParseAsync`, so async refinements work), and the parsed data is rendered. Invalid data throws a `MailDataError` with the list of issues.
|
|
139
|
+
|
|
140
|
+
### Escaping
|
|
141
|
+
|
|
142
|
+
| Syntax | HTML version | Subject and text versions |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `{{value}}` | `& < > " '` escaped | inserted as-is |
|
|
145
|
+
| `{{{value}}}` | inserted as-is | inserted as-is |
|
|
146
|
+
|
|
147
|
+
Unlike the default escaping of Mustache, `/`, `=` and `` ` `` are not escaped: many email clients do not decode them in links. Use `{{{url}}}` for URLs built by your app, and `{{value}}` for everything else, in particular user input. Mustache sections work (`{{#items}}…{{/items}}`); partials do not.
|
|
148
|
+
|
|
149
|
+
### Subject
|
|
150
|
+
|
|
151
|
+
The subject is rendered by Mustache without HTML escaping, then line breaks are replaced with spaces: a subject is a single line.
|
|
152
|
+
|
|
153
|
+
### Text version
|
|
154
|
+
|
|
155
|
+
Without a `text` source, the text version is converted from the HTML version by [html-to-text](https://github.com/html-to-text/node-html-to-text): lines wrapped at 78 characters, links followed by their URL, images and hidden elements (like the MJML preview text) left out.
|
|
156
|
+
|
|
157
|
+
## Rendering without sending
|
|
158
|
+
|
|
159
|
+
`mailer.render(template, { locale, data })` returns `{ template, locale, subject, html, text }`, with the default locale of the mailer. `renderMail(template, { locale, defaultLocale, data })` does the same without a mailer. Both are useful to preview templates, and to test them:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
for (const locale of resetPassword.locales) {
|
|
163
|
+
const mail = await renderMail(resetPassword, { locale, data: sampleData })
|
|
164
|
+
expect(mail.html).toContain('reset?token=')
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Sending
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const mailer = createMailer({
|
|
172
|
+
provider, // see Providers
|
|
173
|
+
from: 'Acme <noreply@acme.com>', // default sender
|
|
174
|
+
replyTo: 'support@acme.com', // optional default
|
|
175
|
+
defaultLocale: 'en',
|
|
176
|
+
logger, // optional: logs each sent message
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `mailer.send(template, options)`
|
|
181
|
+
|
|
182
|
+
Renders a template and sends it. Returns `{ messageId }`, the identifier given by the provider (if any).
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await mailer.send(invoiceMail, {
|
|
186
|
+
to: ['ada@example.com', { name: 'Grace Hopper', address: 'grace@example.com' }],
|
|
187
|
+
cc: 'accounting@example.com',
|
|
188
|
+
bcc: ['archive@example.com'],
|
|
189
|
+
replyTo: 'billing@acme.com', // overrides the default
|
|
190
|
+
from: 'Acme Billing <billing@acme.com>', // overrides the default
|
|
191
|
+
subject: 'Your invoice', // overrides the template subject
|
|
192
|
+
locale: user.locale,
|
|
193
|
+
data: { invoiceNumber: 'F-2026-001' },
|
|
194
|
+
attachments: [
|
|
195
|
+
{ filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' },
|
|
196
|
+
{ filename: 'logo.png', content: logoBytes, contentId: 'logo' }, // <img src="cid:logo">
|
|
197
|
+
],
|
|
198
|
+
headers: { 'List-Unsubscribe': '<https://acme.com/unsubscribe?u=42>' },
|
|
199
|
+
tags: ['invoice'], // see Tags and metadata
|
|
200
|
+
metadata: { invoiceId: 'F-2026-001' },
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **Addresses**: `'ada@example.com'`, `'Ada Lovelace <ada@example.com>'` or `{ name, address }`, alone or in a list. `to` needs at least one address.
|
|
205
|
+
- **Attachments**: `content` is a `Uint8Array` (a Node.js `Buffer` works) or a string. `contentId` makes the attachment inline.
|
|
206
|
+
- **Headers**: additional headers only. `From`, `To`, `Subject`, `Content-Type` and the other headers set by the mailer are rejected.
|
|
207
|
+
|
|
208
|
+
Recipients, attachments, headers, tags and metadata are checked before rendering: invalid values (an address without `@`, a line break in a name or a header) throw a `MailMessageError`.
|
|
209
|
+
|
|
210
|
+
### `mailer.sendMessage(options)`
|
|
211
|
+
|
|
212
|
+
Sends a message rendered elsewhere: the same options, with `subject`, and `html`, `text` or both. Without `text`, the text version is converted from `html`.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
await mailer.sendMessage({ to, subject: 'Export ready', html: '<p>…</p>' })
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Tags and metadata
|
|
219
|
+
|
|
220
|
+
Most providers can group messages in their statistics (**tags**), and return key/value pairs in their webhooks to link an event (delivery, bounce, click…) to your app (**metadata**).
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
await mailer.send(invoiceMail, {
|
|
224
|
+
to,
|
|
225
|
+
data,
|
|
226
|
+
tags: ['invoice', 'billing'],
|
|
227
|
+
metadata: { invoiceId: 'F-2026-001', userId: '42' },
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- **Tags** and **metadata keys** have 1 to 256 letters, digits, `_` or `-`: the strictest rule among providers, so they are valid everywhere. Duplicate tags are removed.
|
|
232
|
+
- **Metadata values** are strings on one line.
|
|
233
|
+
|
|
234
|
+
Each provider translates them to its own API. A provider without tags or metadata ignores them: switching providers never breaks a call, only the grouping is lost.
|
|
235
|
+
|
|
236
|
+
| Provider | `tags` | `metadata` |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| SendGrid | `categories` | `custom_args` |
|
|
239
|
+
| Postmark | `Tag` (a single tag) | `Metadata` |
|
|
240
|
+
| Brevo | `tags` | ignored |
|
|
241
|
+
| Mailjet | ignored | `EventPayload` (JSON) |
|
|
242
|
+
| Mailgun | `o:tag` | `v:` variables |
|
|
243
|
+
| Scaleway | ignored | ignored |
|
|
244
|
+
| Amazon SES | `EmailTags`, with the value `true` | `EmailTags` |
|
|
245
|
+
| Resend | `tags`, with the value `true` | `tags` |
|
|
246
|
+
| SMTP, nodemailer | ignored | ignored |
|
|
247
|
+
| console | printed | printed |
|
|
248
|
+
| memory | kept in the message | kept in the message |
|
|
249
|
+
|
|
250
|
+
The limits of each provider (number of tags, characters of the values…) are listed in [Limits of the providers](#limits-of-the-providers).
|
|
251
|
+
|
|
252
|
+
## Providers
|
|
253
|
+
|
|
254
|
+
| Provider | Import | Use |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| `createMemoryProvider()` | `@dotpress/mailer` | tests: keeps messages in `outbox.sent`, checked by `expectMailSent()` of `@dotpress/testing` |
|
|
257
|
+
| `createConsoleProvider()` | `@dotpress/mailer` | development: prints the sender, recipients, subject, attachments, tags and metadata |
|
|
258
|
+
| `createSmtpProvider()` | `@dotpress/mailer/smtp` | any SMTP server, through nodemailer |
|
|
259
|
+
| `createNodemailerProvider()` | `@dotpress/mailer/smtp` | any nodemailer transporter (sendmail, stream…) |
|
|
260
|
+
| `createSesProvider()` | `@dotpress/mailer/aws-ses` | Amazon SES (API v2), through the AWS SDK |
|
|
261
|
+
| `createResendProvider()` | `@dotpress/mailer/resend` | Resend, through its SDK |
|
|
262
|
+
| `createSendgridProvider()` | `@dotpress/mailer/sendgrid` | Twilio SendGrid (Mail Send API v3), through `fetch` |
|
|
263
|
+
| `createPostmarkProvider()` | `@dotpress/mailer/postmark` | Postmark (Email API), through `fetch` |
|
|
264
|
+
| `createBrevoProvider()` | `@dotpress/mailer/brevo` | Brevo (transactional email API v3), through `fetch` |
|
|
265
|
+
| `createMailjetProvider()` | `@dotpress/mailer/mailjet` | Mailjet (Send API v3.1), through `fetch` |
|
|
266
|
+
| `createMailgunProvider()` | `@dotpress/mailer/mailgun` | Mailgun (messages API), through `fetch` |
|
|
267
|
+
| `createScalewayProvider()` | `@dotpress/mailer/scaleway` | Scaleway Transactional Email, through `fetch` |
|
|
268
|
+
|
|
269
|
+
Every provider sends the sender and reply-to addresses, multiple `to`, `cc` and `bcc` recipients, the HTML and text versions, attachments (inline ones included) and additional headers, within the limits of its API: a message that the API cannot accept throws a `MailMessageError` before any request (see [Limits of the providers](#limits-of-the-providers)).
|
|
270
|
+
|
|
271
|
+
The providers based on an HTTP API (SendGrid, Postmark, Brevo, Mailjet, Mailgun, Scaleway) have no dependency, and take two common options: `baseUrl` (region, proxy, tests) and `fetch` (replaces the global `fetch`). Their errors are thrown as `MailSendError`, with the HTTP `status` and the message of the API.
|
|
272
|
+
|
|
273
|
+
The console provider prints the text version only with `{ body: true }`: messages often contain secrets (reset links, login codes). Never use it as a fallback when the configuration of the real provider is missing: messages would be lost silently.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const outbox = createMemoryProvider()
|
|
277
|
+
const mailer = createMailer({ provider: outbox, from: 'noreply@example.com' })
|
|
278
|
+
|
|
279
|
+
await signUp('ada@example.com')
|
|
280
|
+
expect(outbox.last()).toMatchObject({
|
|
281
|
+
to: [{ address: 'ada@example.com' }],
|
|
282
|
+
template: 'welcome',
|
|
283
|
+
})
|
|
284
|
+
outbox.clear()
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### SMTP
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
pnpm add nodemailer
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { createSmtpProvider } from '@dotpress/mailer/smtp'
|
|
295
|
+
|
|
296
|
+
const provider = createSmtpProvider({
|
|
297
|
+
host: 'smtp.example.com',
|
|
298
|
+
port: 465,
|
|
299
|
+
secure: true,
|
|
300
|
+
auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASSWORD! },
|
|
301
|
+
pool: true, // optional: reuses connections
|
|
302
|
+
})
|
|
303
|
+
// or createSmtpProvider('smtps://user:password@smtp.example.com')
|
|
304
|
+
|
|
305
|
+
// On shutdown, with pool: true:
|
|
306
|
+
provider.close()
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The options are those of [nodemailer](https://nodemailer.com/smtp/) (nodemailer 10). Most email services also have an SMTP interface: Amazon SES, Postmark, Mailgun, Brevo, Mailjet… SMTP errors (`cause.responseCode`, like `550`) are thrown as `MailSendError`.
|
|
310
|
+
|
|
311
|
+
`createNodemailerProvider(transporter, name?)` wraps any nodemailer transporter, for instance `nodemailer.createTransport({ sendmail: true })`.
|
|
312
|
+
|
|
313
|
+
The SMTP provider runs on Node.js only.
|
|
314
|
+
|
|
315
|
+
### Amazon SES
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
pnpm add @aws-sdk/client-sesv2
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { createSesProvider } from '@dotpress/mailer/aws-ses'
|
|
323
|
+
|
|
324
|
+
const provider = createSesProvider({
|
|
325
|
+
clientConfig: { region: 'eu-west-3' }, // credentials from the environment
|
|
326
|
+
configurationSetName: 'transactional', // optional
|
|
327
|
+
})
|
|
328
|
+
// or createSesProvider({ client: existingSesV2Client })
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Messages are sent with the `SendEmail` operation of the SES API v2 (simple content: subject, HTML and text versions, headers and attachments; `@aws-sdk/client-sesv2` 3.782 or later). Display names with non-ASCII characters are encoded as SES requires. SES errors are thrown as `MailSendError`, with the HTTP `status` and the error name (`MessageRejected: Email address is not verified.`).
|
|
332
|
+
|
|
333
|
+
On AWS Lambda, the Node.js runtimes include the AWS SDK: it does not need to be bundled, as long as the version of the runtime is 3.782 or later.
|
|
334
|
+
|
|
335
|
+
### Resend
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
pnpm add resend
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { createResendProvider } from '@dotpress/mailer/resend'
|
|
343
|
+
|
|
344
|
+
const provider = createResendProvider({ apiKey: process.env.RESEND_API_KEY })
|
|
345
|
+
// or createResendProvider({ client: existingResendClient })
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Without `apiKey`, the SDK reads the `RESEND_API_KEY` environment variable. Resend errors are thrown as `MailSendError`, with the HTTP `status` and the error name (`validation_error: …`).
|
|
349
|
+
|
|
350
|
+
### SendGrid
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { createSendgridProvider } from '@dotpress/mailer/sendgrid'
|
|
354
|
+
|
|
355
|
+
const provider = createSendgridProvider({
|
|
356
|
+
apiKey: process.env.SENDGRID_API_KEY!,
|
|
357
|
+
// baseUrl: 'https://api.eu.sendgrid.com', // EU regional subusers
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The provider calls the [Mail Send API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send) (v3) with `fetch`: no SDK to install. The message ID is read from the `X-Message-Id` response header, when present. API errors are thrown as `MailSendError`, with the HTTP `status` and the messages of the response (`400 Invalid from email address`). A `fetch` option replaces the global `fetch` (proxy, tests).
|
|
362
|
+
|
|
363
|
+
### Postmark
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { createPostmarkProvider } from '@dotpress/mailer/postmark'
|
|
367
|
+
|
|
368
|
+
const provider = createPostmarkProvider({
|
|
369
|
+
serverToken: process.env.POSTMARK_SERVER_TOKEN!, // POSTMARK_API_TEST: test mode
|
|
370
|
+
messageStream: 'outbound', // optional, the transactional stream by default
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The provider calls the [Email API](https://postmarkapp.com/developer/api/email-api). The message ID is the `MessageID` of the response. API errors include the Postmark `ErrorCode` (`422 Invalid email request (ErrorCode 300)`).
|
|
375
|
+
|
|
376
|
+
### Brevo
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { createBrevoProvider } from '@dotpress/mailer/brevo'
|
|
380
|
+
|
|
381
|
+
const provider = createBrevoProvider({ apiKey: process.env.BREVO_API_KEY! })
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The provider calls the [transactional email API](https://developers.brevo.com/reference/sendtransacemail). The message ID is the `messageId` of the response. API errors include the Brevo `code` (`400 invalid_parameter: …`).
|
|
385
|
+
|
|
386
|
+
### Mailjet
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { createMailjetProvider } from '@dotpress/mailer/mailjet'
|
|
390
|
+
|
|
391
|
+
const provider = createMailjetProvider({
|
|
392
|
+
apiKey: process.env.MAILJET_API_KEY!,
|
|
393
|
+
apiSecret: process.env.MAILJET_API_SECRET!,
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The provider calls the [Send API v3.1](https://dev.mailjet.com/email/reference/send-emails/). Mailjet returns an identifier per recipient: the message ID is the `MessageUUID` of the first `to` recipient (the numeric `MessageID` when the UUID is missing). Metadata is sent as `EventPayload` (JSON), returned by the event webhooks. A message rejected in a successful response is reported as an error too.
|
|
398
|
+
|
|
399
|
+
### Mailgun
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { createMailgunProvider } from '@dotpress/mailer/mailgun'
|
|
403
|
+
|
|
404
|
+
const provider = createMailgunProvider({
|
|
405
|
+
apiKey: process.env.MAILGUN_API_KEY!, // or a sending key of the domain
|
|
406
|
+
domain: 'mg.example.com',
|
|
407
|
+
// baseUrl: 'https://api.eu.mailgun.net', // domains of the EU region
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The provider calls the [messages API](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages) with `multipart/form-data` (`FormData`). Reply-to addresses and additional headers are sent as `h:` options, tags as `o:tag` and metadata as `v:` variables. Mailgun references inline images by file name: an inline attachment is sent with its `contentId` as file name, so `<img src="cid:logo">` works as with the other providers. The message ID is the `id` of the response.
|
|
412
|
+
|
|
413
|
+
### Scaleway
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { createScalewayProvider } from '@dotpress/mailer/scaleway'
|
|
417
|
+
|
|
418
|
+
const provider = createScalewayProvider({
|
|
419
|
+
secretKey: process.env.SCW_SECRET_KEY!,
|
|
420
|
+
projectId: process.env.SCW_DEFAULT_PROJECT_ID!,
|
|
421
|
+
region: 'fr-par', // default
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The provider calls the Transactional Email API (`v1alpha1`), with the request format of the official Scaleway SDK. The API has no reply-to field: reply-to addresses are sent as a `Reply-To` additional header. Scaleway returns an email per recipient: the message ID is the `id` of the first one, usable with the API to get the status of the email. Tags and metadata are ignored.
|
|
426
|
+
|
|
427
|
+
### Limits of the providers
|
|
428
|
+
|
|
429
|
+
Checked before calling the API; they throw a `MailMessageError`.
|
|
430
|
+
|
|
431
|
+
| Provider | Limits |
|
|
432
|
+
| --- | --- |
|
|
433
|
+
| SendGrid | 10 tags of 255 characters at most; metadata of 10,000 bytes at most (JSON); requests of 30 MB at most |
|
|
434
|
+
| Amazon SES, Resend | metadata values of 1 to 256 letters, digits, `_` or `-` (use identifiers, not email addresses or dates); a name cannot be both a tag and a metadata key, since both end up in the same list |
|
|
435
|
+
| Postmark | 50 recipients (`to`, `cc` and `bcc`); addresses of 254 characters and a sender of 255 characters at most; subjects of 2,000 characters at most; a single tag; no executable attachments (`.exe`, `.bat`, `.msi`…); requests of 10 MB at most |
|
|
436
|
+
| Brevo | a single reply-to address; no inline attachments; names of 70 characters at most; attachment extensions accepted by Brevo only (`pdf`, `png`, `docx`, `zip`…) |
|
|
437
|
+
| Mailjet | a single reply-to address; requests of 15 MB at most |
|
|
438
|
+
| Mailgun | 16 KB at most (16,384 bytes) for the reply-to addresses, additional headers, tags and metadata together |
|
|
439
|
+
| Scaleway | no inline attachments; subjects of 998 characters at most; no empty header values |
|
|
440
|
+
|
|
441
|
+
Request sizes include the attachments, encoded in base64 (a third larger than the files).
|
|
442
|
+
|
|
443
|
+
### Custom providers
|
|
444
|
+
|
|
445
|
+
A provider has a `name` and a `send(message)` method, which returns `{ messageId }` or nothing. The message is normalized and frozen: `from`, `to`, `cc`, `bcc`, `replyTo` (`{ name?, address }`), `subject`, `html` (undefined for a text-only message), `text`, `attachments`, `headers`, `tags`, `metadata`, plus `template` and `locale` for logs. `formatAddress()` formats an address for a header (`"Ada" <ada@example.com>`).
|
|
446
|
+
|
|
447
|
+
Throw a `MailSendError` with the HTTP `status` of the API, if any, and a `MailMessageError` for a message the provider cannot send (a limit of its API). Other errors are wrapped in a `MailSendError`.
|
|
448
|
+
|
|
449
|
+
## Dotpress plugin
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { mailerPlugin } from '@dotpress/mailer/plugin'
|
|
453
|
+
|
|
454
|
+
await createApp({ adapter, plugins: [mailerPlugin(mailer)] })
|
|
455
|
+
|
|
456
|
+
defineRoute({
|
|
457
|
+
method: 'post',
|
|
458
|
+
path: '/invitations',
|
|
459
|
+
handler: async ({ mailer, body }) => {
|
|
460
|
+
await mailer.send(invitation, { to: body.email, data: { team: body.team } })
|
|
461
|
+
return { sent: true }
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`ctx.mailer` logs sent messages with the request logger, so logs carry the request ID. Using `ctx.mailer` without the plugin throws an explicit error. The plugin is in its own entry point: the main one does not depend on `@dotpress/core`.
|
|
467
|
+
|
|
468
|
+
Sending is not retried and not queued: for that, send from a job or a queue consumer.
|
|
469
|
+
|
|
470
|
+
## Logs
|
|
471
|
+
|
|
472
|
+
With a `logger` (a Dotpress or Pino logger, or any object with an `info(data, message)` method), each sent message is logged at the `info` level as `Mail sent`, with the provider, the template, the message ID and the number of recipients. Addresses are not logged. Failures are thrown, not logged.
|
|
473
|
+
|
|
474
|
+
## Performance
|
|
475
|
+
|
|
476
|
+
MJML is loaded on the first render of an MJML template (about 100 to 300 ms), and html-to-text on the first text conversion. MJML compilation is cached by template and locale: later renders only run Mustache, in well under a millisecond.
|
|
477
|
+
|
|
478
|
+
## Errors
|
|
479
|
+
|
|
480
|
+
| Error | When |
|
|
481
|
+
| --- | --- |
|
|
482
|
+
| `TypeError` | invalid options of `defineTemplate()`, `defineLayout()`, `createMailer()`, `mailer.send()`… |
|
|
483
|
+
| `MailDataError` | data rejected by the schema (`template`, `issues`) |
|
|
484
|
+
| `MailRenderError` | MJML missing or invalid, unknown translation key, no available locale, invalid Mustache, failing source function (`template`, `cause`) |
|
|
485
|
+
| `MailMessageError` | invalid address, attachment, header, tag or metadata, or a limit of the provider |
|
|
486
|
+
| `MailSendError` | the provider failed (`provider`, `status`, `cause`) |
|
|
487
|
+
|
|
488
|
+
The other error classes extend `MailerError`.
|
|
489
|
+
|
|
490
|
+
## License
|
|
491
|
+
|
|
492
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SendEmailCommand, SendEmailCommandOutput, SESv2ClientConfig } from '@aws-sdk/client-sesv2';
|
|
2
|
+
import { MailProvider } from './index.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/** An SES client, or any object with the same `send()` method. */
|
|
6
|
+
interface SesClient {
|
|
7
|
+
send(command: SendEmailCommand): Promise<SendEmailCommandOutput>;
|
|
8
|
+
}
|
|
9
|
+
/** Options of `createSesProvider()`. */
|
|
10
|
+
interface SesProviderOptions {
|
|
11
|
+
/** Existing client. Defaults to a client created with `clientConfig`. */
|
|
12
|
+
client?: SesClient | undefined;
|
|
13
|
+
/** Configuration of the created client: `region`, `credentials`… */
|
|
14
|
+
clientConfig?: SESv2ClientConfig | undefined;
|
|
15
|
+
/** SES configuration set (events, dedicated IPs). */
|
|
16
|
+
configurationSetName?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a provider that sends messages with Amazon SES (API v2), through
|
|
20
|
+
* the AWS SDK (`@aws-sdk/client-sesv2`, peer dependency). Credentials and
|
|
21
|
+
* region come from the environment unless `clientConfig` sets them.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const provider = createSesProvider({ clientConfig: { region: 'eu-west-3' } })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare const createSesProvider: (options?: SesProviderOptions) => MailProvider;
|
|
28
|
+
|
|
29
|
+
export { createSesProvider };
|
|
30
|
+
export type { SesClient, SesProviderOptions };
|
package/dist/aws-ses.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
keyValueTags
|
|
3
|
+
} from "./chunk-GOAXQ2GE.js";
|
|
4
|
+
import {
|
|
5
|
+
base64,
|
|
6
|
+
utf8
|
|
7
|
+
} from "./chunk-2ZABFOZQ.js";
|
|
8
|
+
import {
|
|
9
|
+
MailSendError,
|
|
10
|
+
checkKeys,
|
|
11
|
+
formatAddress
|
|
12
|
+
} from "./chunk-QYVN5N3B.js";
|
|
13
|
+
|
|
14
|
+
// src/providers/aws-ses.ts
|
|
15
|
+
import {
|
|
16
|
+
SendEmailCommand,
|
|
17
|
+
SESv2Client
|
|
18
|
+
} from "@aws-sdk/client-sesv2";
|
|
19
|
+
var OPTIONS = /* @__PURE__ */ new Set(["client", "clientConfig", "configurationSetName"]);
|
|
20
|
+
var UTF8 = "UTF-8";
|
|
21
|
+
var toSesAddress = (address) => address.name !== void 0 && /[^\x20-\x7e]/.test(address.name) ? `=?UTF-8?B?${base64(address.name)}?= <${address.address}>` : formatAddress(address);
|
|
22
|
+
var emailTags = (message) => {
|
|
23
|
+
const tags = keyValueTags(message, "SES");
|
|
24
|
+
return tags.length > 0 ? {
|
|
25
|
+
EmailTags: tags.map(({ name, value }) => ({
|
|
26
|
+
Name: name,
|
|
27
|
+
Value: value
|
|
28
|
+
}))
|
|
29
|
+
} : {};
|
|
30
|
+
};
|
|
31
|
+
var toSendEmailInput = (message, configurationSetName) => ({
|
|
32
|
+
FromEmailAddress: toSesAddress(message.from),
|
|
33
|
+
Destination: {
|
|
34
|
+
ToAddresses: message.to.map(toSesAddress),
|
|
35
|
+
CcAddresses: message.cc.map(toSesAddress),
|
|
36
|
+
BccAddresses: message.bcc.map(toSesAddress)
|
|
37
|
+
},
|
|
38
|
+
ReplyToAddresses: message.replyTo.map(toSesAddress),
|
|
39
|
+
Content: {
|
|
40
|
+
Simple: {
|
|
41
|
+
Subject: { Data: message.subject, Charset: UTF8 },
|
|
42
|
+
Body: {
|
|
43
|
+
Text: { Data: message.text, Charset: UTF8 },
|
|
44
|
+
...message.html === void 0 ? {} : { Html: { Data: message.html, Charset: UTF8 } }
|
|
45
|
+
},
|
|
46
|
+
Headers: Object.entries(message.headers).map(([Name, Value]) => ({
|
|
47
|
+
Name,
|
|
48
|
+
Value
|
|
49
|
+
})),
|
|
50
|
+
Attachments: message.attachments.map(
|
|
51
|
+
({ filename, content, contentType, contentId }) => ({
|
|
52
|
+
FileName: filename,
|
|
53
|
+
RawContent: typeof content === "string" ? utf8(content) : content,
|
|
54
|
+
...contentType === void 0 ? {} : { ContentType: contentType },
|
|
55
|
+
...contentId === void 0 ? { ContentDisposition: "ATTACHMENT" } : { ContentDisposition: "INLINE", ContentId: contentId }
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
...configurationSetName === void 0 ? {} : { ConfigurationSetName: configurationSetName },
|
|
61
|
+
...emailTags(message)
|
|
62
|
+
});
|
|
63
|
+
var createSesProvider = (options = {}) => {
|
|
64
|
+
const { client, clientConfig, configurationSetName } = checkKeys(
|
|
65
|
+
"createSesProvider()",
|
|
66
|
+
options,
|
|
67
|
+
OPTIONS
|
|
68
|
+
);
|
|
69
|
+
if (client !== void 0 && clientConfig !== void 0) {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
"createSesProvider(): set client or clientConfig, not both."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (client !== void 0 && typeof client?.send !== "function") {
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
"createSesProvider(): client must have a send() method."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (configurationSetName !== void 0 && (typeof configurationSetName !== "string" || configurationSetName === "")) {
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
"createSesProvider(): configurationSetName must be a non-empty string."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const ses = client ?? new SESv2Client(clientConfig ?? {});
|
|
85
|
+
return {
|
|
86
|
+
name: "aws-ses",
|
|
87
|
+
send: async (message) => {
|
|
88
|
+
const command = new SendEmailCommand(
|
|
89
|
+
toSendEmailInput(message, configurationSetName)
|
|
90
|
+
);
|
|
91
|
+
let output;
|
|
92
|
+
try {
|
|
93
|
+
output = await ses.send(command);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
96
|
+
throw new MailSendError("aws-ses", String(error), {
|
|
97
|
+
cause: error,
|
|
98
|
+
status: typeof status === "number" ? status : void 0
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { messageId: output.MessageId };
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
export {
|
|
106
|
+
createSesProvider
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=aws-ses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/aws-ses.ts"],"sourcesContent":["import {\n SendEmailCommand,\n SESv2Client,\n type SendEmailCommandInput,\n type SendEmailCommandOutput,\n type SESv2ClientConfig,\n} from '@aws-sdk/client-sesv2'\nimport { base64, utf8 } from '../encoding.js'\nimport { MailSendError } from '../errors.js'\nimport type { MailProvider } from '../mailer.js'\nimport { keyValueTags } from './key-value-tags.js'\nimport {\n checkKeys,\n formatAddress,\n type Address,\n type MailMessage,\n} from '../message.js'\n\n/** An SES client, or any object with the same `send()` method. */\nexport interface SesClient {\n send(command: SendEmailCommand): Promise<SendEmailCommandOutput>\n}\n\n/** Options of `createSesProvider()`. */\nexport interface SesProviderOptions {\n /** Existing client. Defaults to a client created with `clientConfig`. */\n client?: SesClient | undefined\n /** Configuration of the created client: `region`, `credentials`… */\n clientConfig?: SESv2ClientConfig | undefined\n /** SES configuration set (events, dedicated IPs). */\n configurationSetName?: string | undefined\n}\n\nconst OPTIONS = new Set(['client', 'clientConfig', 'configurationSetName'])\n\nconst UTF8 = 'UTF-8'\n\n/**\n * Address for SES: a display name with non-ASCII characters is encoded\n * (RFC 2047 encoded-word), as SES requires.\n */\nconst toSesAddress = (address: Address) =>\n address.name !== undefined && /[^\\x20-\\x7e]/.test(address.name)\n ? `=?UTF-8?B?${base64(address.name)}?= <${address.address}>`\n : formatAddress(address)\n\n/** Tags and metadata as `EmailTags`. */\nconst emailTags = (message: MailMessage) => {\n const tags = keyValueTags(message, 'SES')\n return tags.length > 0\n ? {\n EmailTags: tags.map(({ name, value }) => ({\n Name: name,\n Value: value,\n })),\n }\n : {}\n}\n\n/** `SendEmail` input of a message (simple content). */\nconst toSendEmailInput = (\n message: MailMessage,\n configurationSetName?: string\n): SendEmailCommandInput => ({\n FromEmailAddress: toSesAddress(message.from),\n Destination: {\n ToAddresses: message.to.map(toSesAddress),\n CcAddresses: message.cc.map(toSesAddress),\n BccAddresses: message.bcc.map(toSesAddress),\n },\n ReplyToAddresses: message.replyTo.map(toSesAddress),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: UTF8 },\n Body: {\n Text: { Data: message.text, Charset: UTF8 },\n ...(message.html === undefined\n ? {}\n : { Html: { Data: message.html, Charset: UTF8 } }),\n },\n Headers: Object.entries(message.headers).map(([Name, Value]) => ({\n Name,\n Value,\n })),\n Attachments: message.attachments.map(\n ({ filename, content, contentType, contentId }) => ({\n FileName: filename,\n RawContent: typeof content === 'string' ? utf8(content) : content,\n ...(contentType === undefined ? {} : { ContentType: contentType }),\n ...(contentId === undefined\n ? { ContentDisposition: 'ATTACHMENT' as const }\n : { ContentDisposition: 'INLINE' as const, ContentId: contentId }),\n })\n ),\n },\n },\n ...(configurationSetName === undefined\n ? {}\n : { ConfigurationSetName: configurationSetName }),\n ...emailTags(message),\n})\n\n/**\n * Creates a provider that sends messages with Amazon SES (API v2), through\n * the AWS SDK (`@aws-sdk/client-sesv2`, peer dependency). Credentials and\n * region come from the environment unless `clientConfig` sets them.\n *\n * ```ts\n * const provider = createSesProvider({ clientConfig: { region: 'eu-west-3' } })\n * ```\n */\nexport const createSesProvider = (\n options: SesProviderOptions = {}\n): MailProvider => {\n const { client, clientConfig, configurationSetName } = checkKeys(\n 'createSesProvider()',\n options,\n OPTIONS\n ) as SesProviderOptions\n if (client !== undefined && clientConfig !== undefined) {\n throw new TypeError(\n 'createSesProvider(): set client or clientConfig, not both.'\n )\n }\n if (client !== undefined && typeof client?.send !== 'function') {\n throw new TypeError(\n 'createSesProvider(): client must have a send() method.'\n )\n }\n if (\n configurationSetName !== undefined &&\n (typeof configurationSetName !== 'string' || configurationSetName === '')\n ) {\n throw new TypeError(\n 'createSesProvider(): configurationSetName must be a non-empty string.'\n )\n }\n const ses = client ?? new SESv2Client(clientConfig ?? {})\n\n return {\n name: 'aws-ses',\n send: async (message) => {\n // Limits of SES (MailMessageError) are thrown as they are.\n const command = new SendEmailCommand(\n toSendEmailInput(message, configurationSetName)\n )\n let output: SendEmailCommandOutput\n try {\n output = await ses.send(command)\n } catch (error) {\n const status = (\n error as { $metadata?: { httpStatusCode?: unknown } } | null\n )?.$metadata?.httpStatusCode\n // SDK errors: \"MessageRejected: Email address is not verified.\"\n throw new MailSendError('aws-ses', String(error), {\n cause: error,\n status: typeof status === 'number' ? status : undefined,\n })\n }\n return { messageId: output.MessageId }\n },\n }\n}\n"],"mappings":"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","names":[]}
|