mailchannels-sdk 1.3.0 → 1.4.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.
Files changed (31) hide show
  1. package/.agents/skills/mailchannels-js/SKILL.md +4 -0
  2. package/.agents/skills/mailchannels-js/resources/attachments.md +8 -10
  3. package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +5 -5
  4. package/.agents/skills/mailchannels-js/resources/custom-headers.md +4 -4
  5. package/.agents/skills/mailchannels-js/resources/dkim.md +10 -10
  6. package/.agents/skills/mailchannels-js/resources/domain-checks.md +6 -6
  7. package/.agents/skills/mailchannels-js/resources/error-handling.md +5 -5
  8. package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +9 -9
  9. package/.agents/skills/mailchannels-js/resources/overview.md +5 -5
  10. package/.agents/skills/mailchannels-js/resources/plugins/nodemailer.md +112 -0
  11. package/.agents/skills/mailchannels-js/resources/sending.md +10 -10
  12. package/.agents/skills/mailchannels-js/resources/sub-accounts.md +7 -9
  13. package/.agents/skills/mailchannels-js/resources/suppressions.md +4 -4
  14. package/.agents/skills/mailchannels-js/resources/templates.md +5 -5
  15. package/.agents/skills/mailchannels-js/resources/testing.md +52 -5
  16. package/.agents/skills/mailchannels-js/resources/unsubscribe.md +3 -3
  17. package/.agents/skills/mailchannels-js/resources/webhooks.md +8 -8
  18. package/README.md +73 -2
  19. package/dist/_chunks/mailchannels.d.mts +2094 -0
  20. package/dist/_chunks/mailchannels.mjs +2201 -0
  21. package/dist/_chunks/simulator.mjs +16 -8
  22. package/dist/cli/index.mjs +268 -0
  23. package/dist/mailchannels.d.mts +1 -2095
  24. package/dist/mailchannels.mjs +1 -2201
  25. package/dist/plugins/nodemailer/index.d.mts +33 -0
  26. package/dist/plugins/nodemailer/index.mjs +145 -0
  27. package/dist/simulator/index.d.mts +20 -0
  28. package/dist/simulator/index.mjs +2 -0
  29. package/package.json +21 -9
  30. package/dist/cli.d.mts +0 -1
  31. package/dist/cli.mjs +0 -50
@@ -4,9 +4,7 @@ Sub-accounts are first-class on MailChannels. Use them for tenants, customers, o
4
4
  senders so that one customer's reputation, limits, and bad traffic don't contaminate the
5
5
  parent account or other tenants.
6
6
 
7
- > Sub-accounts are only available on parent accounts on the 100K and higher plans.
8
-
9
- ### Handles
7
+ ## Handles
10
8
 
11
9
  A handle uniquely identifies a sub-account. Rules:
12
10
 
@@ -15,7 +13,7 @@ A handle uniquely identifies a sub-account. Rules:
15
13
  - Unique per parent account.
16
14
  - If omitted on create, a random handle is generated.
17
15
 
18
- ### Lifecycle
16
+ ## Lifecycle
19
17
 
20
18
  ```ts
21
19
  import { MailChannels } from 'mailchannels-sdk'
@@ -43,7 +41,7 @@ const { error: deleteError } = await mc.subAccounts.delete('clienta')
43
41
  if (deleteError) { /*...*/ }
44
42
  ```
45
43
 
46
- ### Credentials
44
+ ## Credentials
47
45
 
48
46
  Each sub-account has its own API keys and SMTP passwords.
49
47
 
@@ -76,7 +74,7 @@ create time. Store it immediately or rotate. Each sub-account has server-side ca
76
74
  many API keys and SMTP passwords it can hold — once at the cap, create returns
77
75
  `unprocessable_entity_error`; delete an unused credential first.
78
76
 
79
- ### Limits
77
+ ## Limits
80
78
 
81
79
  Per-sub-account monthly send caps. A sub-account without a limit inherits the parent's
82
80
  capacity.
@@ -92,7 +90,7 @@ const { error: deleteLimitErr } = await mc.subAccounts.limits.delete('clienta')
92
90
  if (deleteLimitErr) { /*...*/ }
93
91
  ```
94
92
 
95
- ### Usage
93
+ ## Usage
96
94
 
97
95
  ```ts
98
96
  const { data: parentUsage } = await mc.metrics.usage()
@@ -104,7 +102,7 @@ console.log(parentUsage?.total, parentUsage?.startDate, parentUsage?.endDate)
104
102
  `mc.metrics.usage()` is for the parent account. Use `mc.subAccounts.getUsage(handle)` for
