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/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,4 @@
1
+ import type { MailProvider } from '../types.js';
2
+ export declare function createBrevoProvider(options: {
3
+ apiKey: string;
4
+ }): MailProvider;
@@ -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,8 @@
1
+ import type { MailProvider } from '../types.js';
2
+ interface MailgunOptions {
3
+ apiKey: string;
4
+ domain: string;
5
+ region: 'us' | 'eu';
6
+ }
7
+ export declare function createMailgunProvider(options: MailgunOptions): MailProvider;
8
+ export {};
@@ -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,4 @@
1
+ import type { MailProvider } from '../types.js';
2
+ export declare function createPostmarkProvider(options: {
3
+ apiKey: string;
4
+ }): MailProvider;
@@ -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,4 @@
1
+ import type { MailProvider } from '../types.js';
2
+ export declare function createResendProvider(options: {
3
+ apiKey: string;
4
+ }): MailProvider;
@@ -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,4 @@
1
+ import type { MailProvider } from '../types.js';
2
+ export declare function createSendGridProvider(options: {
3
+ apiKey: string;
4
+ }): MailProvider;
@@ -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,6 @@
1
+ import type { MailProvider } from '../types.js';
2
+ interface SesOptions {
3
+ region: string;
4
+ }
5
+ export declare function createSesProvider(options: SesOptions): MailProvider;
6
+ export {};
@@ -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 {};