typedmailer 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -160,7 +160,7 @@ For local development, start Mailpit with `docker run --rm -p 1025:1025 -p 8025:
160
160
 
161
161
  ## Message options
162
162
 
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.
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.
164
164
 
165
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.
166
166
 
@@ -176,6 +176,32 @@ Provider capabilities differ. Unsupported fields return a `MailError` with code
176
176
  | Amazon SES | No | No | Yes | Yes | Unsupported |
177
177
  | SMTP | Yes | No | No | Yes | Yes |
178
178
 
179
+ ### Custom providers
180
+
181
+ Use `ProviderAdapter` when your provider is not built in or you need to wrap an existing mail SDK. The adapter receives normalized addresses and the configured default sender; its `send` method must resolve with the provider message ID. Implement `verifyConnection` only when the provider offers a safe connection check, and use `close` to release resources such as pooled clients.
182
+
183
+ ```ts
184
+ import { createMailer, type ProviderAdapter } from 'typedmailer';
185
+
186
+ const adapter: ProviderAdapter<'acme'> = {
187
+ name: 'acme',
188
+ async send(message) {
189
+ const response = await acmeClient.sendEmail(message);
190
+ return { messageId: response.id };
191
+ },
192
+ async close() {
193
+ await acmeClient.close();
194
+ },
195
+ };
196
+
197
+ const mailer = createMailer({ provider: adapter, from: 'noreply@example.com' });
198
+ const result = await mailer.send({ to: 'person@example.com', subject: 'Hello', text: 'Hi' });
199
+ // result.provider is typed as "acme".
200
+ await mailer.close();
201
+ ```
202
+
203
+ The custom adapter owns provider SDK setup, field support, and error handling. Throwing an error still passes through TypedMailer error normalization. Connection verification is reported as `unsupported` when the adapter does not implement it.
204
+
179
205
  ```ts
