mailchannels-sdk 1.3.1 → 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 (30) hide show
  1. package/.agents/skills/mailchannels-js/SKILL.md +4 -0
  2. package/.agents/skills/mailchannels-js/resources/attachments.md +7 -7
  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 -7
  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 +71 -0
  19. package/dist/_chunks/mailchannels.d.mts +2094 -0
  20. package/dist/_chunks/mailchannels.mjs +2201 -0
  21. package/dist/_chunks/{simulate.mjs → simulator.mjs} +7 -39
  22. package/dist/cli/index.mjs +266 -13
  23. package/dist/mailchannels.d.mts +1 -2093
  24. package/dist/mailchannels.mjs +1 -2199
  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 +18 -6
  30. package/dist/cli/index.d.mts +0 -1
@@ -34,6 +34,10 @@ You're about to write JS/TS SDK code. What does it need to do?
34
34
  ├── Set up the client (API keys, base URL, options, lifecycle)
35
35
  │ → resources/clients-and-transport.md
36
36
 
37
+ ├── Send through a framework's mail API instead of using the SDK directly
38
+ │ └── …Nodemailer?
39
+ │ → resources/plugins/nodemailer.md
40
+
37
41
  ├── Send email
38
42
  │ → resources/sending.md
39
43
  │ ├── …with file / bytes / URL / inline-image attachments?
@@ -4,7 +4,7 @@ MailChannels expects every attachment to be Base64-encoded with a `filename` and
4
4
  MIME `type`. Use the `Attachment` static helpers — they encode for you and infer the MIME
5
5
  type from the filename.
6
6
 
7
- ### From In-Memory Bytes
7
+ ## From In-Memory Bytes
8
8
 
9
9
  `fromBytes` is synchronous and accepts any `ArrayBuffer` or `Uint8Array`:
10
10
 
@@ -33,7 +33,7 @@ const { data, error } = await mc.emails.queue({
33
33
  })
34
34
  ```
35
35
 
36
- ### From A Blob
36
+ ## From A Blob
37
37
 
38
38
  `fromBlob` is async and accepts any Web API `Blob`. The Blob's own `type` property is used
39
39
  as the MIME type unless you override it in options:
@@ -43,7 +43,7 @@ const blob = new Blob(['Hello world'], { type: 'text/plain' })
43
43
  const attachment = await Attachment.fromBlob(blob, { filename: 'hello.txt' })
44
44
  ```
45
45
 
46
- ### Inline Images (CID References)
46
+ ## Inline Images (CID References)
47
47
 
48
48
  Pass a `contentId` to embed an image inside the HTML body:
49
49
 
@@ -62,7 +62,7 @@ const { data, error } = await mc.emails.queue({
62
62
  })
63
63
  ```
64
64
 
65
- ### Attachment Options
65
+ ## Attachment Options
66
66
 
67
67
  Both `fromBytes` and `fromBlob` accept an `AttachmentOptions` object:
68
68
 
@@ -72,7 +72,7 @@ Both `fromBytes` and `fromBlob` accept an `AttachmentOptions` object:
72
72
  | `type` | `string` | MIME type. Inferred from `filename` if omitted. For `fromBlob`, defaults to the Blob's own `type`. |
73
73
  | `contentId` | `string` | For `cid:` inline image references. |
74
74
 
75
- ### Awaiting Attachments Lazily
75
+ ## Awaiting Attachments Lazily
76
76
 
77
77
  The `attachments` field accepts `(EmailsSendAttachment | Promise<EmailsSendAttachment>)[]`,
78
78
  so you can pass unresolved promises and the SDK will await them before sending:
@@ -90,13 +90,13 @@ const { data, error } = await mc.emails.queue({
90
90
  })
91
91
  ```
92
92
 
93
- ### Limits
93
+ ## Limits
94
94
 
95
95
  - Up to **1000 attachments** per send.
96
96
  - Combined message + headers + attachments must stay under **30 MB**. Exceeding this
97
97
  returns `payload_too_large_error` (HTTP 413) in the `error` field.
