typedmailer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +20 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SECURITY.md +9 -0
- package/SUPPORT.md +21 -0
- package/assets/typedmailer-mark.svg +7 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +60 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +109 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +207 -0
- package/dist/providers/brevo.d.ts +4 -0
- package/dist/providers/brevo.js +55 -0
- package/dist/providers/mailgun.d.ts +8 -0
- package/dist/providers/mailgun.js +77 -0
- package/dist/providers/postmark.d.ts +4 -0
- package/dist/providers/postmark.js +57 -0
- package/dist/providers/resend.d.ts +4 -0
- package/dist/providers/resend.js +59 -0
- package/dist/providers/sendgrid.d.ts +4 -0
- package/dist/providers/sendgrid.js +69 -0
- package/dist/providers/ses.d.ts +6 -0
- package/dist/providers/ses.js +72 -0
- package/dist/providers/smtp.d.ts +13 -0
- package/dist/providers/smtp.js +71 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +30 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/docs/integration-testing.md +12 -0
- package/docs/provider-contracts.md +30 -0
- package/package.json +147 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { MailError, normalizeProviderError } from './errors.js';
|
|
3
|
+
import { mailInputSchema, normalizeAddresses } from './config.js';
|
|
4
|
+
const baseOptions = z.object({
|
|
5
|
+
from: z.union([
|
|
6
|
+
z.string().email(),
|
|
7
|
+
z.string().regex(/^.+ <[^<>\s]+@[^<>\s]+>$/, 'Use a valid email address or "Name <email@example.com>".'),
|
|
8
|
+
z.object({ email: z.string().email(), name: z.string().optional() }).strict(),
|
|
9
|
+
]),
|
|
10
|
+
});
|
|
11
|
+
const providerOptions = {
|
|
12
|
+
resend: baseOptions.extend({ provider: z.literal('resend'), apiKey: z.string().min(1) }),
|
|
13
|
+
brevo: baseOptions.extend({ provider: z.literal('brevo'), apiKey: z.string().min(1) }),
|
|
14
|
+
postmark: baseOptions.extend({ provider: z.literal('postmark'), apiKey: z.string().min(1) }),
|
|
15
|
+
sendgrid: baseOptions.extend({ provider: z.literal('sendgrid'), apiKey: z.string().min(1) }),
|
|
16
|
+
mailgun: baseOptions.extend({
|
|
17
|
+
provider: z.literal('mailgun'),
|
|
18
|
+
apiKey: z.string().min(1),
|
|
19
|
+
domain: z.string().min(1),
|
|
20
|
+
region: z.enum(['us', 'eu']).default('us'),
|
|
21
|
+
}),
|
|
22
|
+
ses: baseOptions.extend({ provider: z.literal('ses'), region: z.string().min(1) }),
|
|
23
|
+
smtp: baseOptions
|
|
24
|
+
.extend({
|
|
25
|
+
provider: z.literal('smtp'),
|
|
26
|
+
host: z.string().min(1),
|
|
27
|
+
port: z.number().int().min(1).max(65535),
|
|
28
|
+
secure: z.boolean(),
|
|
29
|
+
user: z.string().min(1).optional(),
|
|
30
|
+
password: z.string().min(1).optional(),
|
|
31
|
+
connectionTimeout: z.number().int().positive().default(8_000),
|
|
32
|
+
greetingTimeout: z.number().int().positive().default(8_000),
|
|
33
|
+
socketTimeout: z.number().int().positive().default(15_000),
|
|
34
|
+
})
|
|
35
|
+
.refine((options) => Boolean(options.user) === Boolean(options.password), {
|
|
36
|
+
message: 'SMTP user and password must be configured together.',
|
|
37
|
+
path: ['user'],
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
async function loadProvider(options) {
|
|
41
|
+
try {
|
|
42
|
+
switch (options.provider) {
|
|
43
|
+
case 'resend': {
|
|
44
|
+
const { createResendProvider } = await import('./providers/resend.js');
|
|
45
|
+
return createResendProvider(options);
|
|
46
|
+
}
|
|
47
|
+
case 'brevo': {
|
|
48
|
+
const { createBrevoProvider } = await import('./providers/brevo.js');
|
|
49
|
+
return createBrevoProvider(options);
|
|
50
|
+
}
|
|
51
|
+
case 'postmark': {
|
|
52
|
+
const { createPostmarkProvider } = await import('./providers/postmark.js');
|
|
53
|
+
return createPostmarkProvider(options);
|
|
54
|
+
}
|
|
55
|
+
case 'sendgrid': {
|
|
56
|
+
const { createSendGridProvider } = await import('./providers/sendgrid.js');
|
|
57
|
+
return createSendGridProvider(options);
|
|
58
|
+
}
|
|
59
|
+
case 'mailgun': {
|
|
60
|
+
const { createMailgunProvider } = await import('./providers/mailgun.js');
|
|
61
|
+
return createMailgunProvider(options);
|
|
62
|
+
}
|
|
63
|
+
case 'ses': {
|
|
64
|
+
const { createSesProvider } = await import('./providers/ses.js');
|
|
65
|
+
return createSesProvider(options);
|
|
66
|
+
}
|
|
67
|
+
case 'smtp': {
|
|
68
|
+
const { createSmtpProvider } = await import('./providers/smtp.js');
|
|
69
|
+
return createSmtpProvider({
|
|
70
|
+
host: options.host,
|
|
71
|
+
port: options.port,
|
|
72
|
+
secure: options.secure,
|
|
73
|
+
...(options.user ? { user: options.user } : {}),
|
|
74
|
+
...(options.password ? { password: options.password } : {}),
|
|
75
|
+
connectionTimeout: options.connectionTimeout,
|
|
76
|
+
greetingTimeout: options.greetingTimeout,
|
|
77
|
+
socketTimeout: options.socketTimeout,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const moduleNotFound = error;
|
|
84
|
+
if (moduleNotFound?.code === 'ERR_MODULE_NOT_FOUND') {
|
|
85
|
+
const dependencies = {
|
|
86
|
+
brevo: '@getbrevo/brevo',
|
|
87
|
+
mailgun: 'mailgun.js and form-data',
|
|
88
|
+
postmark: 'postmark',
|
|
89
|
+
resend: 'resend',
|
|
90
|
+
sendgrid: '@sendgrid/mail',
|
|
91
|
+
ses: '@aws-sdk/client-sesv2',
|
|
92
|
+
smtp: 'nodemailer',
|
|
93
|
+
};
|
|
94
|
+
const dependency = dependencies[options.provider];
|
|
95
|
+
throw new MailError(`Install the optional provider package "${dependency}" to use ${options.provider}.`, 'configuration', options.provider, false, { cause: error });
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function createMailer(input) {
|
|
101
|
+
let providerPromise;
|
|
102
|
+
let closed = false;
|
|
103
|
+
let closePromise;
|
|
104
|
+
let activeOperations = 0;
|
|
105
|
+
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);
|
|
117
|
+
const assertOpen = () => {
|
|
118
|
+
if (closed)
|
|
119
|
+
throw new MailError('Mailer has been closed.', 'configuration', parsedOptions.provider, false);
|
|
120
|
+
};
|
|
121
|
+
const getProvider = () => {
|
|
122
|
+
assertOpen();
|
|
123
|
+
providerPromise ??= loadProvider(parsedOptions);
|
|
124
|
+
return providerPromise;
|
|
125
|
+
};
|
|
126
|
+
const runWithProvider = async (operation) => {
|
|
127
|
+
assertOpen();
|
|
128
|
+
activeOperations += 1;
|
|
129
|
+
try {
|
|
130
|
+
return await operation(await getProvider());
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
activeOperations -= 1;
|
|
134
|
+
if (activeOperations === 0) {
|
|
135
|
+
resolveOperationsIdle?.();
|
|
136
|
+
resolveOperationsIdle = undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
async send(input) {
|
|
142
|
+
assertOpen();
|
|
143
|
+
const parsed = mailInputSchema.parse(input);
|
|
144
|
+
const normalized = {
|
|
145
|
+
from: (parsed.from ?? parsedOptions.from),
|
|
146
|
+
to: (normalizeAddresses(parsed.to) ?? []),
|
|
147
|
+
subject: parsed.subject,
|
|
148
|
+
...(parsed.messageId ? { messageId: parsed.messageId } : {}),
|
|
149
|
+
...(parsed.text !== undefined ? { text: parsed.text } : {}),
|
|
150
|
+
...(parsed.html !== undefined ? { html: parsed.html } : {}),
|
|
151
|
+
...(parsed.headers ? { headers: parsed.headers } : {}),
|
|
152
|
+
...(parsed.attachments
|
|
153
|
+
? {
|
|
154
|
+
attachments: parsed.attachments.map((item) => ({
|
|
155
|
+
filename: item.filename,
|
|
156
|
+
content: item.content,
|
|
157
|
+
...(item.contentType ? { contentType: item.contentType } : {}),
|
|
158
|
+
...(item.contentId ? { contentId: item.contentId } : {}),
|
|
159
|
+
})),
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
163
|
+
...(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
|
+
: {}),
|
|
170
|
+
...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
const result = await runWithProvider((provider) => provider.send(normalized));
|
|
174
|
+
return {
|
|
175
|
+
provider: parsedOptions.provider,
|
|
176
|
+
messageId: result.messageId,
|
|
177
|
+
acceptedAt: new Date(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw normalizeProviderError(error, parsedOptions.provider, 'send');
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
async verifyConnection() {
|
|
185
|
+
try {
|
|
186
|
+
await runWithProvider((provider) => provider.verifyConnection());
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw normalizeProviderError(error, parsedOptions.provider);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async close() {
|
|
193
|
+
closed = true;
|
|
194
|
+
closePromise ??= (async () => {
|
|
195
|
+
if (activeOperations > 0) {
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
resolveOperationsIdle = resolve;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const provider = await providerPromise?.catch(() => undefined);
|
|
201
|
+
await provider?.close();
|
|
202
|
+
})();
|
|
203
|
+
await closePromise;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export { MailError } from './errors.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BrevoClient } from '@getbrevo/brevo';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
4
|
+
import { addressWithName } from '../config.js';
|
|
5
|
+
export function createBrevoProvider(options) {
|
|
6
|
+
const client = new BrevoClient({ apiKey: options.apiKey });
|
|
7
|
+
return {
|
|
8
|
+
async send(input) {
|
|
9
|
+
try {
|
|
10
|
+
if (input.messageId) {
|
|
11
|
+
throw new MailError('Custom message IDs are only supported by the SMTP adapter.', 'unsupported', 'brevo', false);
|
|
12
|
+
}
|
|
13
|
+
if (input.idempotencyKey) {
|
|
14
|
+
throw new MailError('Brevo idempotency keys are not supported by this adapter.', 'unsupported', 'brevo', false);
|
|
15
|
+
}
|
|
16
|
+
if (input.attachments?.some((attachment) => attachment.contentId)) {
|
|
17
|
+
throw new MailError('Inline attachment content IDs are not supported by the Brevo adapter.', 'unsupported', 'brevo', false);
|
|
18
|
+
}
|
|
19
|
+
const result = await client.transactionalEmails.sendTransacEmail({
|
|
20
|
+
sender: addressWithName(input.from),
|
|
21
|
+
to: input.to.map(addressWithName),
|
|
22
|
+
subject: input.subject,
|
|
23
|
+
...(input.text !== undefined ? { textContent: input.text } : {}),
|
|
24
|
+
...(input.html !== undefined ? { htmlContent: input.html } : {}),
|
|
25
|
+
...(input.replyTo ? { replyTo: addressWithName(input.replyTo) } : {}),
|
|
26
|
+
...(input.cc ? { cc: input.cc.map(addressWithName) } : {}),
|
|
27
|
+
...(input.bcc ? { bcc: input.bcc.map(addressWithName) } : {}),
|
|
28
|
+
...(input.headers ? { headers: input.headers } : {}),
|
|
29
|
+
...(input.metadata ? { tags: Object.entries(input.metadata).map(([key, value]) => `${key}=${value}`) } : {}),
|
|
30
|
+
...(input.attachments
|
|
31
|
+
? {
|
|
32
|
+
attachment: input.attachments.map((attachment) => ({
|
|
33
|
+
name: attachment.filename,
|
|
34
|
+
content: Buffer.from(typeof attachment.content === 'string' ? attachment.content : Buffer.from(attachment.content)).toString('base64'),
|
|
35
|
+
})),
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
});
|
|
39
|
+
if (!result.messageId) {
|
|
40
|
+
throw new MailError('Brevo accepted no message identifier.', 'provider', 'brevo', false, {
|
|
41
|
+
deliveryUnknown: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { messageId: result.messageId };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw normalizeProviderError(error, 'brevo');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async verifyConnection() {
|
|
51
|
+
throw new MailError('Brevo connection verification is not exposed by this adapter yet.', 'unsupported', 'brevo', false);
|
|
52
|
+
},
|
|
53
|
+
async close() { },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import FormData from 'form-data';
|
|
3
|
+
import Mailgun from 'mailgun.js';
|
|
4
|
+
import { addressWithName } from '../config.js';
|
|
5
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
6
|
+
export function createMailgunProvider(options) {
|
|
7
|
+
const mailgun = new Mailgun(FormData);
|
|
8
|
+
const client = mailgun.client({
|
|
9
|
+
username: 'api',
|
|
10
|
+
key: options.apiKey,
|
|
11
|
+
url: options.region === 'eu' ? 'https://api.eu.mailgun.net' : 'https://api.mailgun.net',
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
async send(input) {
|
|
15
|
+
try {
|
|
16
|
+
if (input.messageId || input.idempotencyKey) {
|
|
17
|
+
throw new MailError('Mailgun does not support custom message IDs or idempotency keys through this adapter.', 'unsupported', 'mailgun', false);
|
|
18
|
+
}
|
|
19
|
+
const content = input.text !== undefined
|
|
20
|
+
? { text: input.text, ...(input.html !== undefined ? { html: input.html } : {}) }
|
|
21
|
+
: input.html !== undefined
|
|
22
|
+
? { html: input.html }
|
|
23
|
+
: undefined;
|
|
24
|
+
if (!content) {
|
|
25
|
+
throw new MailError('Provide text or HTML content.', 'configuration', 'mailgun', false);
|
|
26
|
+
}
|
|
27
|
+
const message = {
|
|
28
|
+
from: formatAddress(input.from),
|
|
29
|
+
to: input.to.map(formatAddress),
|
|
30
|
+
subject: input.subject,
|
|
31
|
+
...content,
|
|
32
|
+
...(input.cc ? { cc: input.cc.map(formatAddress) } : {}),
|
|
33
|
+
...(input.bcc ? { bcc: input.bcc.map(formatAddress) } : {}),
|
|
34
|
+
...(input.replyTo ? { 'h:Reply-To': formatAddress(input.replyTo) } : {}),
|
|
35
|
+
...Object.fromEntries(Object.entries(input.headers ?? {}).map(([key, value]) => [`h:${key}`, value])),
|
|
36
|
+
...Object.fromEntries(Object.entries(input.metadata ?? {}).map(([key, value]) => [`v:${key}`, value])),
|
|
37
|
+
...(input.attachments?.length
|
|
38
|
+
? {
|
|
39
|
+
attachment: input.attachments
|
|
40
|
+
.filter((attachment) => !attachment.contentId)
|
|
41
|
+
.map((attachment) => ({
|
|
42
|
+
data: Buffer.from(attachment.content),
|
|
43
|
+
filename: attachment.filename,
|
|
44
|
+
...(attachment.contentType ? { contentType: attachment.contentType } : {}),
|
|
45
|
+
})),
|
|
46
|
+
inline: input.attachments
|
|
47
|
+
.filter((attachment) => Boolean(attachment.contentId))
|
|
48
|
+
.map((attachment) => ({
|
|
49
|
+
data: Buffer.from(attachment.content),
|
|
50
|
+
filename: attachment.contentId,
|
|
51
|
+
...(attachment.contentType ? { contentType: attachment.contentType } : {}),
|
|
52
|
+
})),
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
const response = await client.messages.create(options.domain, message);
|
|
57
|
+
if (!response.id) {
|
|
58
|
+
throw new MailError('Mailgun accepted no message identifier.', 'provider', 'mailgun', false, {
|
|
59
|
+
deliveryUnknown: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { messageId: response.id };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw normalizeProviderError(error, 'mailgun');
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async verifyConnection() {
|
|
69
|
+
throw new MailError('Mailgun does not support connection verification. Send a provider test message instead.', 'unsupported', 'mailgun', false);
|
|
70
|
+
},
|
|
71
|
+
async close() { },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function formatAddress(address) {
|
|
75
|
+
const normalized = addressWithName(address);
|
|
76
|
+
return normalized.name ? `${normalized.name} <${normalized.email}>` : normalized.email;
|
|
77
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { ServerClient } from 'postmark';
|
|
3
|
+
import { addressWithName } from '../config.js';
|
|
4
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
5
|
+
export function createPostmarkProvider(options) {
|
|
6
|
+
const client = new ServerClient(options.apiKey);
|
|
7
|
+
return {
|
|
8
|
+
async send(input) {
|
|
9
|
+
try {
|
|
10
|
+
if (input.messageId || input.idempotencyKey) {
|
|
11
|
+
throw new MailError('Postmark does not support custom message IDs or idempotency keys through this adapter.', 'unsupported', 'postmark', false);
|
|
12
|
+
}
|
|
13
|
+
const response = await client.sendEmail({
|
|
14
|
+
From: formatAddress(input.from),
|
|
15
|
+
To: input.to.map(formatAddress).join(','),
|
|
16
|
+
Subject: input.subject,
|
|
17
|
+
...(input.text !== undefined ? { TextBody: input.text } : {}),
|
|
18
|
+
...(input.html !== undefined ? { HtmlBody: input.html } : {}),
|
|
19
|
+
...(input.replyTo ? { ReplyTo: formatAddress(input.replyTo) } : {}),
|
|
20
|
+
...(input.cc ? { Cc: input.cc.map(formatAddress).join(',') } : {}),
|
|
21
|
+
...(input.bcc ? { Bcc: input.bcc.map(formatAddress).join(',') } : {}),
|
|
22
|
+
...(input.headers
|
|
23
|
+
? { Headers: Object.entries(input.headers).map(([Name, Value]) => ({ Name, Value })) }
|
|
24
|
+
: {}),
|
|
25
|
+
...(input.metadata ? { Metadata: input.metadata } : {}),
|
|
26
|
+
...(input.attachments
|
|
27
|
+
? {
|
|
28
|
+
Attachments: input.attachments.map((attachment) => ({
|
|
29
|
+
Name: attachment.filename,
|
|
30
|
+
Content: Buffer.from(attachment.content).toString('base64'),
|
|
31
|
+
ContentType: attachment.contentType ?? 'application/octet-stream',
|
|
32
|
+
ContentID: attachment.contentId ?? null,
|
|
33
|
+
})),
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
});
|
|
37
|
+
if (!response.MessageID) {
|
|
38
|
+
throw new MailError('Postmark accepted no message identifier.', 'provider', 'postmark', false, {
|
|
39
|
+
deliveryUnknown: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return { messageId: response.MessageID };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw normalizeProviderError(error, 'postmark');
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
async verifyConnection() {
|
|
49
|
+
throw new MailError('Postmark does not support connection verification. Send a provider test message instead.', 'unsupported', 'postmark', false);
|
|
50
|
+
},
|
|
51
|
+
async close() { },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function formatAddress(address) {
|
|
55
|
+
const normalized = addressWithName(address);
|
|
56
|
+
return normalized.name ? `${normalized.name} <${normalized.email}>` : normalized.email;
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Resend } from 'resend';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
4
|
+
import { emailOf } from '../config.js';
|
|
5
|
+
export function createResendProvider(options) {
|
|
6
|
+
const client = new Resend(options.apiKey);
|
|
7
|
+
const formatFrom = (address) => {
|
|
8
|
+
if (typeof address === 'string')
|
|
9
|
+
return address;
|
|
10
|
+
return address.name ? `${address.name} <${address.email}>` : address.email;
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
async send(input) {
|
|
14
|
+
if (input.messageId) {
|
|
15
|
+
throw new MailError('Custom message IDs are only supported by the SMTP adapter.', 'unsupported', 'resend', false);
|
|
16
|
+
}
|
|
17
|
+
const payload = {
|
|
18
|
+
from: formatFrom(input.from),
|
|
19
|
+
to: input.to.map(emailOf),
|
|
20
|
+
subject: input.subject,
|
|
21
|
+
...(input.text !== undefined ? { text: input.text } : {}),
|
|
22
|
+
...(input.html !== undefined ? { html: input.html } : {}),
|
|
23
|
+
...(input.replyTo ? { replyTo: emailOf(input.replyTo) } : {}),
|
|
24
|
+
...(input.cc ? { cc: input.cc.map(emailOf) } : {}),
|
|
25
|
+
...(input.bcc ? { bcc: input.bcc.map(emailOf) } : {}),
|
|
26
|
+
...(input.headers ? { headers: input.headers } : {}),
|
|
27
|
+
...(input.attachments
|
|
28
|
+
? {
|
|
29
|
+
attachments: input.attachments.map((attachment) => ({
|
|
30
|
+
filename: attachment.filename,
|
|
31
|
+
content: typeof attachment.content === 'string' ? attachment.content : Buffer.from(attachment.content),
|
|
32
|
+
...(attachment.contentType ? { contentType: attachment.contentType } : {}),
|
|
33
|
+
...(attachment.contentId ? { contentId: attachment.contentId } : {}),
|
|
34
|
+
})),
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
...(input.metadata ? { tags: Object.entries(input.metadata).map(([name, value]) => ({ name, value })) } : {}),
|
|
38
|
+
};
|
|
39
|
+
const options = input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : undefined;
|
|
40
|
+
const response = options
|
|
41
|
+
? await client.emails.send(payload, options)
|
|
42
|
+
: await client.emails.send(payload);
|
|
43
|
+
const { data, error } = response;
|
|
44
|
+
if (error)
|
|
45
|
+
throw normalizeProviderError(error, 'resend');
|
|
46
|
+
if (!data?.id) {
|
|
47
|
+
throw new MailError('Resend accepted no message identifier.', 'provider', 'resend', false, {
|
|
48
|
+
deliveryUnknown: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { messageId: data.id };
|
|
52
|
+
},
|
|
53
|
+
async verifyConnection() {
|
|
54
|
+
// Resend does not expose a side-effect-free credential check in its SDK.
|
|
55
|
+
throw new MailError('Resend does not support connection verification. Send a provider test message instead.', 'unsupported', 'resend', false);
|
|
56
|
+
},
|
|
57
|
+
async close() { },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { MailService } from '@sendgrid/mail';
|
|
3
|
+
import { addressWithName } from '../config.js';
|
|
4
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
5
|
+
export function createSendGridProvider(options) {
|
|
6
|
+
const client = new MailService();
|
|
7
|
+
client.setApiKey(options.apiKey);
|
|
8
|
+
return {
|
|
9
|
+
async send(input) {
|
|
10
|
+
try {
|
|
11
|
+
if (input.messageId || input.idempotencyKey) {
|
|
12
|
+
throw new MailError('SendGrid does not support custom message IDs or idempotency keys through this adapter.', 'unsupported', 'sendgrid', false);
|
|
13
|
+
}
|
|
14
|
+
const content = input.text !== undefined
|
|
15
|
+
? { text: input.text, ...(input.html !== undefined ? { html: input.html } : {}) }
|
|
16
|
+
: input.html !== undefined
|
|
17
|
+
? { html: input.html }
|
|
18
|
+
: undefined;
|
|
19
|
+
if (!content) {
|
|
20
|
+
throw new MailError('Provide text or HTML content.', 'configuration', 'sendgrid', false);
|
|
21
|
+
}
|
|
22
|
+
const payload = {
|
|
23
|
+
from: addressWithName(input.from),
|
|
24
|
+
to: input.to.map(addressWithName),
|
|
25
|
+
subject: input.subject,
|
|
26
|
+
...content,
|
|
27
|
+
...(input.replyTo ? { replyTo: addressWithName(input.replyTo) } : {}),
|
|
28
|
+
...(input.cc ? { cc: input.cc.map(addressWithName) } : {}),
|
|
29
|
+
...(input.bcc ? { bcc: input.bcc.map(addressWithName) } : {}),
|
|
30
|
+
...(input.headers ? { headers: { ...input.headers } } : {}),
|
|
31
|
+
...(input.metadata ? { customArgs: { ...input.metadata } } : {}),
|
|
32
|
+
...(input.attachments
|
|
33
|
+
? {
|
|
34
|
+
attachments: input.attachments.map((attachment) => ({
|
|
35
|
+
filename: attachment.filename,
|
|
36
|
+
content: Buffer.from(attachment.content).toString('base64'),
|
|
37
|
+
...(attachment.contentType ? { type: attachment.contentType } : {}),
|
|
38
|
+
...(attachment.contentId ? { contentId: attachment.contentId, disposition: 'inline' } : {}),
|
|
39
|
+
})),
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
};
|
|
43
|
+
const [response] = await client.send(payload);
|
|
44
|
+
const messageId = getHeader(response.headers, 'x-message-id');
|
|
45
|
+
if (!messageId) {
|
|
46
|
+
throw new MailError('SendGrid accepted no message identifier.', 'provider', 'sendgrid', false, {
|
|
47
|
+
deliveryUnknown: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { messageId };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw normalizeProviderError(error, 'sendgrid');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async verifyConnection() {
|
|
57
|
+
throw new MailError('SendGrid does not support connection verification. Send a provider test message instead.', 'unsupported', 'sendgrid', false);
|
|
58
|
+
},
|
|
59
|
+
async close() { },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function getHeader(headers, name) {
|
|
63
|
+
const header = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
64
|
+
if (typeof header === 'string')
|
|
65
|
+
return header;
|
|
66
|
+
if (Array.isArray(header) && typeof header[0] === 'string')
|
|
67
|
+
return header[0];
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
|
|
2
|
+
import { addressWithName, emailOf } from '../config.js';
|
|
3
|
+
import { MailError, normalizeProviderError } from '../errors.js';
|
|
4
|
+
export function createSesProvider(options) {
|
|
5
|
+
const client = new SESv2Client({ region: options.region });
|
|
6
|
+
return {
|
|
7
|
+
async send(input) {
|
|
8
|
+
try {
|
|
9
|
+
if (input.messageId || input.idempotencyKey) {
|
|
10
|
+
throw new MailError('Amazon SES does not support custom message IDs or idempotency keys through this adapter.', 'unsupported', 'ses', false);
|
|
11
|
+
}
|
|
12
|
+
const response = await client.send(new SendEmailCommand({
|
|
13
|
+
FromEmailAddress: formatAddress(input.from),
|
|
14
|
+
Destination: {
|
|
15
|
+
ToAddresses: input.to.map(emailOf),
|
|
16
|
+
...(input.cc ? { CcAddresses: input.cc.map(emailOf) } : {}),
|
|
17
|
+
...(input.bcc ? { BccAddresses: input.bcc.map(emailOf) } : {}),
|
|
18
|
+
},
|
|
19
|
+
...(input.replyTo ? { ReplyToAddresses: [emailOf(input.replyTo)] } : {}),
|
|
20
|
+
...(input.metadata
|
|
21
|
+
? { EmailTags: Object.entries(input.metadata).map(([Name, Value]) => ({ Name, Value })) }
|
|
22
|
+
: {}),
|
|
23
|
+
Content: {
|
|
24
|
+
Simple: {
|
|
25
|
+
Subject: { Data: input.subject, Charset: 'UTF-8' },
|
|
26
|
+
Body: {
|
|
27
|
+
...(input.text !== undefined ? { Text: { Data: input.text, Charset: 'UTF-8' } } : {}),
|
|
28
|
+
...(input.html !== undefined ? { Html: { Data: input.html, Charset: 'UTF-8' } } : {}),
|
|
29
|
+
},
|
|
30
|
+
...(input.headers
|
|
31
|
+
? { Headers: Object.entries(input.headers).map(([Name, Value]) => ({ Name, Value })) }
|
|
32
|
+
: {}),
|
|
33
|
+
...(input.attachments
|
|
34
|
+
? {
|
|
35
|
+
Attachments: input.attachments.map((attachment) => ({
|
|
36
|
+
RawContent: typeof attachment.content === 'string'
|
|
37
|
+
? new TextEncoder().encode(attachment.content)
|
|
38
|
+
: attachment.content,
|
|
39
|
+
FileName: attachment.filename,
|
|
40
|
+
ContentType: attachment.contentType ?? 'application/octet-stream',
|
|
41
|
+
ContentDisposition: attachment.contentId ? 'INLINE' : 'ATTACHMENT',
|
|
42
|
+
ContentTransferEncoding: 'BASE64',
|
|
43
|
+
...(attachment.contentId ? { ContentId: attachment.contentId } : {}),
|
|
44
|
+
})),
|
|
45
|
+
}
|
|
46
|
+
: {}),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
if (!response.MessageId) {
|
|
51
|
+
throw new MailError('Amazon SES accepted no message identifier.', 'provider', 'ses', false, {
|
|
52
|
+
deliveryUnknown: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { messageId: response.MessageId };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw normalizeProviderError(error, 'ses');
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async verifyConnection() {
|
|
62
|
+
throw new MailError('Amazon SES does not support connection verification. Send a provider test message instead.', 'unsupported', 'ses', false);
|
|
63
|
+
},
|
|
64
|
+
async close() {
|
|
65
|
+
client.destroy();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function formatAddress(address) {
|
|
70
|
+
const normalized = addressWithName(address);
|
|
71
|
+
return normalized.name ? `${normalized.name} <${normalized.email}>` : normalized.email;
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MailProvider } from '../types.js';
|
|
2
|
+
interface SmtpOptions {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
secure: boolean;
|
|
6
|
+
user?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
connectionTimeout: number;
|
|
9
|
+
greetingTimeout: number;
|
|
10
|
+
socketTimeout: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSmtpProvider(options: SmtpOptions): MailProvider;
|
|
13
|
+
export {};
|