typedmailer 1.0.1 → 1.2.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/CODE_OF_CONDUCT.md +1 -1
- package/README.md +24 -11
- package/SECURITY.md +1 -1
- package/SUPPORT.md +1 -1
- package/dist/index.d.ts +2 -72
- package/dist/index.js +10 -108
- package/dist/providers/registry.d.ts +151 -0
- package/dist/providers/registry.js +109 -0
- package/dist/webhooks/normalize.d.ts +6 -0
- package/dist/webhooks/normalize.js +154 -0
- package/dist/webhooks/shared.d.ts +14 -0
- package/dist/webhooks/shared.js +69 -0
- package/dist/webhooks/signatures.d.ts +16 -0
- package/dist/webhooks/signatures.js +159 -0
- package/dist/webhooks/types.d.ts +43 -0
- package/dist/webhooks/types.js +13 -0
- package/dist/webhooks.d.ts +5 -0
- package/dist/webhooks.js +32 -0
- package/docs/integration-testing.md +1 -1
- package/docs/provider-contracts.md +2 -2
- package/docs/quickstart.md +63 -0
- package/docs/webhooks.md +60 -0
- package/package.json +11 -4
package/CODE_OF_CONDUCT.md
CHANGED
|
@@ -17,4 +17,4 @@ Harassment, insults, discriminatory language, intimidation, unwelcome sexual att
|
|
|
17
17
|
|
|
18
18
|
## Enforcement
|
|
19
19
|
|
|
20
|
-
Report conduct concerns privately
|
|
20
|
+
Report conduct concerns privately using [GitHub's contact and reporting tools](https://github.com/contact) for the repository owner. Reports will be reviewed and handled as fairly and promptly as possible. Maintainers may remove content or restrict participation for behavior that violates this code.
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ TypedMailer is a type-safe email library for Node.js and TypeScript applications
|
|
|
17
17
|
|
|
18
18
|
Use TypedMailer when you want to switch email providers without coupling application code to a provider SDK. Provider SDKs are optional peer dependencies, and only the selected adapter is loaded.
|
|
19
19
|
|
|
20
|
+
The separate `typedmailer/webhooks` entry point verifies and normalizes inbound provider events. It does not provide HTTP routing, persistence, or event processing.
|
|
21
|
+
|
|
20
22
|
## Install
|
|
21
23
|
|
|
22
24
|
Install the `typedmailer` npm package with the provider SDK you plan to use:
|
|
@@ -45,6 +47,8 @@ TypedMailer is for trusted server-side Node.js runtimes. It is not intended for
|
|
|
45
47
|
|
|
46
48
|
## Quick start
|
|
47
49
|
|
|
50
|
+
Want to try TypedMailer without provider credentials? Follow the [local Mailpit quick start](docs/quickstart.md).
|
|
51
|
+
|
|
48
52
|
```ts
|
|
49
53
|
import { createMailer } from 'typedmailer';
|
|
50
54
|
|
|
@@ -54,15 +58,18 @@ const mailer = createMailer({
|
|
|
54
58
|
from: 'Example App <noreply@example.com>',
|
|
55
59
|
});
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
try {
|
|
62
|
+
const result = await mailer.send({
|
|
63
|
+
to: 'person@example.com',
|
|
64
|
+
subject: 'Welcome',
|
|
65
|
+
text: 'Your account is ready.',
|
|
66
|
+
html: '<p>Your account is ready.</p>',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(result.messageId);
|
|
70
|
+
} finally {
|
|
71
|
+
await mailer.close();
|
|
72
|
+
}
|
|
66
73
|
```
|
|
67
74
|
|
|
68
75
|
## Providers
|
|
@@ -153,7 +160,7 @@ For local development, start Mailpit with `docker run --rm -p 1025:1025 -p 8025:
|
|
|
153
160
|
|
|
154
161
|
## Message options
|
|
155
162
|
|
|
156
|
-
`to` accepts an email string, a `{ email, name }` object, or an array. Provide `text` or `html` (or both). Optional fields include `from`, `replyTo`, `cc`, `bcc`, `headers`, `attachments`, `metadata`, and `idempotencyKey`; `messageId` is SMTP-only. Attachments may include `contentId` for inline images with Resend, Postmark, SendGrid, Mailgun, Amazon SES, and SMTP. Attachment content accepts strings or `Uint8Array`; the provider adapters buffer it for SDK requests, so use an application-managed upload or streaming workflow for large files.
|
|
163
|
+
`to` accepts an email string, a `{ email, name }` object, or an array. Provide `text` or `html` (or both). Optional fields include `from`, `replyTo`, `cc`, `bcc`, `headers`, `attachments`, `metadata`, and `idempotencyKey`; `messageId` is SMTP-only. Attachments may include `contentId` for inline images with Resend, Postmark, SendGrid, Mailgun, Amazon SES, and SMTP. Attachment content accepts strings or `Uint8Array`; the provider adapters buffer it for SDK requests, so use an application-managed upload or streaming workflow for large files. Set `maxAttachmentBytes` on `createMailer()` to reject a message before loading or calling the provider when the combined UTF-8 and binary attachment content exceeds your application's memory budget. It is unset by default for backward compatibility.
|
|
157
164
|
|
|
158
165
|
Subjects and attachment filenames cannot be blank. Supplied content types and inline content IDs must be non-empty. The library does not impose a fixed attachment-size limit.
|
|
159
166
|
|
|
@@ -195,6 +202,10 @@ console.log(mailer.sent[0]);
|
|
|
195
202
|
|
|
196
203
|
Messages captured by `createTestMailer` return `provider: 'test'` so test results are not mistaken for SMTP deliveries.
|
|
197
204
|
|
|
205
|
+
## Verify provider webhooks
|
|
206
|
+
|
|
207
|
+
Use `verifyWebhook` from `typedmailer/webhooks` to authenticate a raw request and receive normalized events. Your HTTP route remains responsible for preserving the raw body, responding to the provider, deduplicating events durably, and applying application changes. See [Webhook verification](docs/webhooks.md) for provider-specific configuration and security requirements.
|
|
208
|
+
|
|
198
209
|
## Runnable examples
|
|
199
210
|
|
|
200
211
|
The repository includes complete Resend, Amazon SES, and local SMTP/Mailpit examples in [`examples/`](examples/). From a clone, copy `examples/.env.example` to `.env`, replace the example sender and recipient with addresses valid for your account, then install the SDK for the chosen provider:
|
|
@@ -221,7 +232,7 @@ Provider and transport failures are normalized as `MailError`, with `code`, `pro
|
|
|
221
232
|
|
|
222
233
|
`retryable` is guidance from the normalized failure: recognized network failures and rate limits are retryable; API provider 5xx failures are retryable; authentication, configuration, unsupported, and SMTP 5xx failures are not. `deliveryUnknown` is separate: it is true when a send timeout/socket interruption, a provider 5xx, or an accepted response without a message ID means the provider may have accepted the message without returning a clear result. It stays false for verification failures, DNS lookup failures, authentication errors, rate limits, and SMTP response errors. This does not guarantee that retrying is safe. Use provider-supported idempotency where available and apply retry policy in your application. `cause` retains the original SDK error for diagnostics and can contain provider details; avoid logging it without reviewing your data handling policy.
|
|
223
234
|
|
|
224
|
-
A successful `send()` means the provider accepted the request; it does not confirm inbox delivery.
|
|
235
|
+
A successful `send()` means the provider accepted the request; it does not confirm inbox delivery. Verify delivery, bounce, and complaint callbacks with [`typedmailer/webhooks`](docs/webhooks.md).
|
|
225
236
|
|
|
226
237
|
`verifyConnection()` currently supports SMTP. API provider adapters report `unsupported` because they do not expose a side-effect-free credential check through this API; verify credentials with a controlled provider test message.
|
|
227
238
|
|
|
@@ -238,6 +249,8 @@ A successful `send()` means the provider accepted the request; it does not confi
|
|
|
238
249
|
|
|
239
250
|
Issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for development and contribution guidelines.
|
|
240
251
|
|
|
252
|
+
Contributors are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
253
|
+
|
|
241
254
|
After cloning, run `npm ci` to install dependencies and enable the local Git hooks. Commits run staged-file lint and format checks plus unit tests. Pushes run the full `npm run check` quality gate, including an isolated npm tarball consumer smoke test; GitHub Actions runs it on Node.js 22 and 24 and audits dependencies before merge and publish.
|
|
242
255
|
|
|
243
256
|
## License
|
package/SECURITY.md
CHANGED
|
@@ -6,4 +6,4 @@ Only the latest published version is currently supported.
|
|
|
6
6
|
|
|
7
7
|
## Reporting a vulnerability
|
|
8
8
|
|
|
9
|
-
Please do not report security vulnerabilities in public issues. Use GitHub's private vulnerability reporting for this repository. Include the affected version, impact, and a minimal reproduction. Do not include real credentials, recipient data, or message contents.
|
|
9
|
+
Please do not report security vulnerabilities in public issues. Use [GitHub's private vulnerability reporting](https://github.com/erolsenol/typedmailer/security/advisories/new) for this repository. Include the affected version, impact, and a minimal reproduction. Do not include real credentials, recipient data, or message contents.
|
package/SUPPORT.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Runtime support
|
|
4
4
|
|
|
5
|
-
TypedMailer requires Node.js 22 or newer. CI tests Node.js 22 and 24. We support the latest patch release of each Node.js major that is
|
|
5
|
+
TypedMailer requires Node.js 22 or newer. CI tests Node.js 22 and 24; Node.js 26 runs as a non-blocking compatibility canary while it is Current. We support the latest patch release of each Node.js major that is in its official maintenance or active LTS period; end-of-life Node.js releases are unsupported even when they satisfy the package engine range.
|
|
6
6
|
|
|
7
7
|
Install the provider SDK required by your selected adapter. Provider SDKs are optional peer dependencies; see the [provider contracts](docs/provider-contracts.md) and README compatibility table.
|
|
8
8
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,76 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type MailerOptions } from './providers/registry.js';
|
|
2
2
|
import type { Mailer } from './types.js';
|
|
3
|
-
|
|
4
|
-
readonly resend: z.ZodObject<{
|
|
5
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
6
|
-
email: z.ZodString;
|
|
7
|
-
name: z.ZodOptional<z.ZodString>;
|
|
8
|
-
}, z.core.$strict>]>;
|
|
9
|
-
provider: z.ZodLiteral<"resend">;
|
|
10
|
-
apiKey: z.ZodString;
|
|
11
|
-
}, z.core.$strip>;
|
|
12
|
-
readonly brevo: z.ZodObject<{
|
|
13
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
14
|
-
email: z.ZodString;
|
|
15
|
-
name: z.ZodOptional<z.ZodString>;
|
|
16
|
-
}, z.core.$strict>]>;
|
|
17
|
-
provider: z.ZodLiteral<"brevo">;
|
|
18
|
-
apiKey: z.ZodString;
|
|
19
|
-
}, z.core.$strip>;
|
|
20
|
-
readonly postmark: z.ZodObject<{
|
|
21
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
22
|
-
email: z.ZodString;
|
|
23
|
-
name: z.ZodOptional<z.ZodString>;
|
|
24
|
-
}, z.core.$strict>]>;
|
|
25
|
-
provider: z.ZodLiteral<"postmark">;
|
|
26
|
-
apiKey: z.ZodString;
|
|
27
|
-
}, z.core.$strip>;
|
|
28
|
-
readonly sendgrid: z.ZodObject<{
|
|
29
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
30
|
-
email: z.ZodString;
|
|
31
|
-
name: z.ZodOptional<z.ZodString>;
|
|
32
|
-
}, z.core.$strict>]>;
|
|
33
|
-
provider: z.ZodLiteral<"sendgrid">;
|
|
34
|
-
apiKey: z.ZodString;
|
|
35
|
-
}, z.core.$strip>;
|
|
36
|
-
readonly mailgun: z.ZodObject<{
|
|
37
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
38
|
-
email: z.ZodString;
|
|
39
|
-
name: z.ZodOptional<z.ZodString>;
|
|
40
|
-
}, z.core.$strict>]>;
|
|
41
|
-
provider: z.ZodLiteral<"mailgun">;
|
|
42
|
-
apiKey: z.ZodString;
|
|
43
|
-
domain: z.ZodString;
|
|
44
|
-
region: z.ZodDefault<z.ZodEnum<{
|
|
45
|
-
us: "us";
|
|
46
|
-
eu: "eu";
|
|
47
|
-
}>>;
|
|
48
|
-
}, z.core.$strip>;
|
|
49
|
-
readonly ses: z.ZodObject<{
|
|
50
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
51
|
-
email: z.ZodString;
|
|
52
|
-
name: z.ZodOptional<z.ZodString>;
|
|
53
|
-
}, z.core.$strict>]>;
|
|
54
|
-
provider: z.ZodLiteral<"ses">;
|
|
55
|
-
region: z.ZodString;
|
|
56
|
-
}, z.core.$strip>;
|
|
57
|
-
readonly smtp: z.ZodObject<{
|
|
58
|
-
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
59
|
-
email: z.ZodString;
|
|
60
|
-
name: z.ZodOptional<z.ZodString>;
|
|
61
|
-
}, z.core.$strict>]>;
|
|
62
|
-
provider: z.ZodLiteral<"smtp">;
|
|
63
|
-
host: z.ZodString;
|
|
64
|
-
port: z.ZodNumber;
|
|
65
|
-
secure: z.ZodBoolean;
|
|
66
|
-
user: z.ZodOptional<z.ZodString>;
|
|
67
|
-
password: z.ZodOptional<z.ZodString>;
|
|
68
|
-
connectionTimeout: z.ZodDefault<z.ZodNumber>;
|
|
69
|
-
greetingTimeout: z.ZodDefault<z.ZodNumber>;
|
|
70
|
-
socketTimeout: z.ZodDefault<z.ZodNumber>;
|
|
71
|
-
}, z.core.$strip>;
|
|
72
|
-
};
|
|
73
|
-
export type MailerOptions = z.input<typeof providerOptions.resend> | z.input<typeof providerOptions.brevo> | z.input<typeof providerOptions.postmark> | z.input<typeof providerOptions.sendgrid> | z.input<typeof providerOptions.mailgun> | z.input<typeof providerOptions.ses> | z.input<typeof providerOptions.smtp>;
|
|
3
|
+
export type { MailerOptions };
|
|
74
4
|
export declare function createMailer(input: MailerOptions): Mailer;
|
|
75
5
|
export { MailError } from './errors.js';
|
|
76
6
|
export type { MailErrorCode, MailErrorOperation, MailErrorOptions } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -1,119 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { MailError, normalizeProviderError } from './errors.js';
|
|
3
3
|
import { mailInputSchema, normalizeAddresses } from './config.js';
|
|
4
|
-
|
|
5
|
-
from: z.union([
|
|
6
|
-
z.string().email(),
|
|
7
|
-
z.string().regex(/^.+ <[^<>\s]+@[^<>\s]+>$/, 'Use a valid email address or "Name <email@example.com>".'),
|
|
8
|
-
z.object({ email: z.string().email(), name: z.string().optional() }).strict(),
|
|
9
|
-
]),
|
|
10
|
-
});
|
|
11
|
-
const providerOptions = {
|
|
12
|
-
resend: baseOptions.extend({ provider: z.literal('resend'), apiKey: z.string().min(1) }),
|
|
13
|
-
brevo: baseOptions.extend({ provider: z.literal('brevo'), apiKey: z.string().min(1) }),
|
|
14
|
-
postmark: baseOptions.extend({ provider: z.literal('postmark'), apiKey: z.string().min(1) }),
|
|
15
|
-
sendgrid: baseOptions.extend({ provider: z.literal('sendgrid'), apiKey: z.string().min(1) }),
|
|
16
|
-
mailgun: baseOptions.extend({
|
|
17
|
-
provider: z.literal('mailgun'),
|
|
18
|
-
apiKey: z.string().min(1),
|
|
19
|
-
domain: z.string().min(1),
|
|
20
|
-
region: z.enum(['us', 'eu']).default('us'),
|
|
21
|
-
}),
|
|
22
|
-
ses: baseOptions.extend({ provider: z.literal('ses'), region: z.string().min(1) }),
|
|
23
|
-
smtp: baseOptions
|
|
24
|
-
.extend({
|
|
25
|
-
provider: z.literal('smtp'),
|
|
26
|
-
host: z.string().min(1),
|
|
27
|
-
port: z.number().int().min(1).max(65535),
|
|
28
|
-
secure: z.boolean(),
|
|
29
|
-
user: z.string().min(1).optional(),
|
|
30
|
-
password: z.string().min(1).optional(),
|
|
31
|
-
connectionTimeout: z.number().int().positive().default(8_000),
|
|
32
|
-
greetingTimeout: z.number().int().positive().default(8_000),
|
|
33
|
-
socketTimeout: z.number().int().positive().default(15_000),
|
|
34
|
-
})
|
|
35
|
-
.refine((options) => Boolean(options.user) === Boolean(options.password), {
|
|
36
|
-
message: 'SMTP user and password must be configured together.',
|
|
37
|
-
path: ['user'],
|
|
38
|
-
}),
|
|
39
|
-
};
|
|
40
|
-
async function loadProvider(options) {
|
|
41
|
-
try {
|
|
42
|
-
switch (options.provider) {
|
|
43
|
-
case 'resend': {
|
|
44
|
-
const { createResendProvider } = await import('./providers/resend.js');
|
|
45
|
-
return createResendProvider(options);
|
|
46
|
-
}
|
|
47
|
-
case 'brevo': {
|
|
48
|
-
const { createBrevoProvider } = await import('./providers/brevo.js');
|
|
49
|
-
return createBrevoProvider(options);
|
|
50
|
-
}
|
|
51
|
-
case 'postmark': {
|
|
52
|
-
const { createPostmarkProvider } = await import('./providers/postmark.js');
|
|
53
|
-
return createPostmarkProvider(options);
|
|
54
|
-
}
|
|
55
|
-
case 'sendgrid': {
|
|
56
|
-
const { createSendGridProvider } = await import('./providers/sendgrid.js');
|
|
57
|
-
return createSendGridProvider(options);
|
|
58
|
-
}
|
|
59
|
-
case 'mailgun': {
|
|
60
|
-
const { createMailgunProvider } = await import('./providers/mailgun.js');
|
|
61
|
-
return createMailgunProvider(options);
|
|
62
|
-
}
|
|
63
|
-
case 'ses': {
|
|
64
|
-
const { createSesProvider } = await import('./providers/ses.js');
|
|
65
|
-
return createSesProvider(options);
|
|
66
|
-
}
|
|
67
|
-
case 'smtp': {
|
|
68
|
-
const { createSmtpProvider } = await import('./providers/smtp.js');
|
|
69
|
-
return createSmtpProvider({
|
|
70
|
-
host: options.host,
|
|
71
|
-
port: options.port,
|
|
72
|
-
secure: options.secure,
|
|
73
|
-
...(options.user ? { user: options.user } : {}),
|
|
74
|
-
...(options.password ? { password: options.password } : {}),
|
|
75
|
-
connectionTimeout: options.connectionTimeout,
|
|
76
|
-
greetingTimeout: options.greetingTimeout,
|
|
77
|
-
socketTimeout: options.socketTimeout,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
const moduleNotFound = error;
|
|
84
|
-
if (moduleNotFound?.code === 'ERR_MODULE_NOT_FOUND') {
|
|
85
|
-
const dependencies = {
|
|
86
|
-
brevo: '@getbrevo/brevo',
|
|
87
|
-
mailgun: 'mailgun.js and form-data',
|
|
88
|
-
postmark: 'postmark',
|
|
89
|
-
resend: 'resend',
|
|
90
|
-
sendgrid: '@sendgrid/mail',
|
|
91
|
-
ses: '@aws-sdk/client-sesv2',
|
|
92
|
-
smtp: 'nodemailer',
|
|
93
|
-
};
|
|
94
|
-
const dependency = dependencies[options.provider];
|
|
95
|
-
throw new MailError(`Install the optional provider package "${dependency}" to use ${options.provider}.`, 'configuration', options.provider, false, { cause: error });
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
4
|
+
import { loadProvider, mailerOptionsSchema } from './providers/registry.js';
|
|
100
5
|
export function createMailer(input) {
|
|
101
6
|
let providerPromise;
|
|
102
7
|
let closed = false;
|
|
103
8
|
let closePromise;
|
|
104
9
|
let activeOperations = 0;
|
|
105
10
|
let resolveOperationsIdle;
|
|
106
|
-
const parsedOptions =
|
|
107
|
-
.discriminatedUnion('provider', [
|
|
108
|
-
providerOptions.resend,
|
|
109
|
-
providerOptions.brevo,
|
|
110
|
-
providerOptions.postmark,
|
|
111
|
-
providerOptions.sendgrid,
|
|
112
|
-
providerOptions.mailgun,
|
|
113
|
-
providerOptions.ses,
|
|
114
|
-
providerOptions.smtp,
|
|
115
|
-
])
|
|
116
|
-
.parse(input);
|
|
11
|
+
const parsedOptions = mailerOptionsSchema.parse(input);
|
|
117
12
|
const assertOpen = () => {
|
|
118
13
|
if (closed)
|
|
119
14
|
throw new MailError('Mailer has been closed.', 'configuration', parsedOptions.provider, false);
|
|
@@ -141,6 +36,13 @@ export function createMailer(input) {
|
|
|
141
36
|
async send(input) {
|
|
142
37
|
assertOpen();
|
|
143
38
|
const parsed = mailInputSchema.parse(input);
|
|
39
|
+
const attachmentBytes = parsed.attachments?.reduce((total, attachment) => total +
|
|
40
|
+
(typeof attachment.content === 'string'
|
|
41
|
+
? Buffer.byteLength(attachment.content)
|
|
42
|
+
: attachment.content.byteLength), 0) ?? 0;
|
|
43
|
+
if (parsedOptions.maxAttachmentBytes !== undefined && attachmentBytes > parsedOptions.maxAttachmentBytes) {
|
|
44
|
+
throw new MailError(`Attachment content exceeds the configured limit of ${parsedOptions.maxAttachmentBytes} bytes.`, 'configuration', parsedOptions.provider, false);
|
|
45
|
+
}
|
|
144
46
|
const normalized = {
|
|
145
47
|
from: (parsed.from ?? parsedOptions.from),
|
|
146
48
|
to: (normalizeAddresses(parsed.to) ?? []),
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { MailProvider } from '../types.js';
|
|
3
|
+
export declare const providerOptions: {
|
|
4
|
+
readonly resend: z.ZodObject<{
|
|
5
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
6
|
+
email: z.ZodString;
|
|
7
|
+
name: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strict>]>;
|
|
9
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
provider: z.ZodLiteral<"resend">;
|
|
11
|
+
apiKey: z.ZodString;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
readonly brevo: z.ZodObject<{
|
|
14
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
15
|
+
email: z.ZodString;
|
|
16
|
+
name: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strict>]>;
|
|
18
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
provider: z.ZodLiteral<"brevo">;
|
|
20
|
+
apiKey: z.ZodString;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
readonly postmark: z.ZodObject<{
|
|
23
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
24
|
+
email: z.ZodString;
|
|
25
|
+
name: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$strict>]>;
|
|
27
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
provider: z.ZodLiteral<"postmark">;
|
|
29
|
+
apiKey: z.ZodString;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
readonly sendgrid: z.ZodObject<{
|
|
32
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
33
|
+
email: z.ZodString;
|
|
34
|
+
name: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, z.core.$strict>]>;
|
|
36
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
provider: z.ZodLiteral<"sendgrid">;
|
|
38
|
+
apiKey: z.ZodString;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
readonly mailgun: z.ZodObject<{
|
|
41
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
42
|
+
email: z.ZodString;
|
|
43
|
+
name: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, z.core.$strict>]>;
|
|
45
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
provider: z.ZodLiteral<"mailgun">;
|
|
47
|
+
apiKey: z.ZodString;
|
|
48
|
+
domain: z.ZodString;
|
|
49
|
+
region: z.ZodDefault<z.ZodEnum<{
|
|
50
|
+
us: "us";
|
|
51
|
+
eu: "eu";
|
|
52
|
+
}>>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
readonly ses: z.ZodObject<{
|
|
55
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
56
|
+
email: z.ZodString;
|
|
57
|
+
name: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strict>]>;
|
|
59
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
provider: z.ZodLiteral<"ses">;
|
|
61
|
+
region: z.ZodString;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
readonly smtp: z.ZodObject<{
|
|
64
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
65
|
+
email: z.ZodString;
|
|
66
|
+
name: z.ZodOptional<z.ZodString>;
|
|
67
|
+
}, z.core.$strict>]>;
|
|
68
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
provider: z.ZodLiteral<"smtp">;
|
|
70
|
+
host: z.ZodString;
|
|
71
|
+
port: z.ZodNumber;
|
|
72
|
+
secure: z.ZodBoolean;
|
|
73
|
+
user: z.ZodOptional<z.ZodString>;
|
|
74
|
+
password: z.ZodOptional<z.ZodString>;
|
|
75
|
+
connectionTimeout: z.ZodDefault<z.ZodNumber>;
|
|
76
|
+
greetingTimeout: z.ZodDefault<z.ZodNumber>;
|
|
77
|
+
socketTimeout: z.ZodDefault<z.ZodNumber>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
};
|
|
80
|
+
export declare const mailerOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
81
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
82
|
+
email: z.ZodString;
|
|
83
|
+
name: z.ZodOptional<z.ZodString>;
|
|
84
|
+
}, z.core.$strict>]>;
|
|
85
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
86
|
+
provider: z.ZodLiteral<"resend">;
|
|
87
|
+
apiKey: z.ZodString;
|
|
88
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
89
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
90
|
+
email: z.ZodString;
|
|
91
|
+
name: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strict>]>;
|
|
93
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
94
|
+
provider: z.ZodLiteral<"brevo">;
|
|
95
|
+
apiKey: z.ZodString;
|
|
96
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
97
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
98
|
+
email: z.ZodString;
|
|
99
|
+
name: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strict>]>;
|
|
101
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
102
|
+
provider: z.ZodLiteral<"postmark">;
|
|
103
|
+
apiKey: z.ZodString;
|
|
104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
105
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
106
|
+
email: z.ZodString;
|
|
107
|
+
name: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$strict>]>;
|
|
109
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
110
|
+
provider: z.ZodLiteral<"sendgrid">;
|
|
111
|
+
apiKey: z.ZodString;
|
|
112
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
113
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
114
|
+
email: z.ZodString;
|
|
115
|
+
name: z.ZodOptional<z.ZodString>;
|
|
116
|
+
}, z.core.$strict>]>;
|
|
117
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
118
|
+
provider: z.ZodLiteral<"mailgun">;
|
|
119
|
+
apiKey: z.ZodString;
|
|
120
|
+
domain: z.ZodString;
|
|
121
|
+
region: z.ZodDefault<z.ZodEnum<{
|
|
122
|
+
us: "us";
|
|
123
|
+
eu: "eu";
|
|
124
|
+
}>>;
|
|
125
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
126
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
127
|
+
email: z.ZodString;
|
|
128
|
+
name: z.ZodOptional<z.ZodString>;
|
|
129
|
+
}, z.core.$strict>]>;
|
|
130
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
131
|
+
provider: z.ZodLiteral<"ses">;
|
|
132
|
+
region: z.ZodString;
|
|
133
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
134
|
+
from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
|
|
135
|
+
email: z.ZodString;
|
|
136
|
+
name: z.ZodOptional<z.ZodString>;
|
|
137
|
+
}, z.core.$strict>]>;
|
|
138
|
+
maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
|
|
139
|
+
provider: z.ZodLiteral<"smtp">;
|
|
140
|
+
host: z.ZodString;
|
|
141
|
+
port: z.ZodNumber;
|
|
142
|
+
secure: z.ZodBoolean;
|
|
143
|
+
user: z.ZodOptional<z.ZodString>;
|
|
144
|
+
password: z.ZodOptional<z.ZodString>;
|
|
145
|
+
connectionTimeout: z.ZodDefault<z.ZodNumber>;
|
|
146
|
+
greetingTimeout: z.ZodDefault<z.ZodNumber>;
|
|
147
|
+
socketTimeout: z.ZodDefault<z.ZodNumber>;
|
|
148
|
+
}, z.core.$strip>], "provider">;
|
|
149
|
+
export type MailerOptions = z.input<typeof mailerOptionsSchema>;
|
|
150
|
+
export type ParsedMailerOptions = z.output<typeof mailerOptionsSchema>;
|
|
151
|
+
export declare function loadProvider(options: ParsedMailerOptions): Promise<MailProvider>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { MailError } from '../errors.js';
|
|
3
|
+
const baseOptions = z.object({
|
|
4
|
+
from: z.union([
|
|
5
|
+
z.string().email(),
|
|
6
|
+
z.string().regex(/^.+ <[^<>\s]+@[^<>\s]+>$/, 'Use a valid email address or "Name <email@example.com>".'),
|
|
7
|
+
z.object({ email: z.string().email(), name: z.string().optional() }).strict(),
|
|
8
|
+
]),
|
|
9
|
+
/** Optional aggregate in-memory attachment cap. No default preserves existing behavior. */
|
|
10
|
+
maxAttachmentBytes: z.number().int().positive().optional(),
|
|
11
|
+
});
|
|
12
|
+
export const providerOptions = {
|
|
13
|
+
resend: baseOptions.extend({ provider: z.literal('resend'), apiKey: z.string().min(1) }),
|
|
14
|
+
brevo: baseOptions.extend({ provider: z.literal('brevo'), apiKey: z.string().min(1) }),
|
|
15
|
+
postmark: baseOptions.extend({ provider: z.literal('postmark'), apiKey: z.string().min(1) }),
|
|
16
|
+
sendgrid: baseOptions.extend({ provider: z.literal('sendgrid'), apiKey: z.string().min(1) }),
|
|
17
|
+
mailgun: baseOptions.extend({
|
|
18
|
+
provider: z.literal('mailgun'),
|
|
19
|
+
apiKey: z.string().min(1),
|
|
20
|
+
domain: z.string().min(1),
|
|
21
|
+
region: z.enum(['us', 'eu']).default('us'),
|
|
22
|
+
}),
|
|
23
|
+
ses: baseOptions.extend({ provider: z.literal('ses'), region: z.string().min(1) }),
|
|
24
|
+
smtp: baseOptions
|
|
25
|
+
.extend({
|
|
26
|
+
provider: z.literal('smtp'),
|
|
27
|
+
host: z.string().min(1),
|
|
28
|
+
port: z.number().int().min(1).max(65535),
|
|
29
|
+
secure: z.boolean(),
|
|
30
|
+
user: z.string().min(1).optional(),
|
|
31
|
+
password: z.string().min(1).optional(),
|
|
32
|
+
connectionTimeout: z.number().int().positive().default(8_000),
|
|
33
|
+
greetingTimeout: z.number().int().positive().default(8_000),
|
|
34
|
+
socketTimeout: z.number().int().positive().default(15_000),
|
|
35
|
+
})
|
|
36
|
+
.refine((options) => Boolean(options.user) === Boolean(options.password), {
|
|
37
|
+
message: 'SMTP user and password must be configured together.',
|
|
38
|
+
path: ['user'],
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
export const mailerOptionsSchema = z.discriminatedUnion('provider', [
|
|
42
|
+
providerOptions.resend,
|
|
43
|
+
providerOptions.brevo,
|
|
44
|
+
providerOptions.postmark,
|
|
45
|
+
providerOptions.sendgrid,
|
|
46
|
+
providerOptions.mailgun,
|
|
47
|
+
providerOptions.ses,
|
|
48
|
+
providerOptions.smtp,
|
|
49
|
+
]);
|
|
50
|
+
export async function loadProvider(options) {
|
|
51
|
+
try {
|
|
52
|
+
switch (options.provider) {
|
|
53
|
+
case 'resend': {
|
|
54
|
+
const { createResendProvider } = await import('./resend.js');
|
|
55
|
+
return createResendProvider(options);
|
|
56
|
+
}
|
|
57
|
+
case 'brevo': {
|
|
58
|
+
const { createBrevoProvider } = await import('./brevo.js');
|
|
59
|
+
return createBrevoProvider(options);
|
|
60
|
+
}
|
|
61
|
+
case 'postmark': {
|
|
62
|
+
const { createPostmarkProvider } = await import('./postmark.js');
|
|
63
|
+
return createPostmarkProvider(options);
|
|
64
|
+
}
|
|
65
|
+
case 'sendgrid': {
|
|
66
|
+
const { createSendGridProvider } = await import('./sendgrid.js');
|
|
67
|
+
return createSendGridProvider(options);
|
|
68
|
+
}
|
|
69
|
+
case 'mailgun': {
|
|
70
|
+
const { createMailgunProvider } = await import('./mailgun.js');
|
|
71
|
+
return createMailgunProvider(options);
|
|
72
|
+
}
|
|
73
|
+
case 'ses': {
|
|
74
|
+
const { createSesProvider } = await import('./ses.js');
|
|
75
|
+
return createSesProvider(options);
|
|
76
|
+
}
|
|
77
|
+
case 'smtp': {
|
|
78
|
+
const { createSmtpProvider } = await import('./smtp.js');
|
|
79
|
+
return createSmtpProvider({
|
|
80
|
+
host: options.host,
|
|
81
|
+
port: options.port,
|
|
82
|
+
secure: options.secure,
|
|
83
|
+
...(options.user ? { user: options.user } : {}),
|
|
84
|
+
...(options.password ? { password: options.password } : {}),
|
|
85
|
+
connectionTimeout: options.connectionTimeout,
|
|
86
|
+
greetingTimeout: options.greetingTimeout,
|
|
87
|
+
socketTimeout: options.socketTimeout,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const moduleNotFound = error;
|
|
94
|
+
if (moduleNotFound?.code === 'ERR_MODULE_NOT_FOUND') {
|
|
95
|
+
const dependencies = {
|
|
96
|
+
brevo: '@getbrevo/brevo',
|
|
97
|
+
mailgun: 'mailgun.js and form-data',
|
|
98
|
+
postmark: 'postmark',
|
|
99
|
+
resend: 'resend',
|
|
100
|
+
sendgrid: '@sendgrid/mail',
|
|
101
|
+
ses: '@aws-sdk/client-sesv2',
|
|
102
|
+
smtp: 'nodemailer',
|
|
103
|
+
};
|
|
104
|
+
const dependency = dependencies[options.provider];
|
|
105
|
+
throw new MailError(`Install the optional provider package "${dependency}" to use ${options.provider}.`, 'configuration', options.provider, false, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EmailWebhookEvent } from './types.js';
|
|
2
|
+
export declare function normalizeResend(payload: unknown): readonly EmailWebhookEvent[];
|
|
3
|
+
export declare function normalizeMailgun(payload: unknown): readonly EmailWebhookEvent[];
|
|
4
|
+
export declare function normalizeSendGrid(payload: unknown): readonly EmailWebhookEvent[];
|
|
5
|
+
export declare function normalizeSes(payload: unknown): readonly EmailWebhookEvent[];
|
|
6
|
+
export declare function normalizeProviderEvents(provider: 'brevo' | 'postmark', payload: unknown): readonly EmailWebhookEvent[];
|