105
103
  one specific sub-account.
106
104
 
107
- ### Sending As A Sub-Account
105
+ ## Sending As A Sub-Account
108
106
 
109
107
  Create a separate `MailChannels` instance with the sub-account's API key:
110
108
 
@@ -123,7 +121,7 @@ if (error) { /*...*/ }
123
121
  This keeps the account boundary explicit in code and avoids hard-to-debug issues where the
124
122
  wrong key is used at the wrong call site.
125
123
 
126
- ### Suppressions And Sub-Accounts
124
+ ## Suppressions And Sub-Accounts
127
125
 
128
126
  When creating suppressions on the parent, set `addToSubAccounts: true` to also copy entries
129
127
  into every sub-account. See [suppressions](suppressions.md).
@@ -3,7 +3,7 @@
3
3
  A suppression list keeps known-bad or opted-out recipients out of future sends. MailChannels
4
4
  suppresses by recipient + suppression type + source.
5
5
 
6
- ### Create Entries
6
+ ## Create Entries
7
7
 
8
8
  ```ts
9
9
  import { MailChannels } from 'mailchannels-sdk'
@@ -38,7 +38,7 @@ Constraints:
38
38
  All entries created via this endpoint have an inherent source of `'api'`.
39
39
  The endpoint does not have a field to set the source value.
40
40
 
41
- ### List Entries
41
+ ## List Entries
42
42
 
43
43
  ```ts
