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.
- package/.agents/skills/mailchannels-js/SKILL.md +4 -0
- package/.agents/skills/mailchannels-js/resources/attachments.md +8 -10
- package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +5 -5
- package/.agents/skills/mailchannels-js/resources/custom-headers.md +4 -4
- package/.agents/skills/mailchannels-js/resources/dkim.md +10 -10
- package/.agents/skills/mailchannels-js/resources/domain-checks.md +6 -6
- package/.agents/skills/mailchannels-js/resources/error-handling.md +5 -5
- package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +9 -9
- package/.agents/skills/mailchannels-js/resources/overview.md +5 -5
- package/.agents/skills/mailchannels-js/resources/plugins/nodemailer.md +112 -0
- package/.agents/skills/mailchannels-js/resources/sending.md +10 -10
- package/.agents/skills/mailchannels-js/resources/sub-accounts.md +7 -9
- package/.agents/skills/mailchannels-js/resources/suppressions.md +4 -4
- package/.agents/skills/mailchannels-js/resources/templates.md +5 -5
- package/.agents/skills/mailchannels-js/resources/testing.md +52 -5
- package/.agents/skills/mailchannels-js/resources/unsubscribe.md +3 -3
- package/.agents/skills/mailchannels-js/resources/webhooks.md +8 -8
- package/README.md +73 -2
- package/dist/_chunks/mailchannels.d.mts +2094 -0
- package/dist/_chunks/mailchannels.mjs +2201 -0
- package/dist/_chunks/simulator.mjs +16 -8
- package/dist/cli/index.mjs +268 -0
- package/dist/mailchannels.d.mts +1 -2095
- package/dist/mailchannels.mjs +1 -2201
- package/dist/plugins/nodemailer/index.d.mts +33 -0
- package/dist/plugins/nodemailer/index.mjs +145 -0
- package/dist/simulator/index.d.mts +20 -0
- package/dist/simulator/index.mjs +2 -0
- package/package.json +21 -9
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -50
|
@@ -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
|
-
|
|
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
|
-
|
|
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,14 +43,13 @@ 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
|
-
|
|
46
|
+
## Inline Images (CID References)
|
|
47
47
|
|
|
48
|
-
Pass
|
|
48
|
+
Pass a `contentId` to embed an image inside the HTML body:
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
51
|
const logo = Attachment.fromBytes(bytes, {
|
|
52
52
|
filename: 'logo.png',
|
|
53
|
-
disposition: 'inline',
|
|
54
53
|
contentId: 'company-logo'
|
|
55
54
|
})
|
|
56
55
|
|
|
@@ -63,7 +62,7 @@ const { data, error } = await mc.emails.queue({
|
|
|
63
62
|
})
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
## Attachment Options
|
|
67
66
|
|
|
68
67
|
Both `fromBytes` and `fromBlob` accept an `AttachmentOptions` object:
|
|
69
68
|
|
|
@@ -72,9 +71,8 @@ Both `fromBytes` and `fromBlob` accept an `AttachmentOptions` object:
|
|
|
72
71
|
| `filename` | `string` | Required. MIME type is inferred from it when `type` is omitted. |
|
|
73
72
|
| `type` | `string` | MIME type. Inferred from `filename` if omitted. For `fromBlob`, defaults to the Blob's own `type`. |
|
|
74
73
|
| `contentId` | `string` | For `cid:` inline image references. |
|
|
75
|
-
| `disposition` | `'attachment' \| 'inline'` | Defaults to `'attachment'`. |
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
## Awaiting Attachments Lazily
|
|
78
76
|
|
|
79
77
|
The `attachments` field accepts `(EmailsSendAttachment | Promise<EmailsSendAttachment>)[]`,
|
|
80
78
|
so you can pass unresolved promises and the SDK will await them before sending:
|
|
@@ -92,13 +90,13 @@ const { data, error } = await mc.emails.queue({
|
|
|
92
90
|
})
|
|
93
91
|
```
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
## Limits
|
|
96
94
|
|
|
97
95
|
- Up to **1000 attachments** per send.
|
|
98
96
|
- Combined message + headers + attachments must stay under **30 MB**. Exceeding this
|
|
99
97
|
returns `payload_too_large_error` (HTTP 413) in the `error` field.
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
## Raw Attachment Shape
|
|
102
100
|
|
|
103
101
|
If you need to construct one by hand:
|
|
104
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
### Volume Buckets
|
|
55
55
|
|
|
56
56
|
`data.buckets` has `processed`, `delivered`, `dropped`.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
### Engagement Buckets
|
|
59
59
|
|
|
60
60
|
`data.buckets` has `open`, `click`, `uniqueOpen`, `uniqueClick`, `openTrackingDelivered`,
|
|
61
61
|
`clickTrackingDelivered`, `uniqueOpenTrackingDelivered`, `uniqueClickTrackingDelivered`.
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
### Performance Buckets
|
|
64
64
|
|
|
65
65
|
`data.buckets` has `processed`, `delivered`, `bounced`, `complained`.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
### Recipient Behaviour Buckets
|
|
68
68
|
|
|
69
69
|
`data.buckets` has `unsubscribed`, `unsubscribeDelivered`.
|
|
70
70
|
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|