typedmailer 1.1.0 → 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/README.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 +3 -42
- package/dist/webhooks.js +4 -389
- package/docs/provider-contracts.md +2 -2
- 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
|
|
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[];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { WebhookVerificationError } from './types.js';
|
|
2
|
+
import { asId, asOptionalRecord, asRecord, asString, asStringArray, firstString, isString, parseDate, } from './shared.js';
|
|
3
|
+
export function normalizeResend(payload) {
|
|
4
|
+
const root = asRecord(payload);
|
|
5
|
+
const data = asRecord(root.data);
|
|
6
|
+
return [
|
|
7
|
+
makeEvent('resend', root, {
|
|
8
|
+
id: asString(root.id),
|
|
9
|
+
eventType: asString(root.type),
|
|
10
|
+
type: mapEventType(asString(root.type)),
|
|
11
|
+
messageId: asString(data.email_id),
|
|
12
|
+
recipient: firstString(data.to),
|
|
13
|
+
occurredAt: parseDate(root.created_at),
|
|
14
|
+
}),
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
export function normalizeMailgun(payload) {
|
|
18
|
+
const root = asRecord(payload);
|
|
19
|
+
const event = asRecord(root['event-data']);
|
|
20
|
+
const message = asRecord(event.message);
|
|
21
|
+
const headers = asRecord(message.headers);
|
|
22
|
+
const eventName = asString(event.event);
|
|
23
|
+
return [
|
|
24
|
+
makeEvent('mailgun', event, {
|
|
25
|
+
id: asId(event.id),
|
|
26
|
+
eventType: eventName,
|
|
27
|
+
type: mapEventType(eventName),
|
|
28
|
+
messageId: asString(headers['message-id']),
|
|
29
|
+
recipient: asString(event.recipient),
|
|
30
|
+
occurredAt: parseDate(event.timestamp),
|
|
31
|
+
}),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export function normalizeSendGrid(payload) {
|
|
35
|
+
if (!Array.isArray(payload))
|
|
36
|
+
throw new WebhookVerificationError('invalid_payload');
|
|
37
|
+
return payload.map((item) => {
|
|
38
|
+
const event = asRecord(item);
|
|
39
|
+
const eventName = asString(event.event);
|
|
40
|
+
return makeEvent('sendgrid', event, {
|
|
41
|
+
id: asString(event.sg_event_id),
|
|
42
|
+
eventType: eventName,
|
|
43
|
+
type: mapEventType(eventName),
|
|
44
|
+
messageId: asString(event.sg_message_id),
|
|
45
|
+
recipient: asString(event.email),
|
|
46
|
+
occurredAt: parseDate(event.timestamp),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function normalizeSes(payload) {
|
|
51
|
+
const message = asRecord(payload);
|
|
52
|
+
const eventName = asString(message.eventType) ?? asString(message.notificationType);
|
|
53
|
+
const mail = asRecord(message.mail);
|
|
54
|
+
const bounce = asOptionalRecord(message.bounce);
|
|
55
|
+
const complaint = asOptionalRecord(message.complaint);
|
|
56
|
+
const delivery = asOptionalRecord(message.delivery);
|
|
57
|
+
const recipients = Array.isArray(bounce?.bouncedRecipients)
|
|
58
|
+
? bounce.bouncedRecipients
|
|
59
|
+
: Array.isArray(complaint?.complainedRecipients)
|
|
60
|
+
? complaint.complainedRecipients
|
|
61
|
+
: [];
|
|
62
|
+
const to = recipients.map((recipient) => asString(asRecord(recipient).emailAddress)).filter(isString);
|
|
63
|
+
const emails = to.length > 0 ? to : asStringArray(mail.destination);
|
|
64
|
+
return emails.length > 0
|
|
65
|
+
? emails.map((recipient) => makeEvent('ses', message, {
|
|
66
|
+
id: asString(message.messageId),
|
|
67
|
+
eventType: eventName,
|
|
68
|
+
type: mapEventType(eventName),
|
|
69
|
+
messageId: asString(mail.messageId),
|
|
70
|
+
recipient,
|
|
71
|
+
occurredAt: parseDate(delivery?.timestamp) ?? parseDate(bounce?.timestamp),
|
|
72
|
+
}))
|
|
73
|
+
: [
|
|
74
|
+
makeEvent('ses', message, {
|
|
75
|
+
id: asString(message.messageId),
|
|
76
|
+
eventType: eventName,
|
|
77
|
+
type: mapEventType(eventName),
|
|
78
|
+
messageId: asString(mail.messageId),
|
|
79
|
+
occurredAt: parseDate(asRecord(message.delivery).timestamp),
|
|
80
|
+
}),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
export function normalizeProviderEvents(provider, payload) {
|
|
84
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
85
|
+
return items.map((item) => {
|
|
86
|
+
const event = asRecord(item);
|
|
87
|
+
const eventName = provider === 'brevo' ? asString(event.event) : asString(event.RecordType);
|
|
88
|
+
return makeEvent(provider, event, {
|
|
89
|
+
id: provider === 'brevo'
|
|
90
|
+
? (asId(event.id) ?? asString(event['message-id']))
|
|
91
|
+
: (asId(event.ID) ?? asString(event.MessageID)),
|
|
92
|
+
eventType: eventName,
|
|
93
|
+
type: mapEventType(eventName),
|
|
94
|
+
messageId: provider === 'brevo' ? asString(event['message-id']) : asString(event.MessageID),
|
|
95
|
+
recipient: provider === 'brevo' ? asString(event.email) : asString(event.Recipient),
|
|
96
|
+
occurredAt: parseDate(provider === 'brevo' ? (event.ts_epoch ?? event.date) : (event.DeliveredAt ?? event.BouncedAt)),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function makeEvent(provider, raw, fields) {
|
|
101
|
+
if (!fields.eventType)
|
|
102
|
+
throw new WebhookVerificationError('invalid_payload');
|
|
103
|
+
return {
|
|
104
|
+
provider,
|
|
105
|
+
raw,
|
|
106
|
+
eventType: fields.eventType,
|
|
107
|
+
type: fields.type,
|
|
108
|
+
...(fields.id ? { id: fields.id } : {}),
|
|
109
|
+
...(fields.messageId ? { messageId: fields.messageId } : {}),
|
|
110
|
+
...(fields.recipient ? { recipient: fields.recipient } : {}),
|
|
111
|
+
...(fields.occurredAt ? { occurredAt: fields.occurredAt } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function mapEventType(eventType) {
|
|
115
|
+
const event = eventType?.toLowerCase().replaceAll(/[._ -]/g, '') ?? '';
|
|
116
|
+
const mappings = {
|
|
117
|
+
accepted: 'accepted',
|
|
118
|
+
sent: 'accepted',
|
|
119
|
+
request: 'accepted',
|
|
120
|
+
emailsent: 'accepted',
|
|
121
|
+
delivery: 'delivered',
|
|
122
|
+
delivered: 'delivered',
|
|
123
|
+
emaildelivered: 'delivered',
|
|
124
|
+
bounce: 'bounced',
|
|
125
|
+
bounced: 'bounced',
|
|
126
|
+
emailbounced: 'bounced',
|
|
127
|
+
hardbounce: 'bounced',
|
|
128
|
+
softbounce: 'bounced',
|
|
129
|
+
spamcomplaint: 'complained',
|
|
130
|
+
complaint: 'complained',
|
|
131
|
+
complained: 'complained',
|
|
132
|
+
emailcomplained: 'complained',
|
|
133
|
+
spamreport: 'complained',
|
|
134
|
+
deferred: 'delayed',
|
|
135
|
+
deliverydelayed: 'delayed',
|
|
136
|
+
deliverydelay: 'delayed',
|
|
137
|
+
emaildeliverydelayed: 'delayed',
|
|
138
|
+
open: 'opened',
|
|
139
|
+
opened: 'opened',
|
|
140
|
+
emailopened: 'opened',
|
|
141
|
+
click: 'clicked',
|
|
142
|
+
clicked: 'clicked',
|
|
143
|
+
emailclicked: 'clicked',
|
|
144
|
+
unsubscribed: 'unsubscribed',
|
|
145
|
+
groupunsubscribe: 'unsubscribed',
|
|
146
|
+
reject: 'rejected',
|
|
147
|
+
rejected: 'rejected',
|
|
148
|
+
dropped: 'failed',
|
|
149
|
+
failed: 'failed',
|
|
150
|
+
emailfailed: 'failed',
|
|
151
|
+
renderingfailure: 'failed',
|
|
152
|
+
};
|
|
153
|
+
return mappings[event] ?? 'other';
|
|
154
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebhookHeaders } from './types.js';
|
|
2
|
+
export declare function assertRecentTimestamp(value: string | undefined, now: Date | undefined, toleranceSeconds: number | undefined): void;
|
|
3
|
+
export declare function getHeader(headers: WebhookHeaders, name: string): string | undefined;
|
|
4
|
+
export declare function safeEqual(left: Uint8Array, right: Uint8Array): boolean;
|
|
5
|
+
export declare function decodeBase64(value: string): Buffer;
|
|
6
|
+
export declare function decodeHex(value: string): Buffer;
|
|
7
|
+
export declare function parseDate(value: unknown): Date | undefined;
|
|
8
|
+
export declare function asRecord(value: unknown): Record<string, unknown>;
|
|
9
|
+
export declare function asOptionalRecord(value: unknown): Record<string, unknown> | undefined;
|
|
10
|
+
export declare function asString(value: unknown): string | undefined;
|
|
11
|
+
export declare function asId(value: unknown): string | undefined;
|
|
12
|
+
export declare function isString(value: unknown): value is string;
|
|
13
|
+
export declare function asStringArray(value: unknown): string[];
|
|
14
|
+
export declare function firstString(value: unknown): string | undefined;
|