44
44
  const { data, error } = await mc.suppressions.list({
@@ -67,7 +67,7 @@ const { data: recent } = await mc.suppressions.list({
67
67
 
68
68
  Date formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object.
69
69
 
70
- ### Delete An Entry
70
+ ## Delete An Entry
71
71
 
72
72
  Warning:
73
73
  Do not remove entries from the suppression list if the recipient has not explicitly opted back in.
@@ -81,7 +81,7 @@ const { error: delAllErr } = await mc.suppressions.delete('recipient@example.net
81
81
  If `source` is omitted it defaults to `'api'`. Use `'all'` to remove every suppression for
82
82
  that recipient regardless of origin.
83
83
 
84
- ### Patterns
84
+ ## Patterns
85
85
 
86
86
  - **Preference center opt-out**: set `types` according to the email category, e.g. `non-transactional` for marketing
87
87
  emails, `transactional` for order updates, and so on.
@@ -8,7 +8,7 @@ no "create template" endpoint. To use a template:
8
8
 
9
9
  The only supported template type is `'mustache'`.
10
10
 
11
- ### Single Recipient
11
+ ## Single Recipient
12
12
 
13
13
  ```ts
14
14
  import { MailChannels } from 'mailchannels-sdk'
@@ -28,7 +28,7 @@ const { data, error } = await mc.emails.queue({
28
28
  })
29
29
  ```
30
30
 
31
- ### Multiple Recipients With Per-Recipient Variables
31
+ ## Multiple Recipients With Per-Recipient Variables
32
32
 
33
33
  Each personalization renders independently with its own variables. Root-level
34
34
  `template.data` is the base; per-personalization `template.data` is merged on top
@@ -56,7 +56,7 @@ const { data, error } = await mc.emails.queue({
56
56
  })
57
57
  ```
58
58
 
59
- ### Allowed Template Variable Value Types
59
+ ## Allowed Template Variable Value Types
60
60
 
61
61
  Keys are strings. Values may be:
62
62
 
@@ -69,12 +69,12 @@ Keys are strings. Values may be:
69
69
  `null`, `undefined`, and class instances are rejected with a `validation_error` before
70
70
  the request leaves the client.
71
71
 
72
- ### Subject Templates
72
+ ## Subject Templates
73
73
 
74
74
  The root `subject` field is **also** mustache-rendered when a `template` is set. There is
75
75
  no separate subject template configuration.
76
76
 
77
- ### Preview Without Sending
77
+ ## Preview Without Sending
78
78
 
79
79
  Use `dryRun: true` on `emails.send()` to render and validate without delivering:
80
80
 
@@ -3,7 +3,7 @@
3
3
  The SDK ships a built-in API simulator that runs a local HTTP server mimicking the real
4
4
  MailChannels API. Use it for integration tests without hitting the live API.
5
5
 
6
- ### Starting The Simulator
6
+ ## Starting The Simulator (CLI)
7
7
 
8
8
  Start the simulator as a background process before your test run:
9
9
 
@@ -25,12 +25,12 @@ alongside your test runner:
25
25
  "test:integration": "start-server-and-test 'npx mailchannels-sdk simulate --silent' http://localhost:8787 vitest"
26
26
  ```
27
27
 
28
- ### Example Test (Vitest / Jest)
28
+ ### Example Test: CLI (Vitest / Jest)
29
29
 
30
30
  Point the `MailChannels` client at the running simulator via `baseUrl`:
31
31
 
32
32
  ```ts
33
- import { describe, it, expect } from 'vitest'
33
+ import { it, expect } from 'vitest'
34
34
  import { MailChannels } from 'mailchannels-sdk'
35
35
 
36
36
  // Simulator must already be running: npx mailchannels-sdk simulate --port 8787 --silent
@@ -49,7 +49,54 @@ it('queues an email', async () => {
49
49
  })
50
50
  ```
51
51
 
52
- ### Client-Side Validation Without A Simulator
52
+ ## Starting The Simulator (Programmatic)
53
+
54
+ Import `createSimulator` from the `mailchannels-sdk/simulator` entrypoint and start the
55
+ simulator directly within your code. This gives you deterministic control over its
56
+ lifecycle, making it ideal for integration tests that need a fresh simulator instance
57
+ per run or when running in CI without a separate background process.
58
+
59
+ ```ts
60
+ import { createSimulator } from 'mailchannels-sdk/simulator'
61
+
62
+ const simulator = createSimulator({ port: 8787, silent: true })
63
+ const simulatorUrl = await simulator.listen()
64
+ ```
65
+
66
+ ### Example Test: Programmatic (Vitest / Jest)
67
+
68
+ ```ts
69
+ import { afterAll, beforeAll, expect, it } from 'vitest'
70
+ import { createSimulator } from 'mailchannels-sdk/simulator'
71
+ import { MailChannels } from 'mailchannels-sdk'
72
+
73
+ let simulator: ReturnType<typeof createSimulator>
74
+ let mc: MailChannels
75
+
76
+ beforeAll(async () => {
77
+ simulator = createSimulator({ port: 8787, silent: true })
78
+ const simulatorUrl = await simulator.listen()
79
+ mc = new MailChannels('test-key', { baseUrl: simulatorUrl })
80
+ })
81
+
82
+ afterAll(async () => {
83
+ await simulator.close()
84
+ })
85
+
86
+ it('queues an email', async () => {
87
+ const { data, error } = await mc.emails.queue({
88
+ from: 'sender@example.com',
89
+ to: 'recipient@example.net',
90
+ subject: 'Test',
91
+ text: 'Hello'
92
+ })
93
+
94
+ expect(error).toBeNull()
95
+ expect(data?.requestId).toBeDefined()
96
+ })
97
+ ```
98
+
99
+ ## Client-Side Validation Without A Simulator
53
100
 
54
101
  The SDK validates payloads before making any HTTP call — `validation_error` results are
55
102
  returned synchronously (no network required). Tests for client-side validation rules don't
@@ -73,7 +120,7 @@ it('rejects reserved headers', async () => {
73
120
 
74
121
  No network call is made because the error is caught before `_fetch` runs.
75
122
 
76
- ### Dry Run For Template Assertions
123
+ ## Dry Run For Template Assertions
77
124
 
78
125
  Use `emails.send(options, true)` (dry-run) against the real API in a staging pipeline to
79
126
  assert templates render correctly before shipping:
@@ -10,7 +10,7 @@ inbox providers (Gmail, Yahoo, etc.) effectively require both for bulk senders.
10
10
  Both mechanisms require the message to have **exactly one recipient per personalization**
11
11
  and to be **DKIM-signed**.
12
12
 
13
- ### In-Body Unsubscribe Link
13
+ ## In-Body Unsubscribe Link
14
14
 
15
15
  Use the literal placeholder string `{{mc-unsubscribe-url}}` inside a mustache HTML body.
16
16
  MailChannels substitutes a hosted one-click unsubscribe URL at render time.
@@ -30,7 +30,7 @@ const { data, error } = await mc.emails.queue({
30
30
 
31
31
  The `template` field must be present for `{{mc-unsubscribe-url}}` to be substituted.
32
32
 
33
- ### `List-Unsubscribe` Headers (Non-Transactional)
33
+ ## `List-Unsubscribe` Headers (Non-Transactional)
34
34
 
35
35
  Setting `transactional: false` tells MailChannels to add `List-Unsubscribe` and
36
36
  `List-Unsubscribe-Post` headers automatically. These headers are what inbox providers read
@@ -60,7 +60,7 @@ const { data, error } = await mc.emails.queue({
60
60
  If `transactional: false` is set but a personalization has more than one recipient,
61
61
  the SDK returns a `validation_error` before making any HTTP call.
62
62
 
63
- ### When To Use Which
63
+ ## When To Use Which
64
64
 
65
65
  | Message type | In-body link | `transactional: false` headers |
66
66
  | --- | --- | --- |
@@ -3,7 +3,7 @@
3
3
  MailChannels posts batched delivery events to a URL you register. Events cover both
4
4
  `emails.send()` and `emails.queue()` sends and use the same payload shape.
5
5
 
6
- ### Enroll And Manage
6
+ ## Enroll And Manage
7
7
 
8
8
  ```ts
9
9
  import { MailChannels } from 'mailchannels-sdk'
@@ -25,7 +25,7 @@ Enroll the replacement first if you're swapping URLs.
25
25
 
26
26
  If the endpoint is already enrolled, `create()` returns `conflict_error`.
27
27
 
28
- ### Validate
28
+ ## Validate
29
29
 
30
30
  `mc.webhooks.validate()` sends a synthetic test request to **every** enrolled webhook and
31
31
  reports each one's response. Useful as a deploy check.
@@ -47,7 +47,7 @@ for (const entry of data?.results ?? []) {
47
47
 
48
48
  The test payload carries `event: 'test'` and a hardcoded sender of `test@mailchannels.com`.
49
49
 
50
- ### Inspect Batches
50
+ ## Inspect Batches
51
51
 
52
52
  `mc.webhooks.batches()` returns up to 500 batch summaries with status, status code,
53
53
  duration, and event count. Use it to investigate failed deliveries.
@@ -72,7 +72,7 @@ const { data: recent } = await mc.webhooks.batches({
72
72
  Time formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object. If neither `createdAfter`
73
73
  nor `createdBefore` is set, the default range is the last 3 days.
74
74
 
75
- ### Resend A Batch
75
+ ## Resend A Batch
76
76
 
77
77
  ```ts
78
78
  const { data, error } = await mc.webhooks.resendBatch(12345)
@@ -83,7 +83,7 @@ console.log(data?.statusCode, data?.duration)
83
83
  A successful call means the resend attempt completed — not that your webhook returned 2xx.
84
84
  Check `data.statusCode` to see what your endpoint actually returned.
85
85
 
86
- ### Verify Incoming Webhooks (Crucial)
86
+ ## Verify Incoming Webhooks (Crucial)
87
87
 
88
88
  MailChannels signs every webhook request with an Ed25519 signature. `Webhooks.verify()`
89
89
  does the full verification — content digest, freshness, and signature — in one call.
@@ -126,7 +126,7 @@ instance (no API key needed). It is also available as an instance method on
126
126
  the signature will never match. Use `req.body.toString()` (Express with raw middleware),
127
127
  `await request.text()` (Fetch API / Hono / Cloudflare Workers), or the framework equivalent.
128
128
 
129
- #### Supplying The Public Key Manually
129
+ ### Supplying The Public Key Manually
130
130
 
131
131
  By default `verify()` fetches and caches the public key automatically from MailChannels.
132
132
  You can supply it yourself to avoid the outbound call:
@@ -148,7 +148,7 @@ Cache the key — it only changes on rotation, and MailChannels may publish mult
148
148
  keys at once during a rollover. Always fetch by the `keyId` from the incoming request
149
149
  rather than holding a single "current" key.
150
150
 
151
- ### Event Payload Shape
151
+ ## Event Payload Shape
152
152
 
153
153
  After successful verification `data` is typed as an array of webhook events. Common shared
154
154
  fields:
@@ -167,7 +167,7 @@ fields:
167
167
  | `reason` | `string?` | A human readable explanation of the status code. Do not use for system logic. Present on `hard-bounced`, `soft-bounced` events. |
168
168
  | `url` / `userAgent` / `ip` | `string?` | Present on `click` / `open` events. |
169
169
 
170
- ### Responding
170
+ ## Responding
171
171
 
172
172
  Return any 2xx status code quickly. MailChannels treats anything else as a failure and may
173
173
  retry. Keep your handler thin: enqueue the event and return, then do the actual processing
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![TypeScript][typescript-src]][typescript-href]
10
10
  [![Node.js][node-src]][node-href]
11
11
 
12
- > Built and tested against Email API `1.5.0`
12
+ > Built and tested against Email API `1.6.0`
13
13
 
14
14
  Node.js SDK to integrate [MailChannels Email API](https://docs.mailchannels.com/email-api) into your JavaScript or TypeScript server-side applications.
15
15
 
@@ -28,6 +28,7 @@ This library provides a simple way to interact with the [MailChannels Email API]
28
28
  - 📚 [Usage](#usage)
29
29
  - 📐 [Naming Conventions](#naming-conventions)
30
30
  - 🧪 [Local simulator](#local-simulator)
31
+ - 📬 [Using with Nodemailer](#using-with-nodemailer)
31
32
  - 🤖 [Using with an AI agent](#using-with-an-ai-agent)
32
33
  - ⚖️ [License](#license)
33
34
  - 💻 [Development](#development)
@@ -120,11 +121,22 @@ This package includes a local MailChannels simulator you can run via the CLI. It
120
121
 
121
122
  ### Start the simulator
122
123
 
124
+ CLI:
125
+
123
126
  ```sh
124
127
  # default: http://127.0.0.1:8787
125
128
  npx mailchannels-sdk simulate
126
129
  ```
127
130
 
131
+ Programmatically:
132
+
133
+ ```ts
134
+ import { createSimulator } from 'mailchannels-sdk/simulator'
135
+
136
+ const simulator = createSimulator()
137
+ const simulatorUrl = await simulator.listen()
138
+ ```
139
+
128
140
  ### Options
129
141
 
130
142
  | Option | Description | Default |
@@ -185,6 +197,65 @@ const { data, error } = await mailchannels.emails.send({
185
197
  The next planned expansion is outbound webhook delivery so client applications can test webhook ingestion flows against the simulator as well.
186
198
  <!-- #endregion simulator -->
187
199
 
200
+ ## <a name="using-with-nodemailer">📬 Using with Nodemailer</a>
201
+
202
+ The SDK provides a transport for Nodemailer that allows you to send emails using the MailChannels Email API.
203
+
204
+ ### Install
205
+
206
+ ```sh
207
+ # npm
208
+ npm i mailchannels-sdk nodemailer && npm i -D @types/nodemailer
209
+
210
+ # yarn
211
+ yarn add mailchannels-sdk nodemailer && yarn add -D @types/nodemailer
212
+
213
+ # pnpm
214
+ pnpm add mailchannels-sdk nodemailer && pnpm add -D @types/nodemailer
215
+ ```
216
+
217
+ ### Sending
218
+
219
+ ```ts
220
+ import nodemailer from 'nodemailer'
221
+ import { mailchannelsTransport } from 'mailchannels-sdk/nodemailer'
222
+
223
+ const transport = nodemailer.createTransport(
224
+ mailchannelsTransport({
225
+ apiKey: 'your-api-key',
226
+ sendMode: 'async', // 'async' or 'sync'. Default is 'async'
227
+ // SDK client options: `baseUrl`, `timeout`, `signal`, `retry`
228
+ })
229
+ )
230
+
231
+ transport.sendMail({
232
+ from: 'sender@example.com',
233
+ to: 'recipient@example.com',
234
+ subject: 'Hello from Nodemailer',
235
+ html: '<p>Hello World</p>',
236
+ mailchannels: {
237
+ // SDK send options: `campaignId`, `tracking`, `transactional`, `unsubscribe`
238
+ }
239
+ }, (error, info) => {
240
+ if (error) {
241
+ console.error('Error sending email:', error)
242
+ return;
243
+ }
244
+ console.log('Sent message info:', info)
245
+ })
246
+ ```
247
+
248
+ ### Limitations
249
+
250
+ The MailChannels Nodemailer transport maps a subset of Nodemailer features to the MailChannels SDK. Some features are not supported or have limitations:
251
+
252
+ - Only a single `replyTo` address is supported
253
+ - Multiple DKIM signatures are not supported
254
+ - For async sends (`sendMode: 'async'`), the response will have a `messageId` of `null`, and both the `accepted` and `rejected` arrays will be empty
255
+ - Attachment `path`, `href` and other URL fields are not supported
256
+ - MailChannels-specific send options must be passed via the augmented `mailchannels` field in the `sendMail` options
257
+ - Some advanced Nodemailer behaviors may be ignored or transformed when mapped to the SDK
258
+
188
259
  ## <a name="using-with-an-ai-agent">🤖 Using with an AI agent</a>
189
260
 
190
261
  This repository ships a complete agent skill at
@@ -273,7 +344,7 @@ pnpm test:watch
273
344
  # Run typecheck
274
345
  pnpm test:types
275
346
 
276
- # Refresh API parity fixtures, specs, and README version note
347
+ # Refresh API parity fixtures and README version note
277
348
  pnpm parity:fixtures
278
349
 
279
350
  # Run the local simulator