180
206
  await mailer.send({
181
207
  to: [{ email: 'person@example.com', name: 'Sam' }],
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { MailAddress } from './types.js';
2
3
  export declare const mailInputSchema: z.ZodObject<{
3
4
  from: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
4
5
  email: z.ZodString;
@@ -44,14 +45,8 @@ export declare const mailInputSchema: z.ZodObject<{
44
45
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
45
46
  }, z.core.$strict>;
46
47
  export declare function normalizeAddresses<T>(value: T | readonly T[] | undefined): readonly T[] | undefined;
47
- export declare function emailOf(address: string | {
48
- email: string;
49
- name?: string;
50
- }): string;
51
- export declare function addressWithName(address: string | {
52
- email: string;
53
- name?: string;
54
- }): {
48
+ export declare function emailOf(address: MailAddress): string;
49
+ export declare function addressWithName(address: MailAddress): {
55
50
  email: string;
56
51
  name?: string;
57
52
  };
package/dist/config.js CHANGED
@@ -53,8 +53,9 @@ export function emailOf(address) {
53
53
  return namedAddress?.[2] ?? address;
54
54
  }
55
55
  export function addressWithName(address) {
56
- if (typeof address !== 'string')
57
- return address;
56
+ if (typeof address !== 'string') {
57
+ return { email: address.email, ...(address.name !== undefined ? { name: address.name } : {}) };
58
+ }
58
59
  const namedAddress = address.match(/^(.+) <([^<>\s]+@[^<>\s]+)>$/);
59
60
  return namedAddress ? { email: namedAddress[2], name: namedAddress[1] } : { email: address };
60
61
  }
package/dist/index.d.ts CHANGED
@@ -1,77 +1,9 @@
1
- import { z } from 'zod';
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>;
74
- export declare function createMailer(input: MailerOptions): Mailer;
1
+ import { type MailerOptions as BuiltInMailerOptions } from './providers/registry.js';
2
+ import type { CustomMailerOptions, Mailer, ProviderName } from './types.js';
3
+ export type MailerOptions = BuiltInMailerOptions | CustomMailerOptions;
4
+ export declare function createMailer<const TProvider extends string>(input: CustomMailerOptions<TProvider>): Mailer<TProvider>;
5
+ export declare function createMailer(input: BuiltInMailerOptions): Mailer<ProviderName | 'test'>;
6
+ export declare function createMailer(input: MailerOptions): Mailer<string>;
75
7
  export { MailError } from './errors.js';
76
8
  export type { MailErrorCode, MailErrorOperation, MailErrorOptions } from './errors.js';
77
- export type { MailAddress, MailAttachment, Mailer, NormalizedMailInput, ProviderName, SendMailInput, SendMailResult, } from './types.js';
9
+ export type { CustomMailerOptions, MailAddress, MailAttachment, Mailer, NormalizedMailInput, ProviderAdapter, ProviderName, ProviderSendResult, SendMailInput, SendMailResult, } from './types.js';
package/dist/index.js CHANGED
@@ -1,101 +1,38 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { z } from 'zod';
2
3
  import { MailError, normalizeProviderError } from './errors.js';
3
4
  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
- });
5
+ import { loadProvider, mailerBaseOptionsSchema, mailerOptionsSchema, } from './providers/registry.js';
6
+ function isProviderAdapter(value) {
7
+ if (typeof value !== 'object' || value === null)
8
+ return false;
9
+ const adapter = value;
10
+ return (typeof adapter.name === 'string' &&
11
+ adapter.name.length > 0 &&
12
+ adapter.name.trim() === adapter.name &&
13
+ typeof adapter.send === 'function' &&
14
+ (adapter.verifyConnection === undefined || typeof adapter.verifyConnection === 'function') &&
15
+ (adapter.close === undefined || typeof adapter.close === 'function'));
16
+ }
17
+ const customMailerOptionsSchema = mailerBaseOptionsSchema
18
+ .extend({ provider: z.custom(isProviderAdapter, 'Invalid custom provider adapter.') })
19
+ .strict();
20
+ function isCustomOptions(input) {
21
+ return typeof input.provider !== 'string';
22
+ }
23
+ function createCustomProvider(adapter) {
24
+ return {
25
+ send: (input) => adapter.send(input),
26
+ async verifyConnection() {
27
+ if (!adapter.verifyConnection) {
28
+ throw new MailError(`Connection verification is not supported by the custom provider "${adapter.name}".`, 'unsupported', adapter.name, false);
79
29
  }
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
- }
30
+ await adapter.verifyConnection();
31
+ },
32
+ async close() {
33
+ await adapter.close?.();
34
+ },
35
+ };
99
36
  }
100
37
  export function createMailer(input) {
101
38
  let providerPromise;
@@ -103,24 +40,20 @@ export function createMailer(input) {
103
40
  let closePromise;
104
41
  let activeOperations = 0;
105
42
  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);
43
+ const parsedOptions = isCustomOptions(input)
44
+ ? customMailerOptionsSchema.parse(input)
45
+ : mailerOptionsSchema.parse(input);
46
+ const providerName = typeof parsedOptions.provider === 'string' ? parsedOptions.provider : parsedOptions.provider.name;
117
47
  const assertOpen = () => {
118
48
  if (closed)
119
- throw new MailError('Mailer has been closed.', 'configuration', parsedOptions.provider, false);
49
+ throw new MailError('Mailer has been closed.', 'configuration', providerName, false);
120
50
  };
121
51
  const getProvider = () => {
122
52
  assertOpen();
123
- providerPromise ??= loadProvider(parsedOptions);
53
+ providerPromise ??=
54
+ typeof parsedOptions.provider === 'string'
55
+ ? loadProvider(parsedOptions)
56
+ : Promise.resolve(createCustomProvider(parsedOptions.provider));
124
57
  return providerPromise;
125
58
  };
126
59
  const runWithProvider = async (operation) => {
@@ -141,9 +74,18 @@ export function createMailer(input) {
141
74
  async send(input) {
142
75
  assertOpen();
143
76
  const parsed = mailInputSchema.parse(input);
77
+ const attachmentBytes = parsed.attachments?.reduce((total, attachment) => total +
78
+ (typeof attachment.content === 'string'
79
+ ? Buffer.byteLength(attachment.content)
80
+ : attachment.content.byteLength), 0) ?? 0;
81
+ if (parsedOptions.maxAttachmentBytes !== undefined && attachmentBytes > parsedOptions.maxAttachmentBytes) {
82
+ throw new MailError(`Attachment content exceeds the configured limit of ${parsedOptions.maxAttachmentBytes} bytes.`, 'configuration', providerName, false);
83
+ }
84
+ const cc = normalizeAddresses(parsed.cc);
85
+ const bcc = normalizeAddresses(parsed.bcc);
144
86
  const normalized = {
145
- from: (parsed.from ?? parsedOptions.from),
146
- to: (normalizeAddresses(parsed.to) ?? []),
87
+ from: parsed.from ?? parsedOptions.from,
88
+ to: normalizeAddresses(parsed.to) ?? [],
147
89
  subject: parsed.subject,
148
90
  ...(parsed.messageId ? { messageId: parsed.messageId } : {}),
149
91
  ...(parsed.text !== undefined ? { text: parsed.text } : {}),
@@ -161,24 +103,20 @@ export function createMailer(input) {
161
103
  : {}),
162
104
  ...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
163
105
  ...(parsed.metadata ? { metadata: parsed.metadata } : {}),
164
- ...(normalizeAddresses(parsed.cc)
165
- ? { cc: normalizeAddresses(parsed.cc) }
166
- : {}),
167
- ...(normalizeAddresses(parsed.bcc)
168
- ? { bcc: normalizeAddresses(parsed.bcc) }
169
- : {}),
106
+ ...(cc ? { cc } : {}),
107
+ ...(bcc ? { bcc } : {}),
170
108
  ...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),
171
109
  };
172
110
  try {
173
111
  const result = await runWithProvider((provider) => provider.send(normalized));
174
112
  return {
175
- provider: parsedOptions.provider,
113
+ provider: providerName,
176
114
  messageId: result.messageId,
177
115
  acceptedAt: new Date(),
178
116
  };
179
117
  }
180
118
  catch (error) {
181
- throw normalizeProviderError(error, parsedOptions.provider, 'send');
119
+ throw normalizeProviderError(error, providerName, 'send');
182
120
  }
183
121
  },
184
122
  async verifyConnection() {
@@ -186,7 +124,7 @@ export function createMailer(input) {
186
124
  await runWithProvider((provider) => provider.verifyConnection());
187
125
  }
188
126
  catch (error) {
189
- throw normalizeProviderError(error, parsedOptions.provider);
127
+ throw normalizeProviderError(error, providerName);
190
128
  }
191
129
  },
192
130
  async close() {
@@ -0,0 +1,158 @@
1
+ import { z } from 'zod';
2
+ import type { MailProvider } from '../types.js';
3
+ export declare const mailerBaseOptionsSchema: z.ZodObject<{
4
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
5
+ email: z.ZodString;
6
+ name: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strict>]>;
8
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ export declare const providerOptions: {
11
+ readonly resend: z.ZodObject<{
12
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
13
+ email: z.ZodString;
14
+ name: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strict>]>;
16
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
17
+ provider: z.ZodLiteral<"resend">;
18
+ apiKey: z.ZodString;
19
+ }, z.core.$strip>;
20
+ readonly brevo: 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
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
26
+ provider: z.ZodLiteral<"brevo">;
27
+ apiKey: z.ZodString;
28
+ }, z.core.$strip>;
29
+ readonly postmark: z.ZodObject<{
30
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
31
+ email: z.ZodString;
32
+ name: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strict>]>;
34
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
35
+ provider: z.ZodLiteral<"postmark">;
36
+ apiKey: z.ZodString;
37
+ }, z.core.$strip>;
38
+ readonly sendgrid: z.ZodObject<{
39
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
40
+ email: z.ZodString;
41
+ name: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strict>]>;
43
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
44
+ provider: z.ZodLiteral<"sendgrid">;
45
+ apiKey: z.ZodString;
46
+ }, z.core.$strip>;
47
+ readonly mailgun: z.ZodObject<{
48
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
49
+ email: z.ZodString;
50
+ name: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$strict>]>;
52
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
53
+ provider: z.ZodLiteral<"mailgun">;
54
+ apiKey: z.ZodString;
55
+ domain: z.ZodString;
56
+ region: z.ZodDefault<z.ZodEnum<{
57
+ us: "us";
58
+ eu: "eu";
59
+ }>>;
60
+ }, z.core.$strip>;
61
+ readonly ses: z.ZodObject<{
62
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
63
+ email: z.ZodString;
64
+ name: z.ZodOptional<z.ZodString>;
65
+ }, z.core.$strict>]>;
66
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
67
+ provider: z.ZodLiteral<"ses">;
68
+ region: z.ZodString;
69
+ }, z.core.$strip>;
70
+ readonly smtp: z.ZodObject<{
71
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
72
+ email: z.ZodString;
73
+ name: z.ZodOptional<z.ZodString>;
74
+ }, z.core.$strict>]>;
75
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
76
+ provider: z.ZodLiteral<"smtp">;
77
+ host: z.ZodString;
78
+ port: z.ZodNumber;
79
+ secure: z.ZodBoolean;
80
+ user: z.ZodOptional<z.ZodString>;
81
+ password: z.ZodOptional<z.ZodString>;
82
+ connectionTimeout: z.ZodDefault<z.ZodNumber>;
83
+ greetingTimeout: z.ZodDefault<z.ZodNumber>;
84
+ socketTimeout: z.ZodDefault<z.ZodNumber>;
85
+ }, z.core.$strip>;
86
+ };
87
+ export declare const mailerOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
88
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
89
+ email: z.ZodString;
90
+ name: z.ZodOptional<z.ZodString>;
91
+ }, z.core.$strict>]>;
92
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
93
+ provider: z.ZodLiteral<"resend">;
94
+ apiKey: z.ZodString;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
97
+ email: z.ZodString;
98
+ name: z.ZodOptional<z.ZodString>;
99
+ }, z.core.$strict>]>;
100
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
101
+ provider: z.ZodLiteral<"brevo">;
102
+ apiKey: z.ZodString;
103
+ }, z.core.$strip>, z.ZodObject<{
104
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
105
+ email: z.ZodString;
106
+ name: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strict>]>;
108
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
109
+ provider: z.ZodLiteral<"postmark">;
110
+ apiKey: z.ZodString;
111
+ }, z.core.$strip>, z.ZodObject<{
112
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
113
+ email: z.ZodString;
114
+ name: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strict>]>;
116
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
117
+ provider: z.ZodLiteral<"sendgrid">;
118
+ apiKey: z.ZodString;
119
+ }, z.core.$strip>, z.ZodObject<{
120
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
121
+ email: z.ZodString;
122
+ name: z.ZodOptional<z.ZodString>;
123
+ }, z.core.$strict>]>;
124
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
125
+ provider: z.ZodLiteral<"mailgun">;
126
+ apiKey: z.ZodString;
127
+ domain: z.ZodString;
128
+ region: z.ZodDefault<z.ZodEnum<{
129
+ us: "us";
130
+ eu: "eu";
131
+ }>>;
132
+ }, z.core.$strip>, z.ZodObject<{
133
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
134
+ email: z.ZodString;
135
+ name: z.ZodOptional<z.ZodString>;
136
+ }, z.core.$strict>]>;
137
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
138
+ provider: z.ZodLiteral<"ses">;
139
+ region: z.ZodString;
140
+ }, z.core.$strip>, z.ZodObject<{
141
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
142
+ email: z.ZodString;
143
+ name: z.ZodOptional<z.ZodString>;
144
+ }, z.core.$strict>]>;
145
+ maxAttachmentBytes: z.ZodOptional<z.ZodNumber>;
146
+ provider: z.ZodLiteral<"smtp">;
147
+ host: z.ZodString;
148
+ port: z.ZodNumber;
149
+ secure: z.ZodBoolean;
150
+ user: z.ZodOptional<z.ZodString>;
151
+ password: z.ZodOptional<z.ZodString>;
152
+ connectionTimeout: z.ZodDefault<z.ZodNumber>;
153
+ greetingTimeout: z.ZodDefault<z.ZodNumber>;
154
+ socketTimeout: z.ZodDefault<z.ZodNumber>;
155
+ }, z.core.$strip>], "provider">;
156
+ export type MailerOptions = z.input<typeof mailerOptionsSchema>;
157
+ export type ParsedMailerOptions = z.output<typeof mailerOptionsSchema>;
158
+ export declare function loadProvider(options: ParsedMailerOptions): Promise<MailProvider>;
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod';
2
+ import { MailError } from '../errors.js';
3
+ export const mailerBaseOptionsSchema = 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: mailerBaseOptionsSchema.extend({ provider: z.literal('resend'), apiKey: z.string().min(1) }),
14
+ brevo: mailerBaseOptionsSchema.extend({ provider: z.literal('brevo'), apiKey: z.string().min(1) }),
15
+ postmark: mailerBaseOptionsSchema.extend({ provider: z.literal('postmark'), apiKey: z.string().min(1) }),
16
+ sendgrid: mailerBaseOptionsSchema.extend({ provider: z.literal('sendgrid'), apiKey: z.string().min(1) }),
17
+ mailgun: mailerBaseOptionsSchema.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: mailerBaseOptionsSchema.extend({ provider: z.literal('ses'), region: z.string().min(1) }),
24
+ smtp: mailerBaseOptionsSchema
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
+ }