98
98
 
99
- ### Raw Attachment Shape
99
+ ## Raw Attachment Shape
100
100
 
101
101
  If you need to construct one by hand:
102
102
 
@@ -17,7 +17,7 @@ A missing or empty API key throws synchronously at construction:
17
17
  new MailChannels('') // throws Error: "Missing MailChannels API key."
18
18
  ```
19
19
 
20
- ### Constructor Options
20
+ ## Constructor Options
21
21
 
22
22
  ```ts
23
23
  // new MailChannels(apiKey: string, options?: MailChannelsClientOptions)
@@ -30,7 +30,7 @@ new MailChannels('') // throws Error: "Missing MailChannels API key."
30
30
  | `retry` | `boolean \| number \| RetryOptions` | `false` | `ofetch` retry options. |
31
31
  | `signal` | `AbortSignal` | `undefined` | Propagated to every fetch call made by this instance. |
32
32
 
33
- ### Multi-Tenant: One Client Per Credential
33
+ ## Multi-Tenant: One Client Per Credential
34
34
 
35
35
  Never share a parent key and a sub-account key on the same instance. Create a separate
36
36
  `MailChannels` for each credential:
@@ -50,7 +50,7 @@ const { data: queueB, error: queueErrB } = await tenantB.emails.queue({ ... })
50
50
  if (queueErrB) { /*...*/ }
51
51
  ```
52
52
 
53
- ### Request Cancellation
53
+ ## Request Cancellation
54
54
 
55
55
  Pass an `AbortSignal` to cancel in-flight requests:
56
56
 
@@ -63,7 +63,7 @@ const mc = new MailChannels(apiKey, { signal: controller.signal })
63
63
  controller.abort()
64
64
  ```
65
65
 
66
- ### Headers Sent On Every Request
66
+ ## Headers Sent On Every Request
67
67
 
68
68
  The SDK sets these on every request and they cannot be overridden via `MailChannelsClientOptions`:
69
69
 
@@ -72,7 +72,7 @@ The SDK sets these on every request and they cannot be overridden via `MailChann
72
72
  - `Content-Type: application/json`
73
73
  - `User-Agent: mailchannels-node/<version>`
74
74
 
75
- ### Extending The Client
75
+ ## Extending The Client
76
76
 
77
77
  The `MailChannels` class extends the public `MailChannelsClient` base. If you need to
78
78
  intercept, proxy, or instrument requests you can subclass `MailChannelsClient` and override
@@ -3,7 +3,7 @@
3
3
  Use `headers` for application-specific metadata, tracking IDs, campaign tags, or any
4
4
  custom `X-…` header.
5
5
 
6
- ### Root vs Per-Personalization
6
+ ## Root vs Per-Personalization
7
7
 
8
8
  - **Root `headers`**: applied to every personalization.
9
9
  - **Per-personalization `headers`**: applied only to that personalization.
@@ -31,7 +31,7 @@ const { data, error } = await mc.emails.queue({
31
31
  })
32
32
  ```
33
33
 
34
- ### Reserved Headers — Never Set These
34
+ ## Reserved Headers — Never Set These
35
35
 
36
36
  MailChannels controls a fixed set of message headers. The SDK pre-validates and returns a
37
37
  `validation_error` client-side before any request that tries to set one. Header names are
@@ -53,12 +53,12 @@ checked case-insensitively, so `From`, `from`, and `FROM` are all rejected.
53
53
  This list mirrors the SDK's `RESERVED_HEADER_NAMES` set — keep them in sync if MailChannels
54
54
  changes the list upstream.
55
55
 
56
- ### Case Sensitivity
56
+ ## Case Sensitivity
57
57
 
58
58
  Header names are treated case-insensitively by MailChannels. If two keys differ only by
59
59
  case, only one is used and the choice is unspecified. Pick one canonical casing per header.
60
60
 
61
- ### Values
61
+ ## Values
62
62
 
63
63
  Both keys and values must be strings. A non-string value returns a `validation_error`
64
64
  before the request leaves the client.
