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 +27 -1
- package/dist/config.d.ts +3 -8
- package/dist/config.js +3 -2
- package/dist/index.d.ts +7 -75
- package/dist/index.js +56 -118
- package/dist/providers/registry.d.ts +158 -0
- package/dist/providers/registry.js +109 -0
- package/dist/testing.d.ts +1 -1
- package/dist/types.d.ts +16 -5
- 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 +44 -0
- package/dist/webhooks/types.js +13 -0
- package/dist/webhooks.d.ts +3 -42
- package/dist/webhooks.js +9 -389
- package/docs/provider-contracts.md +7 -3
- package/docs/webhooks.md +1 -1
- package/package.json +3 -2
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
|
-
|
|
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 {
|
|
2
|
-
import type { Mailer } from './types.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 =
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
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',
|
|
49
|
+
throw new MailError('Mailer has been closed.', 'configuration', providerName, false);
|
|
120
50
|
};
|
|
121
51
|
const getProvider = () => {
|
|
122
52
|
assertOpen();
|
|
123
|
-
providerPromise ??=
|
|
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:
|
|
146
|
-
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
|
-
...(
|
|
165
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
+
}
|