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.
@@ -0,0 +1,20 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We are committed to making participation in this project a welcoming and respectful experience for everyone, regardless of background or identity.
6
+
7
+ ## Expected behavior
8
+
9
+ - Be respectful and constructive.
10
+ - Focus feedback on the work and its impact.
11
+ - Accept responsibility and correct mistakes when they occur.
12
+ - Respect differing viewpoints and experiences.
13
+
14
+ ## Unacceptable behavior
15
+
16
+ Harassment, insults, discriminatory language, intimidation, unwelcome sexual attention, and publishing another person's private information are not acceptable.
17
+
18
+ ## Enforcement
19
+
20
+ Report conduct concerns privately through GitHub's contact/reporting tools for the repository owner. Reports will be reviewed and handled as fairly and promptly as possible. Maintainers may remove content or restrict participation for behavior that violates this code.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erol Senol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ <p align="center">
2
+ <img src="assets/typedmailer-mark.svg" alt="TypedMailer" width="72" height="72" />
3
+ </p>
4
+
5
+ <h1 align="center">TypedMailer: TypeScript Email Sending for Node.js</h1>
6
+
7
+ <p align="center"><strong>One typed API for sending Node.js email with seven providers.</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/erolsenol/typedmailer/actions/workflows/ci.yml"><img src="https://github.com/erolsenol/typedmailer/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
11
+ <a href="https://www.npmjs.com/package/typedmailer"><img src="https://img.shields.io/npm/v/typedmailer" alt="npm version" /></a>
12
+ <a href="https://www.npmjs.com/package/typedmailer"><img src="https://img.shields.io/npm/dm/typedmailer" alt="monthly npm downloads" /></a>
13
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license" /></a>
14
+ </p>
15
+
16
+ TypedMailer is a type-safe email library for Node.js and TypeScript applications. It gives server-side code one API for sending transactional email with Resend, Brevo, Postmark, SendGrid, Mailgun, Amazon SES, or SMTP. Your application keeps ownership of templates, queues, retries, and business rules.
17
+
18
+ Use TypedMailer when you want to switch email providers without coupling application code to a provider SDK. Provider SDKs are optional peer dependencies, and only the selected adapter is loaded.
19
+
20
+ ## Install
21
+
22
+ Install the `typedmailer` npm package with the provider SDK you plan to use:
23
+
24
+ ```sh
25
+ npm install typedmailer resend
26
+ # or
27
+ npm install typedmailer @getbrevo/brevo
28
+ # or
29
+ npm install typedmailer nodemailer
30
+ # or
31
+ npm install typedmailer postmark
32
+ # or
33
+ npm install typedmailer @sendgrid/mail
34
+ # or
35
+ npm install typedmailer mailgun.js form-data
36
+ # or
37
+ npm install typedmailer @aws-sdk/client-sesv2
38
+ ```
39
+
40
+ Requires Node.js 22 or newer. Provider SDKs are optional peers and are loaded only when their adapter is selected.
41
+
42
+ Supported runtime and release guarantees are documented in [`SUPPORT.md`](SUPPORT.md). Provider behavior guarantees and their test coverage are described in [`docs/provider-contracts.md`](docs/provider-contracts.md). The scheduled and manual provider smoke workflow is documented in [`docs/integration-testing.md`](docs/integration-testing.md).
43
+
44
+ TypedMailer is for trusted server-side Node.js runtimes. It is not intended for browser or mobile client bundles.
45
+
46
+ ## Quick start
47
+
48
+ ```ts
49
+ import { createMailer } from 'typedmailer';
50
+
51
+ const mailer = createMailer({
52
+ provider: 'resend',
53
+ apiKey: process.env.RESEND_API_KEY!,
54
+ from: 'Example App <noreply@example.com>',
55
+ });
56
+
57
+ const result = await mailer.send({
58
+ to: 'person@example.com',
59
+ subject: 'Welcome',
60
+ text: 'Your account is ready.',
61
+ html: '<p>Your account is ready.</p>',
62
+ });
63
+
64
+ console.log(result.messageId);
65
+ await mailer.close();
66
+ ```
67
+
68
+ ## Providers
69
+
70
+ TypedMailer supports these email providers through the same `createMailer` and `send` API:
71
+
72
+ - **Resend** for API-based email delivery.
73
+ - **Brevo** for API-based email delivery.
74
+ - **Postmark** for API-based email delivery.
75
+ - **SendGrid** for API-based email delivery.
76
+ - **Mailgun** for API-based email delivery in US or EU regions.
77
+ - **Amazon SES** for API-based email delivery using AWS credentials.
78
+ - **SMTP** for compatible SMTP services and local development servers such as Mailpit.
79
+
80
+ ### Brevo
81
+
82
+ ```ts
83
+ const mailer = createMailer({
84
+ provider: 'brevo',
85
+ apiKey: process.env.BREVO_API_KEY!,
86
+ from: { email: 'hello@example.com', name: 'Example App' },
87
+ });
88
+ ```
89
+
90
+ ### Postmark
91
+
92
+ Use your Postmark server token as `apiKey`:
93
+
94
+ ```ts
95
+ const mailer = createMailer({
96
+ provider: 'postmark',
97
+ apiKey: process.env.POSTMARK_SERVER_TOKEN!,
98
+ from: 'Example App <noreply@example.com>',
99
+ });
100
+ ```
101
+
102
+ ### SendGrid
103
+
104
+ ```ts
105
+ const mailer = createMailer({
106
+ provider: 'sendgrid',
107
+ apiKey: process.env.SENDGRID_API_KEY!,
108
+ from: 'Example App <noreply@example.com>',
109
+ });
110
+ ```
111
+
112
+ ### Mailgun
113
+
114
+ Mailgun requires a sending domain and API key. Set `region` to `'eu'` for an EU account; it defaults to `'us'`.
115
+
116
+ ```ts
117
+ const mailer = createMailer({
118
+ provider: 'mailgun',
119
+ apiKey: process.env.MAILGUN_API_KEY!,
120
+ domain: process.env.MAILGUN_DOMAIN!,
121
+ region: 'eu',
122
+ from: 'Example App <noreply@example.com>',
123
+ });
124
+ ```
125
+
126
+ ### Amazon SES
127
+
128
+ SES requires a region and uses the AWS SDK credential provider chain, such as environment credentials, a shared profile, or an IAM role.
129
+
130
+ ```ts
131
+ const mailer = createMailer({
132
+ provider: 'ses',
133
+ region: process.env.AWS_REGION!,
134
+ from: 'Example App <noreply@example.com>',
135
+ });
136
+ ```
137
+
138
+ ### SMTP
139
+
140
+ ```ts
141
+ const mailer = createMailer({
142
+ provider: 'smtp',
143
+ host: process.env.SMTP_HOST ?? '127.0.0.1',
144
+ port: Number(process.env.SMTP_PORT ?? 1025),
145
+ secure: process.env.SMTP_SECURE === 'true',
146
+ ...(process.env.SMTP_USER ? { user: process.env.SMTP_USER } : {}),
147
+ ...(process.env.SMTP_PASSWORD ? { password: process.env.SMTP_PASSWORD } : {}),
148
+ from: 'Local App <local@example.test>',
149
+ });
150
+ ```
151
+
152
+ For local development, start Mailpit with `docker run --rm -p 1025:1025 -p 8025:8025 axllent/mailpit`. Messages appear at `http://127.0.0.1:8025`.
153
+
154
+ ## Message options
155
+
156
+ `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.
157
+
158
+ 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.
159
+
160
+ Provider capabilities differ. Unsupported fields return a `MailError` with code `unsupported` instead of being silently ignored.
161
+
162
+ | Provider | Custom `messageId` | `idempotencyKey` | Metadata | Inline attachments | `verifyConnection()` |
163
+ | ---------- | ------------------ | ---------------- | -------- | ------------------ | -------------------- |
164
+ | Resend | No | Yes | Yes | Yes | Unsupported |
165
+ | Brevo | No | No | Yes | No | Unsupported |
166
+ | Postmark | No | No | Yes | Yes | Unsupported |
167
+ | SendGrid | No | No | Yes | Yes | Unsupported |
168
+ | Mailgun | No | No | Yes | Yes | Unsupported |
169
+ | Amazon SES | No | No | Yes | Yes | Unsupported |
170
+ | SMTP | Yes | No | No | Yes | Yes |
171
+
172
+ ```ts
173
+ await mailer.send({
174
+ to: [{ email: 'person@example.com', name: 'Sam' }],
175
+ subject: 'Your receipt',
176
+ text: 'Receipt attached.',
177
+ attachments: [{ filename: 'receipt.txt', content: 'Receipt 123' }],
178
+ idempotencyKey: 'receipt/order-123',
179
+ });
180
+ ```
181
+
182
+ TypedMailer does not retry sends automatically: after a network timeout the provider may already have accepted the message. Apply retries only when you understand the provider's idempotency guarantees.
183
+
184
+ ## Test your application flow
185
+
186
+ Use the in-memory adapter to exercise mail flows without contacting a provider:
187
+
188
+ ```ts
189
+ import { createTestMailer } from 'typedmailer/testing';
190
+
191
+ const mailer = createTestMailer({ from: 'Test <test@example.test>' });
192
+ await mailer.send({ to: 'person@example.test', subject: 'Hello', text: 'Hi' });
193
+ console.log(mailer.sent[0]);
194
+ ```
195
+
196
+ Messages captured by `createTestMailer` return `provider: 'test'` so test results are not mistaken for SMTP deliveries.
197
+
198
+ ## Runnable examples
199
+
200
+ The repository includes complete Resend, Amazon SES, and local SMTP/Mailpit examples in [`examples/`](examples/). From a clone, copy `examples/.env.example` to `.env`, replace the example sender and recipient with addresses valid for your account, then install the SDK for the chosen provider:
201
+
202
+ ```sh
203
+ npm install typedmailer resend
204
+ node --env-file=.env examples/resend.mjs
205
+ ```
206
+
207
+ For Amazon SES, run `npm install typedmailer @aws-sdk/client-sesv2 && node --env-file=.env examples/ses.mjs`; credentials come from the standard AWS SDK credential provider chain. For local SMTP, start Mailpit with `docker run --rm -p 1025:1025 -p 8025:8025 axllent/mailpit`, then run `npm install typedmailer nodemailer && node --env-file=.env examples/smtp-mailpit.mjs`. View captured messages at `http://127.0.0.1:8025`.
208
+
209
+ ## Errors and delivery
210
+
211
+ Provider and transport failures are normalized as `MailError`, with `code`, `provider`, `retryable`, `deliveryUnknown`, and the original error in `cause`. Codes mean:
212
+
213
+ | Code | Meaning |
214
+ | ---------------- | --------------------------------------------------------------------- |
215
+ | `configuration` | Invalid setup, missing optional SDK, or use after close. |
216
+ | `authentication` | The provider rejected credentials or access. |
217
+ | `rate_limit` | The provider throttled the request. |
218
+ | `network` | A recognized connection or timeout failure occurred. |
219
+ | `provider` | The provider returned another failure or an invalid response. |
220
+ | `unsupported` | The selected adapter cannot represent a requested field or operation. |
221
+
222
+ `retryable` is guidance from the normalized failure: recognized network failures and rate limits are retryable; API provider 5xx failures are retryable; authentication, configuration, unsupported, and SMTP 5xx failures are not. `deliveryUnknown` is separate: it is true when a send timeout/socket interruption, a provider 5xx, or an accepted response without a message ID means the provider may have accepted the message without returning a clear result. It stays false for verification failures, DNS lookup failures, authentication errors, rate limits, and SMTP response errors. This does not guarantee that retrying is safe. Use provider-supported idempotency where available and apply retry policy in your application. `cause` retains the original SDK error for diagnostics and can contain provider details; avoid logging it without reviewing your data handling policy.
223
+
224
+ A successful `send()` means the provider accepted the request; it does not confirm inbox delivery. Delivery, bounce, and complaint events require provider webhooks and are outside this package's current scope.
225
+
226
+ `verifyConnection()` currently supports SMTP. API provider adapters report `unsupported` because they do not expose a side-effect-free credential check through this API; verify credentials with a controlled provider test message.
227
+
228
+ `close()` is idempotent. It rejects new sends and verification calls, waits for operations already in progress, and closes the provider transport at most once. Reuse requires creating a new mailer.
229
+
230
+ ## Security
231
+
232
+ - Use TypedMailer only in trusted server-side Node.js code. Never expose provider keys in browser or mobile bundles.
233
+ - Keep secrets in environment variables or secret managers, not source control.
234
+ - TypedMailer does not log message content or credentials.
235
+ - See [SECURITY.md](SECURITY.md) to report a vulnerability.
236
+
237
+ ## Contributing
238
+
239
+ Issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for development and contribution guidelines.
240
+
241
+ After cloning, run `npm ci` to install dependencies and enable the local Git hooks. Commits run staged-file lint and format checks plus unit tests. Pushes run the full `npm run check` quality gate, including an isolated npm tarball consumer smoke test; GitHub Actions runs it on Node.js 22 and 24 and audits dependencies before merge and publish.
242
+
243
+ ## Türkçe kısa başlangıç
244
+
245
+ TypedMailer, Node.js sunucu uygulamalarında Resend, Brevo, Postmark, SendGrid, Mailgun, Amazon SES veya SMTP ile e-posta göndermek için ortak ve tip güvenli bir API sunar. Şablonlar, kuyruk ve tekrar deneme politikaları uygulamanızda kalır. Seçtiğiniz sağlayıcının SDK'sını TypedMailer ile birlikte yükleyin. API anahtarlarını yalnızca sunucu ortamında tutun.
246
+
247
+ ## License
248
+
249
+ MIT © 2026 Erol Senol
package/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Only the latest published version is currently supported.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please do not report security vulnerabilities in public issues. Use GitHub's private vulnerability reporting for this repository. Include the affected version, impact, and a minimal reproduction. Do not include real credentials, recipient data, or message contents.
package/SUPPORT.md ADDED
@@ -0,0 +1,21 @@
1
+ # Support policy
2
+
3
+ ## Runtime support
4
+
5
+ TypedMailer requires Node.js 22 or newer. CI tests Node.js 22 and 24. We support the latest patch release of each Node.js major that is still in its official maintenance or active LTS period; end-of-life Node.js releases are unsupported even when they satisfy the package engine range.
6
+
7
+ Install the provider SDK required by your selected adapter. Provider SDKs are optional peer dependencies; see the [provider contracts](docs/provider-contracts.md) and README compatibility table.
8
+
9
+ ## Versioning
10
+
11
+ TypedMailer follows Semantic Versioning for its public TypeScript API and documented runtime behavior:
12
+
13
+ - Patch: backward-compatible bug and security fixes.
14
+ - Minor: backward-compatible features and provider support.
15
+ - Major: breaking API, runtime requirement, or documented behavior changes.
16
+
17
+ A documented provider capability is part of the public contract. Changes to these guarantees require compatibility review, tests, and a changelog entry.
18
+
19
+ ## Security and support
20
+
21
+ Report vulnerabilities through GitHub's private vulnerability reporting. For usage questions and reproducible bugs, use the repository's GitHub Issues. Include the TypedMailer version, Node.js version, provider and SDK version, and sanitized error fields. Never include credentials, recipient addresses, message content, or unreviewed provider error causes.
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
2
+ <rect width="96" height="96" rx="24" fill="#111827" />
3
+ <path d="M22 34.5 48 53l26-18.5" stroke="#F8F7F4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
4
+ <path d="M24 32h48a5 5 0 0 1 5 5v24a5 5 0 0 1-5 5H24a5 5 0 0 1-5-5V37a5 5 0 0 1 5-5Z" stroke="#F8F7F4" stroke-width="5" />
5
+ <path d="M62 66h11a4 4 0 0 0 4-4v-1" stroke="#34D399" stroke-width="5" stroke-linecap="round" />
6
+ <circle cx="78" cy="56" r="5" fill="#34D399" />
7
+ </svg>
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ export declare const mailInputSchema: z.ZodObject<{
3
+ from: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
4
+ email: z.ZodString;
5
+ name: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strict>]>>;
7
+ to: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
8
+ email: z.ZodString;
9
+ name: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strict>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
11
+ email: z.ZodString;
12
+ name: z.ZodOptional<z.ZodString>;
13
+ }, z.core.$strict>]>>]>;
14
+ subject: z.ZodString;
15
+ messageId: z.ZodOptional<z.ZodString>;
16
+ text: z.ZodOptional<z.ZodString>;
17
+ html: z.ZodOptional<z.ZodString>;
18
+ replyTo: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
19
+ email: z.ZodString;
20
+ name: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strict>]>>;
22
+ cc: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
23
+ email: z.ZodString;
24
+ name: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strict>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
26
+ email: z.ZodString;
27
+ name: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$strict>]>>]>>;
29
+ bcc: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
30
+ email: z.ZodString;
31
+ name: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strict>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
33
+ email: z.ZodString;
34
+ name: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strict>]>>]>>;
36
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
37
+ attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
38
+ filename: z.ZodString;
39
+ content: z.ZodUnion<readonly [z.ZodString, z.ZodInstanceOf<Uint8Array<ArrayBuffer>>]>;
40
+ contentType: z.ZodOptional<z.ZodString>;
41
+ contentId: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>>>;
43
+ idempotencyKey: z.ZodOptional<z.ZodString>;
44
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
45
+ }, z.core.$strict>;
46
+ export declare function normalizeAddresses<T>(value: T | readonly T[] | undefined): readonly T[] | undefined;
47
+ export declare function emailOf(address: string | {
48
+ email: string;
49
+ name?: string;
50
+ }): string;
51
+ export declare function addressWithName(address: string | {
52
+ email: string;
53
+ name?: string;
54
+ }): {
55
+ email: string;
56
+ name?: string;
57
+ };
package/dist/config.js ADDED
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ const addressSchema = z.union([
3
+ z.string().email(),
4
+ z.object({ email: z.string().email(), name: z.string().optional() }).strict(),
5
+ ]);
6
+ const senderSchema = z.union([
7
+ z.string().email(),
8
+ z.string().regex(/^.+ <[^<>\s]+@[^<>\s]+>$/, 'Use a valid email address or "Name <email@example.com>".'),
9
+ z.object({ email: z.string().email(), name: z.string().optional() }).strict(),
10
+ ]);
11
+ export const mailInputSchema = z
12
+ .object({
13
+ from: senderSchema.optional(),
14
+ to: z.union([addressSchema, z.array(addressSchema).min(1)]),
15
+ subject: z
16
+ .string()
17
+ .min(1)
18
+ .refine((value) => value.trim().length > 0, 'Subject cannot be blank.'),
19
+ messageId: z.string().min(1).optional(),
20
+ text: z.string().optional(),
21
+ html: z.string().optional(),
22
+ replyTo: addressSchema.optional(),
23
+ cc: z.union([addressSchema, z.array(addressSchema)]).optional(),
24
+ bcc: z.union([addressSchema, z.array(addressSchema)]).optional(),
25
+ headers: z.record(z.string(), z.string()).optional(),
26
+ attachments: z
27
+ .array(z.object({
28
+ filename: z
29
+ .string()
30
+ .min(1)
31
+ .refine((value) => value.trim().length > 0, 'Attachment filename cannot be blank.'),
32
+ content: z.union([z.string(), z.instanceof(Uint8Array)]),
33
+ contentType: z.string().min(1).optional(),
34
+ contentId: z.string().min(1).optional(),
35
+ }))
36
+ .optional(),
37
+ idempotencyKey: z.string().min(1).optional(),
38
+ metadata: z.record(z.string(), z.string()).optional(),
39
+ })
40
+ .strict()
41
+ .refine((input) => input.text !== undefined || input.html !== undefined, {
42
+ message: 'Provide at least one of text or html.',
43
+ });
44
+ export function normalizeAddresses(value) {
45
+ if (value === undefined)
46
+ return undefined;
47
+ return Array.isArray(value) ? value : [value];
48
+ }
49
+ export function emailOf(address) {
50
+ if (typeof address !== 'string')
51
+ return address.email;
52
+ const namedAddress = address.match(/^(.+) <([^<>\s]+@[^<>\s]+)>$/);
53
+ return namedAddress?.[2] ?? address;
54
+ }
55
+ export function addressWithName(address) {
56
+ if (typeof address !== 'string')
57
+ return address;
58
+ const namedAddress = address.match(/^(.+) <([^<>\s]+@[^<>\s]+)>$/);
59
+ return namedAddress ? { email: namedAddress[2], name: namedAddress[1] } : { email: address };
60
+ }
@@ -0,0 +1,13 @@
1
+ export type MailErrorCode = 'configuration' | 'authentication' | 'rate_limit' | 'network' | 'provider' | 'unsupported';
2
+ export interface MailErrorOptions extends ErrorOptions {
3
+ readonly deliveryUnknown?: boolean;
4
+ }
5
+ export declare class MailError extends Error {
6
+ readonly code: MailErrorCode;
7
+ readonly provider: string;
8
+ readonly retryable: boolean;
9
+ readonly deliveryUnknown: boolean;
10
+ constructor(message: string, code: MailErrorCode, provider: string, retryable: boolean, options?: MailErrorOptions);
11
+ }
12
+ export type MailErrorOperation = 'send' | 'verify';
13
+ export declare function normalizeProviderError(error: unknown, provider: string, operation?: MailErrorOperation): MailError;
package/dist/errors.js ADDED
@@ -0,0 +1,109 @@
1
+ export class MailError extends Error {
2
+ code;
3
+ provider;
4
+ retryable;
5
+ deliveryUnknown;
6
+ constructor(message, code, provider, retryable, options) {
7
+ super(message, options);
8
+ this.code = code;
9
+ this.provider = provider;
10
+ this.retryable = retryable;
11
+ this.name = 'MailError';
12
+ this.deliveryUnknown = options?.deliveryUnknown ?? false;
13
+ }
14
+ }
15
+ const networkErrorCodes = new Set([
16
+ 'ETIMEDOUT',
17
+ 'ECONNECTION',
18
+ 'ENOTFOUND',
19
+ 'ECONNRESET',
20
+ 'EPIPE',
21
+ 'ECONNREFUSED',
22
+ 'EHOSTUNREACH',
23
+ 'ENETUNREACH',
24
+ 'ESOCKET',
25
+ ]);
26
+ const ambiguousNetworkErrorCodes = new Set(['ETIMEDOUT', 'ECONNECTION', 'ECONNRESET', 'EPIPE', 'ESOCKET']);
27
+ function mayHaveBeenAccepted(error, provider) {
28
+ const candidate = error;
29
+ const status = typeof candidate?.statusCode === 'number'
30
+ ? candidate.statusCode
31
+ : typeof candidate?.status === 'number'
32
+ ? candidate.status
33
+ : typeof candidate?.code === 'number'
34
+ ? candidate.code
35
+ : typeof error?.responseCode === 'number'
36
+ ? error.responseCode
37
+ : typeof error?.$metadata?.httpStatusCode ===
38
+ 'number'
39
+ ? error.$metadata.httpStatusCode
40
+ : undefined;
41
+ const code = typeof candidate?.code === 'string' ? candidate.code : candidate?.name;
42
+ if (provider === 'smtp' && status !== undefined)
43
+ return false;
44
+ if (status === 429 || status === 401 || status === 403 || (status !== undefined && status >= 400 && status < 500)) {
45
+ return false;
46
+ }
47
+ if (status !== undefined)
48
+ return status >= 500 && status < 600;
49
+ return typeof code === 'string' && ambiguousNetworkErrorCodes.has(code);
50
+ }
51
+ export function normalizeProviderError(error, provider, operation = 'verify') {
52
+ if (error instanceof MailError) {
53
+ const deliveryUnknown = operation === 'send' && (error.deliveryUnknown || mayHaveBeenAccepted(error.cause, provider));
54
+ if (error.deliveryUnknown === deliveryUnknown)
55
+ return error;
56
+ return new MailError(error.message, error.code, error.provider, error.retryable, {
57
+ ...(error.cause !== undefined ? { cause: error.cause } : {}),
58
+ deliveryUnknown,
59
+ });
60
+ }
61
+ const candidate = error;
62
+ const status = typeof candidate?.statusCode === 'number'
63
+ ? candidate.statusCode
64
+ : typeof candidate?.status === 'number'
65
+ ? candidate.status
66
+ : typeof candidate?.code === 'number'
67
+ ? candidate.code
68
+ : typeof error?.responseCode === 'number'
69
+ ? error.responseCode
70
+ : typeof error?.$metadata?.httpStatusCode ===
71
+ 'number'
72
+ ? error.$metadata.httpStatusCode
73
+ : undefined;
74
+ const code = typeof candidate?.code === 'string'
75
+ ? candidate.code
76
+ : typeof candidate?.name === 'string'
77
+ ? candidate.name
78
+ : undefined;
79
+ const authenticationFailure = status === 401 ||
80
+ status === 403 ||
81
+ code === 'EAUTH' ||
82
+ code === 'AccessDeniedException' ||
83
+ code === 'UnrecognizedClientException' ||
84
+ code === 'InvalidClientTokenId';
85
+ const rateLimitFailure = status === 429 || code === 'ThrottlingException' || code === 'TooManyRequestsException';
86
+ const safeMessage = authenticationFailure
87
+ ? 'The email provider rejected the configured credentials.'
88
+ : rateLimitFailure
89
+ ? 'The email provider rate limit was reached.'
90
+ : status !== undefined
91
+ ? `The email provider request failed with status ${status}.`
92
+ : typeof code === 'string' && networkErrorCodes.has(code)
93
+ ? 'Could not connect to the email provider.'
94
+ : 'The email provider could not accept the message.';
95
+ const errorCode = authenticationFailure
96
+ ? 'authentication'
97
+ : rateLimitFailure
98
+ ? 'rate_limit'
99
+ : typeof code === 'string' && networkErrorCodes.has(code)
100
+ ? 'network'
101
+ : 'provider';
102
+ const retryable = errorCode === 'network' ||
103
+ errorCode === 'rate_limit' ||
104
+ (provider !== 'smtp' && status !== undefined && status >= 500 && status < 600);
105
+ return new MailError(safeMessage, errorCode, provider, retryable, {
106
+ cause: error,
107
+ deliveryUnknown: operation === 'send' && mayHaveBeenAccepted(error, provider),
108
+ });
109
+ }
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ import type { Mailer } from './types.js';
3
+ declare const providerOptions: {
4
+ readonly resend: z.ZodObject<{
5
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
6
+ email: z.ZodString;
7
+ name: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strict>]>;
9
+ provider: z.ZodLiteral<"resend">;
10
+ apiKey: z.ZodString;
11
+ }, z.core.$strip>;
12
+ readonly brevo: z.ZodObject<{
13
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
14
+ email: z.ZodString;
15
+ name: z.ZodOptional<z.ZodString>;
16
+ }, z.core.$strict>]>;
17
+ provider: z.ZodLiteral<"brevo">;
18
+ apiKey: z.ZodString;
19
+ }, z.core.$strip>;
20
+ readonly postmark: z.ZodObject<{
21
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
22
+ email: z.ZodString;
23
+ name: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strict>]>;
25
+ provider: z.ZodLiteral<"postmark">;
26
+ apiKey: z.ZodString;
27
+ }, z.core.$strip>;
28
+ readonly sendgrid: z.ZodObject<{
29
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
30
+ email: z.ZodString;
31
+ name: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strict>]>;
33
+ provider: z.ZodLiteral<"sendgrid">;
34
+ apiKey: z.ZodString;
35
+ }, z.core.$strip>;
36
+ readonly mailgun: z.ZodObject<{
37
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
38
+ email: z.ZodString;
39
+ name: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strict>]>;
41
+ provider: z.ZodLiteral<"mailgun">;
42
+ apiKey: z.ZodString;
43
+ domain: z.ZodString;
44
+ region: z.ZodDefault<z.ZodEnum<{
45
+ us: "us";
46
+ eu: "eu";
47
+ }>>;
48
+ }, z.core.$strip>;
49
+ readonly ses: z.ZodObject<{
50
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
51
+ email: z.ZodString;
52
+ name: z.ZodOptional<z.ZodString>;
53
+ }, z.core.$strict>]>;
54
+ provider: z.ZodLiteral<"ses">;
55
+ region: z.ZodString;
56
+ }, z.core.$strip>;
57
+ readonly smtp: z.ZodObject<{
58
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodString, z.ZodObject<{
59
+ email: z.ZodString;
60
+ name: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strict>]>;
62
+ provider: z.ZodLiteral<"smtp">;
63
+ host: z.ZodString;
64
+ port: z.ZodNumber;
65
+ secure: z.ZodBoolean;
66
+ user: z.ZodOptional<z.ZodString>;
67
+ password: z.ZodOptional<z.ZodString>;
68
+ connectionTimeout: z.ZodDefault<z.ZodNumber>;
69
+ greetingTimeout: z.ZodDefault<z.ZodNumber>;
70
+ socketTimeout: z.ZodDefault<z.ZodNumber>;
71
+ }, z.core.$strip>;
72
+ };
73
+ export type MailerOptions = z.input<typeof providerOptions.resend> | z.input<typeof providerOptions.brevo> | z.input<typeof providerOptions.postmark> | z.input<typeof providerOptions.sendgrid> | z.input<typeof providerOptions.mailgun> | z.input<typeof providerOptions.ses> | z.input<typeof providerOptions.smtp>;
74
+ export declare function createMailer(input: MailerOptions): Mailer;
75
+ export { MailError } from './errors.js';
76
+ export type { MailErrorCode, MailErrorOperation, MailErrorOptions } from './errors.js';
77
+ export type { MailAddress, MailAttachment, Mailer, NormalizedMailInput, ProviderName, SendMailInput, SendMailResult, } from './types.js';