@@ -4,7 +4,7 @@ MailChannels can host the **private** DKIM key for you. You publish the **public
4
4
  the domain's DNS as a TXT record. The public DNS record is **not** hosted by MailChannels —
5
5
  you must publish it yourself in whatever DNS provider holds the zone.
6
6
 
7
- ### Create A Hosted Key
7
+ ## Create A Hosted Key
8
8
 
9
9
  ```ts
10
10
  import { MailChannels } from 'mailchannels-sdk'
@@ -27,7 +27,7 @@ for (const record of data?.dnsRecords ?? []) {
27
27
 
28
28
  The returned `data.dnsRecords` contains TXT records you must publish in DNS.
29
29
 
30
- ### List, Filter, And Include DNS Records
30
+ ## List, Filter, And Include DNS Records
31
31
 
32
32
  ```ts
33
33
  const { data, error } = await mc.domains.dkim.list('example.com', {
@@ -39,7 +39,7 @@ const { data, error } = await mc.domains.dkim.list('example.com', {
39
39
  })
40
40
  ```
41
41
 
42
- ### Key Lifecycle
42
+ ## Key Lifecycle
43
43
 
44
44
  | Status | Meaning |
45
45
  | --- | --- |
@@ -48,7 +48,7 @@ const { data, error } = await mc.domains.dkim.list('example.com', {
48
48
  | `retired` | No longer in use. |
49
49
  | `revoked` | Marked compromised. Stop using immediately. |
50
50
 
51
- #### Update status directly
51
+ ### Update status directly
52
52
 
53
53
  ```ts
54
54
  const { success, error } = await mc.domains.dkim.updateStatus('example.com', {
@@ -60,7 +60,7 @@ const { success, error } = await mc.domains.dkim.updateStatus('example.com', {
60
60
  Only `active` keys can move to `rotated`. Only `revoked`, `retired`, and `rotated` are
61
61
  valid update targets.
62
62
 
63
- #### Rotate (recommended for routine rollover)
63
+ ### Rotate (recommended for routine rollover)
64
64
 
65
65
  ```ts
66
66
  const { data, error } = await mc.domains.dkim.rotate('example.com', 'mcdkim', {
@@ -82,7 +82,7 @@ Rotation:
82
82
  **Publish the new DNS record before `gracePeriodExpiresAt`** or emails signed with the new
83
83
  key will fail DKIM at receiving providers.
84
84
 
85
- ### Sending With A Hosted Key
85
+ ## Sending With A Hosted Key
86
86
 
87
87
  ```ts
88
88
  const { data, error } = await mc.emails.queue({
@@ -100,7 +100,7 @@ const { data, error } = await mc.emails.queue({
100
100
  If `dkim.selector` is set without `dkim.domain`, MailChannels takes the domain from the
101
101
  `from` address.
102
102
 
103
- ### Sending With A Customer-Managed Key
103
+ ## Sending With A Customer-Managed Key
104
104
 
105
105
  If you keep the private key yourself, pass it Base64-encoded (PEM headers are stripped
106
106
  automatically):
@@ -121,7 +121,7 @@ const { data, error } = await mc.emails.queue({
121
121
 
122
122
  `dkim` can also be set per-personalization to override the root value.
123
123
 
124
- ### DNS Publication
124
+ ## DNS Publication
125
125
 
126
126
  MailChannels does not host the public DKIM DNS record — you do. The SDK returns the exact
127
127
  record to publish in `data.dnsRecords`:
@@ -132,7 +132,7 @@ record to publish in `data.dnsRecords`:
132
132
  | `type` | Always `TXT`. |
133
133
  | `value` | Public key material. **Publish verbatim** — do not re-wrap, strip quotes, or split lines. |
134
134
 
135
- #### Required Steps
135
+ ### Required Steps
136
136
 
137
137
  1. **Create** with `mc.domains.dkim.create(domain, { selector })`. Capture `dnsRecords`.
138
138
  2. **Resolve the DNS zone** in your provider's API.
@@ -142,7 +142,7 @@ record to publish in `data.dnsRecords`:
142
142
  The `dkim[].verdict` must be `'passed'` before sending real traffic.
143
143
  5. **For rotations**, leave the old TXT record in place until `gracePeriodExpiresAt`.
144
144
 
145
- ### Selector Format
145
+ ## Selector Format
146
146
 
147
147
  - 1–63 characters.
148
148
  - Lowercase letters, numbers, and `-` work everywhere. Avoid `_` in the selector itself.
@@ -11,7 +11,7 @@ sending from it. The check covers four things:
11
11
  - **Domain Lockdown** — a MailChannels feature that ties a sending domain to your account
12
12
  so other MailChannels customers cannot spoof it.
13
13
 
14
- ### Basic Check
14
+ ## Basic Check
15
15
 
16
16
  ```ts
17
17
  import { MailChannels } from 'mailchannels-sdk'
@@ -29,7 +29,7 @@ console.log(data?.dkim) // array of per-selector results
29
29
  console.log(data?.references) // support links if anything failed
30
30
  ```
31
31
 
32
- ### With Specific DKIM Settings
32
+ ## With Specific DKIM Settings
33
33
 
34
34
  If you don't pass `dkim`, MailChannels uses all stored keys for the domain. To target
35
35
  specific selectors:
@@ -63,7 +63,7 @@ const { data, error } = await mc.domains.check('example.com', {
63
63
  })
64
64
  ```
65
65
 
66
- #### DKIM Settings Resolution Rules
66
+ ### DKIM Settings Resolution Rules
67
67
 
68
68
  | Provided fields | Behavior |
69
69
  | --- | --- |
@@ -74,7 +74,7 @@ const { data, error } = await mc.domains.check('example.com', {
74
74
  | `privateKey` set | `selector` is required too. |
75
75
  | `dkim` empty / absent | Use all stored keys for the request domain. |
76
76
 
77
- ### With A Sender ID (Domain Lockdown)
77
+ ## With A Sender ID (Domain Lockdown)
78
78
 
79
79
  If your lockdown record uses `senderid=` or `sidw=` fields, pass the sender identity:
80
80
 
@@ -86,14 +86,14 @@ const { data, error } = await mc.domains.check('example.com', {
86
86
 
87
87
  If your lockdown record uses `auth=` (account-wide authorization), omit `senderId`.
88
88
 
89
- ### Verdicts Reference
89
+ ## Verdicts Reference
90
90
 
91
91
  - DKIM, Domain Lockdown, A, and MX verdicts: `'passed'` or `'failed'`.
92
92
  - SPF has a richer set: `'passed'`, `'failed'`, `'soft failed'`, `'temporary error'`,
93
93
  `'permanent error'`, `'neutral'`, `'none'`, `'unknown'`.
94
94
  - `senderDomain` passes if **either** the A or MX check passes.
95
95
 
96
- ### When To Use
96
+ ## When To Use
97
97
 
98
98
  - During onboarding for every new sending domain.
99
99
  - In CI after rotating DKIM keys, to verify the new DNS record propagated.
@@ -31,7 +31,7 @@ if (error) {
31
31
  console.log(data.requestId)
32
32
  ```
33
33
 
34
- ### Error Types
34
+ ## Error Types
35
35
 
36
36
  | `error.type` | HTTP status | Meaning |
37
37
  | --- | --- | --- |
@@ -48,7 +48,7 @@ console.log(data.requestId)
48
48
  | `'application_error'` | `null` | Unexpected JS/network error (e.g. fetch failed). |
49
49
  | `'api_error'` | varies | Fallback for unmapped status codes. |
50
50
 
51
- ### Handling Patterns
51
+ ## Handling Patterns
52
52
 
53
53
  The `message` field is a human-readable description of the error, useful for logging and debugging,
54
54
  but it should not be parsed or used for control flow as it may change without warning. Instead, use the `type` field for error handling logic.
@@ -87,7 +87,7 @@ if (error) {
87
87
  }
88
88
  ```
89
89
 
90
- ### Client-Side Validation Errors (`validation_error`)
90
+ ## Client-Side Validation Errors (`validation_error`)
91
91
 
92
92
  These produce `error.type === 'validation_error'` and `error.statusCode === null` before
93
93
  any HTTP call:
@@ -102,7 +102,7 @@ any HTTP call:
102
102
 
103
103
  Failing early gives a precise error rather than a vague 400.
104
104
 
105
- ### Retrying and Idempotency
105
+ ## Retrying and Idempotency
106
106
 
107
107
  | Error type | Safe to retry? |
108
108
  | --- | --- |
@@ -114,7 +114,7 @@ The API has no idempotency keys, so retrying `emails.send()` or `emails.queue()`
114
114
  transient failure can produce a duplicate send. Build idempotency into the caller (e.g. a
115
115
  unique `campaignId` + recipient-set check) when at-most-once delivery matters.
116
116
 
117
- ### SDK Exceptions
117
+ ## SDK Exceptions
118
118
 
119
119
  `new MailChannels('')` (empty or missing key) throws synchronously:
120
120
 
@@ -3,7 +3,7 @@
3
3
  The metrics module exposes the operational view of your traffic. Usage exposes
4
4
  billing-period totals.
5
5
 
6
- ### Time-Series Metrics
6
+ ## Time-Series Metrics
7
7
 
8
8
  The four time-series methods share the same `MetricsOptions` parameter shape: optional
9
9
  `startTime`, `endTime`, `campaignId`, and `interval`.
@@ -37,7 +37,7 @@ const { data: behaviour, error: behErr } = await mc.metrics.recipientBehaviou
37
37
  Time formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object. Defaults: `startTime` is
38
38
  one month ago, `endTime` is now.
39
39
 
40
- #### Buckets
40
+ ### Buckets
41
41
 
42
42
  Each response includes both totals **and** a `buckets` object grouped by metric name, each
43
43
  containing a list of `{ count, periodStart }` rows aligned to `interval`.
@@ -51,24 +51,24 @@ for (const bucket of data?.buckets.processed ?? []) {
51
51
  }
52
52
  ```
53
53
 
54
- #### Volume Buckets
54
+ ### Volume Buckets
55
55
 
56
56
  `data.buckets` has `processed`, `delivered`, `dropped`.
57
57
 
58
- #### Engagement Buckets
58
+ ### Engagement Buckets
59
59
 
60
60
  `data.buckets` has `open`, `click`, `uniqueOpen`, `uniqueClick`, `openTrackingDelivered`,
61
61
  `clickTrackingDelivered`, `uniqueOpenTrackingDelivered`, `uniqueClickTrackingDelivered`.
62
62
 
63
- #### Performance Buckets
63
+ ### Performance Buckets
64
64
 
65
65
  `data.buckets` has `processed`, `delivered`, `bounced`, `complained`.
66
66
 
67
- #### Recipient Behaviour Buckets
67
+ ### Recipient Behaviour Buckets
68
68
 
69
69
  `data.buckets` has `unsubscribed`, `unsubscribeDelivered`.
70
70
 
71
- ### Sender Metrics
71
+ ## Sender Metrics
72
72
 
73
73
  `mc.metrics.senders()` lists per-sender totals grouped either by campaign or sub-account.
74
74
 
@@ -92,7 +92,7 @@ for (const sender of data?.senders ?? []) {
92
92
 
93
93
  Senders with **zero traffic in the time range are omitted** from the response.
94
94
 
95
- ### Usage
95
+ ## Usage
96
96
 
97
97
  ```ts
98
98
  const { data: usage, error } = await mc.metrics.usage()
@@ -104,7 +104,7 @@ console.log(usage?.total, usage?.startDate, usage?.endDate)
104
104
  For a specific sub-account, use `mc.subAccounts.getUsage(handle)` instead. See
105
105
  [sub-accounts](sub-accounts.md).
106
106
 
107
- ### Common Patterns
107
+ ## Common Patterns
108
108
 
109
109
  - **Daily dashboard**: `mc.metrics.volume({ interval: 'day' })` plus
110
110
  `mc.metrics.engagement({ interval: 'day' })` over the same range.
@@ -17,7 +17,7 @@ const { data, error } = await mc.emails.queue({
17
17
  })
18
18
  ```
19
19
 
20
- ### Module Inventory
20
+ ## Module Inventory
21
21
 
22
22
  | Property | What it does |
23
23
  | --- | --- |
@@ -28,7 +28,7 @@ const { data, error } = await mc.emails.queue({
28
28
  | `mc.metrics` | Volume, engagement, performance, recipient-behaviour, sender metrics. |
29
29
  | `mc.suppressions` | List, create, delete suppression entries. |
30
30
 
31
- ### Key Concept: Personalizations
31
+ ## Key Concept: Personalizations
32
32
 
33
33
  A single `emails.send()` or `emails.queue()` call can produce **many individual messages**.
34
34
  The `personalizations` array is the advanced form: each entry is one
@@ -39,7 +39,7 @@ act as defaults that each personalization can override.
39
39
  For simple sends the shorthand fields (`to`, `cc`, `bcc`, `html`, `text`) cover most cases
40
40
  without needing to write out personalizations explicitly.
41
41
 
42
- ### `send` vs `queue`
42
+ ## `send` vs `queue`
43
43
 
44
44
  | Method | Server behaviour | When to use |
45
45
  | --- | --- |-----------------------------------------------------------------------------------|
@@ -48,7 +48,7 @@ without needing to write out personalizations explicitly.
48
48
 
49
49
  `emails.sendAsync()` is a deprecated alias for `queue()` — use `queue()` in new code.
50
50
 
51
- ### Response Shape
51
+ ## Response Shape
52
52
 
53
53
  Every SDK method returns one of two shapes:
54
54
 
@@ -76,7 +76,7 @@ if (error) {
76
76
  console.log(data.requestId)
77
77
  ```
78
78
 
79
- ### Configuration
79
+ ## Configuration
80
80
 
81
81
  ```ts
82
82
  const mc = new MailChannels('YOUR-API-KEY', {
@@ -0,0 +1,112 @@
1
+ # Nodemailer Integration
2
+
3
+ The SDK ships a transport for Nodemailer so existing code that uses Nodemailer can be
4
+ easily adapted to send via MailChannels Email API. The transport method
5
+ `mailchannelsTransport` can be imported from `mailchannels-sdk/nodemailer`. This
6
+ integration is optional — it requires the `nodemailer` package which the SDK does not
7
+ include as a dependency.
8
+
9
+ ## Sending
10
+
11
+ ```ts
12
+ import nodemailer from 'nodemailer'
13
+ import { mailchannelsTransport } from 'mailchannels-sdk/nodemailer'
14
+
15
+ const transport = nodemailer.createTransport(
16
+ mailchannelsTransport({
17
+ apiKey: 'YOUR-API-KEY',
18
+ sendMode: 'async', // 'async' or 'sync'. Default is 'async'
19
+ // SDK client options: `baseUrl`, `timeout`, `signal`, `retry`
20
+ })
21
+ )
22
+
23
+ transport.sendMail({
24
+ from: 'sender@example.com',
25
+ to: 'recipient@example.com',
26
+ subject: 'Hello from Nodemailer',
27
+ html: '<p>Hello World</p>',
28
+ mailchannels: {
29
+ // SDK send options: `campaignId`, `tracking`, `transactional`, `unsubscribe`
30
+ }
31
+ }, (error, info) => {
32
+ if (error) {
33
+ console.error('Error sending email:', error)
34
+ return;
35
+ }
36
+ console.log('Sent message info:', info)
37
+ })
38
+ ```
39
+
40
+ ## Transport Options
41
+
42
+ The transport accepts `apiKey` and `sendMode` options, as well as any SDK client
43
+ options (`baseUrl`, `timeout`, `signal`, `retry`).
44
+
45
+ Refer to the [clients-and-transport](../clients-and-transport.md) documentation for
46
+ details and full explanations of each client option.
47
+
48
+ ## Field Mapping
49
+
50
+ The transport converts Nodemailer `Mail.Options` in the `sendMail`
51
+ call into the SDK's `EmailsSendOptions`. The HTTP request itself is
52
+ entirely delegated to the SDK `MailChannelsClient`.
53
+
54
+ | `Mail.Options` | `EmailsSendOptions` |
55
+ | --- | --- |
56
+ | `from` | `from` |
57
+ | `to`, `cc`, `bcc` | `to`, `cc`, `bcc` (parsed into SDK `EmailsSendRecipient` objects or strings) |
58
+ | `replyTo` | `replyTo` (if array, only the first address is used) |
59
+ | `subject` | `subject` |
60
+ | `text` | `text` |
61
+ | `html` | `html` |
62
+ | `headers` | `headers` (converted to a `Record<string,string>`) |
63
+ | `attachments` | `attachments` (only inline buffers/strings supported — `path`/`href` are not supported) |
64
+ | `icalEvent` | added as an attachment |
65
+ | `dkim` | `dkim` (see DKIM notes below) |
66
+ | `mailchannels` (augmented field on the send options) | merged into the SDK send params |
67
+
68
+ ### MailChannels-specific Options
69
+
70
+ The SDK augments Nodemailer's `Mail.Options` type with a `mailchannels` property,
71
+ allowing MailChannels-specific send options to be passed directly through the
72
+ `sendMail` options object.
73
+
74
+ Supported options: `campaignId`, `tracking`, `transactional`, `unsubscribe`
75
+
76
+ Refer back to the original SKILL.md for links to full explanations of each field.
77
+
78
+ ### DKIM
79
+
80
+ - Multiple DKIM signatures with the Nodemailer's `dkim.keys` field are not
81
+ supported; specify a single DKIM signature with `dkim.domainName`,
82
+ `dkim.keySelector`, and `dkim.privateKey`.
83
+ - When providing `dkim.privateKey` as a `{ key, passphrase }` object the
84
+ transport attempts to convert it into a PEM via Node's `createPrivateKey`.
85
+ - Only RSA private keys are supported
86
+
87
+ ## Errors and Result Structure
88
+
89
+ The transport follows the Nodemailer `Transport` contract. The callback is
90
+ called with an `error` object (if any; including SDK errors) and an `info` object
91
+ with the following structure:
92
+
93
+ ```ts
94
+ interface MailChannelsTransportInfo<T extends MailChannelsTransportSendMode> {
95
+ messageId: string | null; // null for async sends
96
+ accepted: string[]; // empty array for async sends
97
+ rejected: string[]; // empty array for async sends
98
+ envelope: MimeNode.Envelope; // `to` includes all recipients (to, cc, bcc)
99
+ response: (T extends "sync" ? EmailsSendResponse : EmailsQueueResponse) | null; // SDK response
100
+ }
101
+ ```
102
+
103
+ ## Limitations
104
+
105
+ - Only a single `replyTo` address is supported.
106
+ - Multiple DKIM signatures are not supported; the API only supports RSA private keys.
107
+ - Attachment `path`, `href`, and other URL-based attachment sources are not supported;
108
+ attachments should be provided as buffers or strings.
109
+ - For async sends (`sendMode: 'async'`) the transport returns a `messageId` of `null`
110
+ and empty `accepted` / `rejected` arrays — this is an API limitation for queued sends.
111
+ - MailChannels-specific options must be passed using the `mailchannels` augmentation on
112
+ the `sendMail` options object.
@@ -3,7 +3,7 @@
3
3
  Use `mc.emails.queue()` as the default. Switch to `mc.emails.send()` only when you
4
4
  specifically need immediate results for a small number of messages, or the dry-run preview.
5
5
 
6
- ### Quickest Send
6
+ ## Quickest Send
7
7
 
8
8
  ```ts
9
9
  import { MailChannels } from 'mailchannels-sdk'
@@ -25,7 +25,7 @@ console.log(data.requestId)
25
25
  Provide both `text` and `html` whenever possible — receiving clients prefer the last
26
26
  matching content type and a plain-text fallback improves deliverability.
27
27
 
28
- ### Recipient Formats
28
+ ## Recipient Formats
29
29
 
30
30
  All recipient fields (`to`, `from`, `cc`, `bcc`, `replyTo`, `envelopeFrom`) accept multiple formats
31
31
  interchangeably. Note: `from`, `replyTo`, `envelopeFrom` must be single
@@ -38,7 +38,7 @@ const r3 = { email: 'recipient@example.net', name: 'Jane Smith' } // ob
38
38
  const r4 = ['a@example.net', { email: 'b@example.net', name: 'Bob' }] // array
39
39
  ```
40
40
 
41
- ### Per-Recipient Personalization
41
+ ## Per-Recipient Personalization
42
42
 
43
43
  `personalizations` is the advanced form — each entry is one fully-rendered outgoing message
44
44
  with optional per-recipient overrides:
@@ -63,7 +63,7 @@ const { data, error } = await mc.emails.queue({
63
63
  Per-personalization overridable fields: `to`, `cc`, `bcc`, `from`, `subject`, `replyTo`,
64
64
  `envelopeFrom`, `headers`, `dkim`, `template.data`.
65
65
 
66
- ### Body Content Fields
66
+ ## Body Content Fields
67
67
 
68
68
  At least one of `html`, `text`, or `content` is required. The shorthand fields are the
69
69
  simplest form; `content` is the explicit array form for more control.
@@ -112,7 +112,7 @@ const { data: data2, error: err2 } = await mc.emails.queue({
112
112
  })
113
113
  ```
114
114
 
115
- ### Dry Run (send endpoint only)
115
+ ## Dry Run (send endpoint only)
116
116
 
117
117
  `emails.send()` supports `dryRun: true`. The API validates and renders the message without
118
118
  delivering it. Useful for asserting templates render before launch:
@@ -136,7 +136,7 @@ console.log(data?.rendered?.[0])
136
136
 
137
137
  `emails.queue()` does **not** support dry-run.
138
138
 
139
- ### When To Pick Which
139
+ ## When To Pick Which
140
140
 
141
141
  | Situation | Method |
142
142
  |------------------------------------------------------------------| --- |
@@ -145,7 +145,7 @@ console.log(data?.rendered?.[0])
145
145
  | Need the rendered message for inspection | `mc.emails.send(options, true)` |
146
146
  | Need `status` results immediately for a small number of messages | `mc.emails.send()` |
147
147
 
148
- ### send() Response Details
148
+ ## send() Response Details
149
149
 
150
150
  ```ts
151
151
  const { data, error } = await mc.emails.send({ ... })
@@ -158,7 +158,7 @@ data?.results?.forEach(r => {
158
158
  })
159
159
  ```
160
160
 
161
- ### Common Pitfalls
161
+ ## Common Pitfalls
162
162
 
163
163
  - **Reserved headers**: don't set `From`, `To`, `Subject`, `Reply-To`, `Message-ID`,
164
164
  `Content-Type`, `DKIM-Signature`, etc. in `headers`. Use the payload fields instead.
@@ -177,7 +177,7 @@ data?.results?.forEach(r => {
177
177
  idempotency into the caller (e.g. a unique `campaignId` + recipient-set check) when
178
178
  at-most-once delivery matters.
179
179
 
180
- ### Tracking
180
+ ## Tracking
181
181
 
182
182
  ```ts
183
183
  const { data, error } = await mc.emails.queue({
@@ -194,7 +194,7 @@ const { data, error } = await mc.emails.queue({
194
194
 
195
195
  Open and click tracking require a subscription that supports them.
196
196
 
197
- ### Custom Tracking Domains
197
+ ## Custom Tracking Domains
198
198
 
199
199
  Pass `customDomainName` on the `tracking` field (or on `unsubscribe`) to route those
200
200
  links through a domain you registered instead of the shared MailChannels domain.