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.
@@ -17,4 +17,4 @@ Harassment, insults, discriminatory language, intimidation, unwelcome sexual att
17
17
 
18
18
  ## Enforcement
19
19
 
20
- Report conduct concerns privately through GitHub's contact/reporting tools 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.
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
- const result = await mailer.send({
58
- to: 'person@example.com',
59
- subject: 'Welcome',
60
- text: 'Your account is ready.',
61
- html: '<p>Your account is ready.</p>',
62
- });
63
-
64
- console.log(result.messageId);
65
- await mailer.close();
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. Delivery, bounce, and complaint events require provider webhooks and are outside this package's current scope.
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 still in its official maintenance or active LTS period; end-of-life Node.js releases are unsupported even when they satisfy the package engine range.
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 { z } from 'zod';
1
+ import { type MailerOptions } from './providers/registry.js';
2
2
  import type { Mailer } from './types.js';
3
- 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
- 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 { z } from 'zod';
1
+ import { Buffer } from 'node:buffer';
2
2
  import { MailError, normalizeProviderError } from './errors.js';
3
3
  import { mailInputSchema, normalizeAddresses } from './config.js';
4
- const baseOptions = z.object({
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 = z
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[];