mailchannels-sdk 1.1.0 → 1.2.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/resources/custom-tracking-domains.md +83 -0
- package/.agents/skills/mailchannels-js/resources/error-handling.md +1 -0
- package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +3 -2
- package/.agents/skills/mailchannels-js/resources/sending.md +26 -0
- package/.agents/skills/mailchannels-js/resources/sub-accounts.md +1 -1
- package/README.md +6 -4
- package/dist/_chunks/simulator.mjs +110 -22
- package/dist/mailchannels.d.mts +247 -4
- package/dist/mailchannels.mjs +286 -13
- package/package.json +8 -8
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Custom Tracking Domains
|
|
2
|
+
|
|
3
|
+
Register your own hostname for click tracking, open tracking, or unsubscribe
|
|
4
|
+
links instead of MailChannels' shared domains. Once a domain is `active`,
|
|
5
|
+
select it by `name` via `customDomainName` on the send payload — see
|
|
6
|
+
[sending.md](sending.md#custom-tracking-domains).
|
|
7
|
+
|
|
8
|
+
## Register A Domain
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { MailChannels } from 'mailchannels-sdk'
|
|
12
|
+
|
|
13
|
+
const mc = new MailChannels('YOUR-API-KEY')
|
|
14
|
+
|
|
15
|
+
const { data, error } = await mc.domains.customTracking.create(
|
|
16
|
+
'newsletter-clicks', // label used at send time, ^[a-z0-9-]+$, max 64
|
|
17
|
+
'click.example.com',
|
|
18
|
+
'click' // click | open | unsubscribe
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Before this can complete, `hostname` needs a CNAME record pointing to
|
|
23
|
+
`links.mailchannels.net`.
|
|
24
|
+
|
|
25
|
+
## DNS Verification Is A Two-Step, Non-Error Flow
|
|
26
|
+
|
|
27
|
+
`create()` and `update()` (when re-activating a disabled domain) return one
|
|
28
|
+
of two shapes in `data` — both are 2xx responses, not errors:
|
|
29
|
+
|
|
30
|
+
- **Registered**: `dnsSetupRequired: false` with `name`, `hostname`, `scope`,
|
|
31
|
+
`status`, `createdAt` are all present..
|
|
32
|
+
- **Pending**: `dnsSetupRequired: true` with `token`, `txtRecordName`,
|
|
33
|
+
`txtRecordValue`, `instructions` is present (a CNAME-only failure can omit the TXT
|
|
34
|
+
fields; a fresh registration includes all of them).
|
|
35
|
+
|
|
36
|
+
Add the TXT record alongside the CNAME, wait for propagation, then call
|
|
37
|
+
`create()` again with the same `name`/`hostname`/`scope`.
|
|
38
|
+
|
|
39
|
+
If DNS still isn't in place on retry, MailChannels responds `422` instead of
|
|
40
|
+
`202` — that's a real error status, so the SDK returns an `error` object.
|
|
41
|
+
The same pending-verification body is on `error.response`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const { data, error } = await mc.domains.customTracking.create(
|
|
45
|
+
'newsletter-clicks',
|
|
46
|
+
'click.example.com',
|
|
47
|
+
'click'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
if (error.statusCode === 422 && error.response) {
|
|
52
|
+
console.log(error.response.instructions)
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## List, Update, Delete
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// List
|
|
62
|
+
const { data: listData, error: listError } = await mc.domains.customTracking.list({
|
|
63
|
+
scope: 'click', // optional filter
|
|
64
|
+
status: 'active', // optional filter: active | disabled
|
|
65
|
+
limit: 100,
|
|
66
|
+
offset: 0
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Update
|
|
70
|
+
const { data: updateData, error: updateError } = await mc.domains.customTracking.update(
|
|
71
|
+
'click.example.com',
|
|
72
|
+
'click',
|
|
73
|
+
{ status: 'disabled' } // or { name: 'new-label' } to rename
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Delete
|
|
77
|
+
const { success, error: deleteError } = await mc.domains.customTracking.delete('click.example.com', 'click')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Deleting a domain takes effect immediately — tracking links and unsubscribe
|
|
81
|
+
URLs already sent using that domain stop working right away. Re-activating a
|
|
82
|
+
domain whose DNS verification has since lapsed goes through the same
|
|
83
|
+
pending-result flow as `create()`.
|
|
@@ -23,6 +23,7 @@ if (error) {
|
|
|
23
23
|
// error.message — human-readable description
|
|
24
24
|
// error.type — stable string identifier (see table below)
|
|
25
25
|
// error.statusCode — HTTP status code, or null for non-HTTP errors
|
|
26
|
+
// error.response — Response body (Object) for 4xx/5xx errors
|
|
26
27
|
console.error(error.message, error.type, error.statusCode)
|
|
27
28
|
return
|
|
28
29
|
}
|
|
@@ -57,11 +57,12 @@ for (const bucket of data?.buckets.processed ?? []) {
|
|
|
57
57
|
|
|
58
58
|
#### Engagement Buckets
|
|
59
59
|
|
|
60
|
-
`data.buckets` has `open`, `click`, `
|
|
60
|
+
`data.buckets` has `open`, `click`, `uniqueOpen`, `uniqueClick`, `openTrackingDelivered`,
|
|
61
|
+
`clickTrackingDelivered`, `uniqueOpenTrackingDelivered`, `uniqueClickTrackingDelivered`.
|
|
61
62
|
|
|
62
63
|
#### Performance Buckets
|
|
63
64
|
|
|
64
|
-
`data.buckets` has `processed`, `delivered`, `bounced`.
|
|
65
|
+
`data.buckets` has `processed`, `delivered`, `bounced`, `complained`.
|
|
65
66
|
|
|
66
67
|
#### Recipient Behaviour Buckets
|
|
67
68
|
|
|
@@ -193,3 +193,29 @@ const { data, error } = await mc.emails.queue({
|
|
|
193
193
|
```
|
|
194
194
|
|
|
195
195
|
Open and click tracking require a subscription that supports them.
|
|
196
|
+
|
|
197
|
+
### Custom Tracking Domains
|
|
198
|
+
|
|
199
|
+
Pass `customDomainName` on the `tracking` field (or on `unsubscribe`) to route those
|
|
200
|
+
links through a domain you registered instead of the shared MailChannels domain.
|
|
201
|
+
The name must match an **active** custom tracking domain registered for that
|
|
202
|
+
scope — see [custom-tracking-domains](custom-tracking-domains.md) for
|
|
203
|
+
registration and DNS verification.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const { data, error } = await mc.emails.queue({
|
|
207
|
+
from: 'sender@example.com',
|
|
208
|
+
to: 'recipient@example.net',
|
|
209
|
+
subject: 'Tracked with a custom domain',
|
|
210
|
+
html: "<p>Hello <a href='https://example.com'>click here</a></p>",
|
|
211
|
+
tracking: {
|
|
212
|
+
click: {
|
|
213
|
+
enable: true,
|
|
214
|
+
customDomainName: 'newsletter-clicks'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
unsubscribe: {
|
|
218
|
+
customDomainName: 'newsletter-unsubscribe'
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
```
|
|
@@ -67,7 +67,7 @@ if (createPwdErr) { /*...*/ }
|
|
|
67
67
|
const { data: passwords, error: listPwdErr } = await mc.subAccounts.smtpPasswords.list('clienta')
|
|
68
68
|
if (listPwdErr) { /*...*/ }
|
|
69
69
|
|
|
70
|
-
const { error: deletePwdErr } = await mc.subAccounts.
|
|
70
|
+
const { error: deletePwdErr } = await mc.subAccounts.smtpPasswords.delete('clienta', storedPasswordId)
|
|
71
71
|
if (deletePwdErr) { /*...*/ }
|
|
72
72
|
```
|
|
73
73
|
|
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
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.
|
|
12
|
+
> Built and tested against Email API `1.4.0`
|
|
13
13
|
|
|
14
|
-
Node.js SDK to integrate [MailChannels Email API](https://docs.mailchannels.
|
|
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
|
|
|
16
16
|
<!-- #region overview -->
|
|
17
|
-
This library provides a simple way to interact with the [MailChannels Email API](https://docs.mailchannels.
|
|
17
|
+
This library provides a simple way to interact with the [MailChannels Email API](https://docs.mailchannels.com/email-api). It is written in TypeScript and can be used in both JavaScript and TypeScript projects and in different runtimes.
|
|
18
18
|
<!-- #endregion overview -->
|
|
19
19
|
|
|
20
20
|
- [✨ Release Notes](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-js/src/HEAD/CHANGELOG.md)
|
|
@@ -35,7 +35,7 @@ This library provides a simple way to interact with the [MailChannels Email API]
|
|
|
35
35
|
## <a name="features">🚀 Features</a>
|
|
36
36
|
|
|
37
37
|
<!-- #region features -->
|
|
38
|
-
This SDK fully supports all features and operations available in the [MailChannels Email API](https://docs.mailchannels.
|
|
38
|
+
This SDK fully supports all features and operations available in the [MailChannels Email API](https://docs.mailchannels.com/email-api). It is actively maintained to ensure compatibility and to quickly add support for new API features as they are released.
|
|
39
39
|
|
|
40
40
|
Some of the things you can do with the SDK:
|
|
41
41
|
|
|
@@ -169,6 +169,7 @@ const { data, error } = await mailchannels.emails.send({
|
|
|
169
169
|
- Email sends and async sends
|
|
170
170
|
- Domain checks
|
|
171
171
|
- DKIM key create, list, rotate, and update
|
|
172
|
+
- Custom tracking domain create, list, update, and delete
|
|
172
173
|
- Webhook enrollment, listing, validation, signing key lookup, and batch inspection
|
|
173
174
|
- Sub-account lifecycle, API keys, SMTP passwords, limits, and usage
|
|
174
175
|
- Engagement, performance, recipient behaviour, sender, volume, and usage metrics
|
|
@@ -179,6 +180,7 @@ const { data, error } = await mailchannels.emails.send({
|
|
|
179
180
|
- State is in-memory only and is reset when the process stops
|
|
180
181
|
- Any non-empty `X-API-Key` is accepted, with separate in-memory state per API key
|
|
181
182
|
- Webhook responses are simulated locally, but the simulator does not yet emit real webhook callbacks to your application
|
|
183
|
+
- Custom tracking domain verification is simulated, but the simulator does not yet check DNS records
|
|
182
184
|
|
|
183
185
|
The next planned expansion is outbound webhook delivery so client applications can test webhook ingestion flows against the simulator as well.
|
|
184
186
|
<!-- #endregion simulator -->
|
|
@@ -55,6 +55,7 @@ const createMetricsBuckets = (count) => [{
|
|
|
55
55
|
const createAccountState = (apiKey) => ({
|
|
56
56
|
apiKey,
|
|
57
57
|
customerHandle: createId("customer"),
|
|
58
|
+
customTrackingDomains: [],
|
|
58
59
|
dkimKeysByDomain: /* @__PURE__ */ new Map(),
|
|
59
60
|
messages: [],
|
|
60
61
|
subAccounts: /* @__PURE__ */ new Map(),
|
|
@@ -101,6 +102,16 @@ const createDkimKey = (domain, selector, overrides = {}) => {
|
|
|
101
102
|
};
|
|
102
103
|
};
|
|
103
104
|
const listDkimKeys = (account, domain) => account.dkimKeysByDomain.get(domain) || [];
|
|
105
|
+
const createCustomTrackingDomain = ({ hostname, name, scope, status = "active" }) => ({
|
|
106
|
+
created_at: currentTimestamp(),
|
|
107
|
+
hostname,
|
|
108
|
+
name,
|
|
109
|
+
scope,
|
|
110
|
+
status
|
|
111
|
+
});
|
|
112
|
+
const findCustomTrackingDomain = (account, hostname, scope) => {
|
|
113
|
+
return account.customTrackingDomains.find((domain) => domain.hostname === hostname && domain.scope === scope);
|
|
114
|
+
};
|
|
104
115
|
const recordWebhookBatch = (account, webhook, eventCount, status = "2xx_response", statusCode = 200) => {
|
|
105
116
|
account.webhookBatches.unshift({
|
|
106
117
|
batch_id: account.nextIds.batch++,
|
|
@@ -125,6 +136,7 @@ const collectMessages = (account, filters = {}) => {
|
|
|
125
136
|
};
|
|
126
137
|
const summarizeMessages = (messages) => ({
|
|
127
138
|
bounced: messages.reduce((total, message) => total + message.bounced, 0),
|
|
139
|
+
complained: messages.reduce((total, message) => total + message.complained, 0),
|
|
128
140
|
click: messages.reduce((total, message) => total + message.click, 0),
|
|
129
141
|
clickTrackingDelivered: messages.reduce((total, message) => total + message.clickTrackingDelivered, 0),
|
|
130
142
|
delivered: messages.reduce((total, message) => total + message.delivered, 0),
|
|
@@ -198,6 +210,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
198
210
|
account.messages.push({
|
|
199
211
|
bounced: 0,
|
|
200
212
|
campaignId: body?.campaign_id || "uncategorized",
|
|
213
|
+
complained: 0,
|
|
201
214
|
click: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
|
|
202
215
|
clickTrackingDelivered: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
|
|
203
216
|
delivered: 1,
|
|
@@ -240,11 +253,12 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
240
253
|
}
|
|
241
254
|
if (method === "POST" && url.pathname === "/tx/v1/check-domain") {
|
|
242
255
|
const domain = body?.domain || "example.com";
|
|
256
|
+
const dkimSettings = body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
|
|
257
|
+
dkim_domain: key.domain,
|
|
258
|
+
dkim_selector: key.selector
|
|
259
|
+
}));
|
|
243
260
|
sendJson(response, 200, { check_results: {
|
|
244
|
-
dkim:
|
|
245
|
-
dkim_domain: key.domain,
|
|
246
|
-
dkim_selector: key.selector
|
|
247
|
-
}))).map((setting) => ({
|
|
261
|
+
dkim: dkimSettings.map((setting) => ({
|
|
248
262
|
dkim_domain: setting.dkim_domain || domain,
|
|
249
263
|
dkim_key_status: setting.dkim_private_key ? "provided" : listDkimKeys(account, setting.dkim_domain || domain).find((key) => key.selector === setting.dkim_selector)?.status || "active",
|
|
250
264
|
dkim_selector: setting.dkim_selector || "default",
|
|
@@ -321,6 +335,71 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
321
335
|
sendNoContent(response);
|
|
322
336
|
return;
|
|
323
337
|
}
|
|
338
|
+
if (url.pathname === "/tx/v1/custom-tracking-domains") {
|
|
339
|
+
if (method === "POST") {
|
|
340
|
+
const { hostname, name, scope } = body || {};
|
|
341
|
+
if (!hostname || !name || !scope) {
|
|
342
|
+
sendJson(response, 400, { error: "Invalid request body." });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const hasDuplicateName = account.customTrackingDomains.some((domain) => domain.name === name);
|
|
346
|
+
const hasDuplicateHostnameAndScope = Boolean(findCustomTrackingDomain(account, hostname, scope));
|
|
347
|
+
if (hasDuplicateName || hasDuplicateHostnameAndScope) {
|
|
348
|
+
sendJson(response, 409, { error: "Custom tracking domain already exists." });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const domain = createCustomTrackingDomain({
|
|
352
|
+
hostname,
|
|
353
|
+
name,
|
|
354
|
+
scope
|
|
355
|
+
});
|
|
356
|
+
account.customTrackingDomains.push(domain);
|
|
357
|
+
sendJson(response, 201, clone(domain));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (method === "GET") {
|
|
361
|
+
const name = url.searchParams.get("name");
|
|
362
|
+
const status = url.searchParams.get("status");
|
|
363
|
+
const scope = url.searchParams.get("scope");
|
|
364
|
+
const offset = Number(url.searchParams.get("offset") || "0");
|
|
365
|
+
const limit = Number(url.searchParams.get("limit") || "100");
|
|
366
|
+
let domains = account.customTrackingDomains;
|
|
367
|
+
if (name) domains = domains.filter((domain) => domain.name === name);
|
|
368
|
+
if (status) domains = domains.filter((domain) => domain.status === status);
|
|
369
|
+
if (scope) domains = domains.filter((domain) => domain.scope === scope);
|
|
370
|
+
sendJson(response, 200, {
|
|
371
|
+
custom_tracking_domains: clone(domains.slice(offset, offset + limit)),
|
|
372
|
+
total: domains.length
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const customTrackingDomainMatch = url.pathname.match(/^\/tx\/v1\/custom-tracking-domains\/([^/]+)\/([^/]+)$/);
|
|
378
|
+
if (customTrackingDomainMatch) {
|
|
379
|
+
const [, hostname, scope] = customTrackingDomainMatch;
|
|
380
|
+
const targetDomain = findCustomTrackingDomain(account, hostname, scope);
|
|
381
|
+
if (!targetDomain) {
|
|
382
|
+
sendJson(response, 404, { error: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.` });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (method === "PATCH") {
|
|
386
|
+
if (body?.name) {
|
|
387
|
+
if (account.customTrackingDomains.some((domain) => domain !== targetDomain && domain.name === body.name)) {
|
|
388
|
+
sendJson(response, 409, { error: "Name already used by another domain." });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
targetDomain.name = body.name;
|
|
392
|
+
}
|
|
393
|
+
if (body?.status) targetDomain.status = body.status;
|
|
394
|
+
sendJson(response, 200, clone(targetDomain));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (method === "DELETE") {
|
|
398
|
+
account.customTrackingDomains = account.customTrackingDomains.filter((domain) => domain !== targetDomain);
|
|
399
|
+
sendNoContent(response);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
324
403
|
if (url.pathname === "/tx/v1/webhook" && method === "POST") {
|
|
325
404
|
const endpoint = url.searchParams.get("endpoint");
|
|
326
405
|
if (!endpoint) {
|
|
@@ -360,19 +439,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
360
439
|
sendJson(response, 404, { error: "No webhooks found for the account." });
|
|
361
440
|
return;
|
|
362
441
|
}
|
|
442
|
+
const results = Array.from(account.webhooks).map((webhook) => {
|
|
443
|
+
recordWebhookBatch(account, webhook, 1);
|
|
444
|
+
return {
|
|
445
|
+
result: "passed",
|
|
446
|
+
webhook,
|
|
447
|
+
response: {
|
|
448
|
+
body: "simulated validation ok",
|
|
449
|
+
status: 200
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
});
|
|
363
453
|
sendJson(response, 200, {
|
|
364
454
|
all_passed: true,
|
|
365
|
-
results
|
|
366
|
-
recordWebhookBatch(account, webhook, 1);
|
|
367
|
-
return {
|
|
368
|
-
result: "passed",
|
|
369
|
-
webhook,
|
|
370
|
-
response: {
|
|
371
|
-
body: "simulated validation ok",
|
|
372
|
-
status: 200
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
})
|
|
455
|
+
results
|
|
376
456
|
});
|
|
377
457
|
return;
|
|
378
458
|
}
|
|
@@ -383,7 +463,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
383
463
|
const webhook = url.searchParams.get("webhook");
|
|
384
464
|
const limit = Number(url.searchParams.get("limit") || "500");
|
|
385
465
|
const offset = Number(url.searchParams.get("offset") || "0");
|
|
386
|
-
|
|
466
|
+
const batches = account.webhookBatches.filter((batch) => {
|
|
387
467
|
if (createdAfter && batch.created_at < createdAfter) return false;
|
|
388
468
|
if (createdBefore && batch.created_at >= createdBefore) return false;
|
|
389
469
|
if (webhook && batch.webhook !== webhook) return false;
|
|
@@ -391,7 +471,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
391
471
|
if (!statuses.map((status) => `${status}_response`.replace("no_response_response", "no_response")).includes(batch.status)) return false;
|
|
392
472
|
}
|
|
393
473
|
return true;
|
|
394
|
-
})
|
|
474
|
+
});
|
|
475
|
+
sendJson(response, 200, { webhook_batches: batches.slice(offset, offset + limit) });
|
|
395
476
|
return;
|
|
396
477
|
}
|
|
397
478
|
const webhookBatchResendMatch = url.pathname.match(/^\/tx\/v1\/webhook-batch\/(\d+)\/resend$/);
|
|
@@ -528,7 +609,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
528
609
|
}
|
|
529
610
|
}
|
|
530
611
|
if (url.pathname === "/tx/v1/metrics/engagement" && method === "GET") {
|
|
531
|
-
const
|
|
612
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
613
|
+
const summary = summarizeMessages(messages);
|
|
532
614
|
sendJson(response, 200, {
|
|
533
615
|
buckets: {
|
|
534
616
|
click: createMetricsBuckets(summary.click),
|
|
@@ -546,11 +628,14 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
546
628
|
return;
|
|
547
629
|
}
|
|
548
630
|
if (url.pathname === "/tx/v1/metrics/performance" && method === "GET") {
|
|
549
|
-
const
|
|
631
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
632
|
+
const summary = summarizeMessages(messages);
|
|
550
633
|
sendJson(response, 200, {
|
|
551
634
|
bounced: summary.bounced,
|
|
635
|
+
complained: summary.complained,
|
|
552
636
|
buckets: {
|
|
553
637
|
bounced: createMetricsBuckets(summary.bounced),
|
|
638
|
+
complained: createMetricsBuckets(summary.complained),
|
|
554
639
|
delivered: createMetricsBuckets(summary.delivered),
|
|
555
640
|
processed: createMetricsBuckets(summary.processed)
|
|
556
641
|
},
|
|
@@ -562,7 +647,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
562
647
|
return;
|
|
563
648
|
}
|
|
564
649
|
if (url.pathname === "/tx/v1/metrics/recipient-behaviour" && method === "GET") {
|
|
565
|
-
const
|
|
650
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
651
|
+
const summary = summarizeMessages(messages);
|
|
566
652
|
sendJson(response, 200, {
|
|
567
653
|
buckets: {
|
|
568
654
|
unsubscribe_delivered: createMetricsBuckets(summary.unsubscribeDelivered),
|
|
@@ -576,7 +662,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
576
662
|
return;
|
|
577
663
|
}
|
|
578
664
|
if (url.pathname === "/tx/v1/metrics/volume" && method === "GET") {
|
|
579
|
-
const
|
|
665
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
666
|
+
const summary = summarizeMessages(messages);
|
|
580
667
|
sendJson(response, 200, {
|
|
581
668
|
buckets: {
|
|
582
669
|
delivered: createMetricsBuckets(summary.delivered),
|
|
@@ -678,7 +765,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
678
765
|
}
|
|
679
766
|
notFound(response);
|
|
680
767
|
} catch (error) {
|
|
681
|
-
|
|
768
|
+
const message = error instanceof Error ? error.message : "Simulator error";
|
|
769
|
+
sendText(response, 500, message);
|
|
682
770
|
}
|
|
683
771
|
};
|
|
684
772
|
return {
|
package/dist/mailchannels.d.mts
CHANGED
|
@@ -62,6 +62,11 @@ interface ErrorResponse {
|
|
|
62
62
|
* This field is intended for diagnostic use only and should not be relied upon.
|
|
63
63
|
*/
|
|
64
64
|
type: ErrorType;
|
|
65
|
+
/**
|
|
66
|
+
* An object containing the response, if available.
|
|
67
|
+
* This field may be `null` if no response is available or if the error is not related to an HTTP request or if the response is not a JSON object.
|
|
68
|
+
*/
|
|
69
|
+
response: Record<string, unknown> | null;
|
|
65
70
|
}
|
|
66
71
|
interface SuccessResponse {
|
|
67
72
|
/**
|
|
@@ -129,6 +134,12 @@ interface EmailsSendTracking {
|
|
|
129
134
|
* Track when a recipient clicks a link in your email.
|
|
130
135
|
*/
|
|
131
136
|
click?: {
|
|
137
|
+
/**
|
|
138
|
+
* The name of a configured active click tracking domain.
|
|
139
|
+
* When specified, click tracking links will use this domain instead of the default MailChannels domain.
|
|
140
|
+
* The domain must be registered in your account and have an active status.
|
|
141
|
+
*/
|
|
142
|
+
customDomainName?: string;
|
|
132
143
|
/**
|
|
133
144
|
* @default false
|
|
134
145
|
*/
|
|
@@ -138,6 +149,12 @@ interface EmailsSendTracking {
|
|
|
138
149
|
* Track when a recipient opens your email. Please note that some email clients may not support open tracking.
|
|
139
150
|
*/
|
|
140
151
|
open?: {
|
|
152
|
+
/**
|
|
153
|
+
* The name of a configured active open tracking domain.
|
|
154
|
+
* When specified, the open tracking pixel will use this domain instead of the default MailChannels domain.
|
|
155
|
+
* The domain must be registered in your account and have an active status.
|
|
156
|
+
*/
|
|
157
|
+
customDomainName?: string;
|
|
141
158
|
/**
|
|
142
159
|
* @default false
|
|
143
160
|
*/
|
|
@@ -360,10 +377,21 @@ interface EmailsSendOptionsBase {
|
|
|
360
377
|
/**
|
|
361
378
|
* Mark these messages as transactional or non-transactional. In order for a message to be marked as non-transactional, it must have exactly one recipient per personalization, and it must be DKIM signed. 400 Bad Request will be returned if there are more than one recipient in any personalization for non-transactional messages. If a message is marked as non-transactional, it changes the sending process as follows:
|
|
362
379
|
*
|
|
363
|
-
* List-Unsubscribe headers will be added.
|
|
380
|
+
* List-Unsubscribe and List-Unsubscribe-Post headers will be added, unless you supply your own List-Unsubscribe header, in which case yours is used and neither is added.
|
|
364
381
|
* @default true
|
|
365
382
|
*/
|
|
366
383
|
transactional?: boolean;
|
|
384
|
+
/**
|
|
385
|
+
* Settings to customize the unsubscribe experience for the message.
|
|
386
|
+
*/
|
|
387
|
+
unsubscribe?: {
|
|
388
|
+
/**
|
|
389
|
+
* The name of a configured active unsubscribe tracking domain.
|
|
390
|
+
* When specified, unsubscribe links will use this domain instead of the default MailChannels domain.
|
|
391
|
+
* The domain must be registered in your account and have an active status.
|
|
392
|
+
*/
|
|
393
|
+
customDomainName?: string;
|
|
394
|
+
};
|
|
367
395
|
}
|
|
368
396
|
type EmailsSendTargetOptions = {
|
|
369
397
|
personalizations: (Omit<EmailsSendPersonalization, "template"> & {
|
|
@@ -697,6 +725,171 @@ declare class DomainsDkim {
|
|
|
697
725
|
*/
|
|
698
726
|
rotate(domain: string, selector: string, options: DomainsDkimRotateOptions): Promise<DomainsDkimRotateResponse>;
|
|
699
727
|
}
|
|
728
|
+
type DomainsCustomTrackingScope = "click" | "open" | "unsubscribe";
|
|
729
|
+
interface DomainsCustomTrackingDomain {
|
|
730
|
+
/**
|
|
731
|
+
* The label for this custom tracking domain.
|
|
732
|
+
*/
|
|
733
|
+
name: string;
|
|
734
|
+
/**
|
|
735
|
+
* The registered domain hostname.
|
|
736
|
+
*/
|
|
737
|
+
hostname: string;
|
|
738
|
+
/**
|
|
739
|
+
* The event type this domain handles.
|
|
740
|
+
*/
|
|
741
|
+
scope: DomainsCustomTrackingScope;
|
|
742
|
+
/**
|
|
743
|
+
* Current status of the custom tracking domain.
|
|
744
|
+
*/
|
|
745
|
+
status: "active" | "disabled";
|
|
746
|
+
/**
|
|
747
|
+
* ISO 8601 timestamp when the domain was registered.
|
|
748
|
+
*/
|
|
749
|
+
createdAt: string;
|
|
750
|
+
}
|
|
751
|
+
interface DomainsCustomTrackingDnsSetupRequired {
|
|
752
|
+
/**
|
|
753
|
+
* UUID v4 nonce; also the TXT record value to set. Present only when TXT ownership verification is pending.
|
|
754
|
+
* @example "550e8400-e29b-41d4-a716-446655440000"
|
|
755
|
+
*/
|
|
756
|
+
token?: string;
|
|
757
|
+
/**
|
|
758
|
+
* Fully-qualified DNS TXT record name to add. Present only when TXT ownership verification is pending.
|
|
759
|
+
* @example "_mailchannels-verify.click.example.com"
|
|
760
|
+
*/
|
|
761
|
+
txtRecordName?: string;
|
|
762
|
+
/**
|
|
763
|
+
* Value for the DNS TXT record (same as token). Present only when TXT ownership verification is pending.
|
|
764
|
+
* @example "550e8400-e29b-41d4-a716-446655440000"
|
|
765
|
+
*/
|
|
766
|
+
txtRecordValue?: string;
|
|
767
|
+
/**
|
|
768
|
+
* Human-readable guidance for the DNS records that must be in place before retrying.
|
|
769
|
+
*/
|
|
770
|
+
instructions?: string;
|
|
771
|
+
}
|
|
772
|
+
type DomainsCustomTrackingWithDnsSetupRequired<T extends 202 | 201 | 200 | undefined = undefined> = T extends undefined ? DomainsCustomTrackingDomain & {
|
|
773
|
+
dnsSetupRequired: false;
|
|
774
|
+
} | DomainsCustomTrackingDnsSetupRequired & {
|
|
775
|
+
dnsSetupRequired: true;
|
|
776
|
+
} : T extends 202 ? DomainsCustomTrackingDnsSetupRequired & {
|
|
777
|
+
dnsSetupRequired: true;
|
|
778
|
+
} : DomainsCustomTrackingDomain & {
|
|
779
|
+
dnsSetupRequired: false;
|
|
780
|
+
};
|
|
781
|
+
type DomainsCustomTrackingCreateResponse = DataResponse<DomainsCustomTrackingWithDnsSetupRequired>;
|
|
782
|
+
interface DomainsCustomTrackingListOptions {
|
|
783
|
+
/**
|
|
784
|
+
* Filter by custom tracking domain label.
|
|
785
|
+
*/
|
|
786
|
+
name?: string;
|
|
787
|
+
/**
|
|
788
|
+
* Filter by status.
|
|
789
|
+
*/
|
|
790
|
+
status?: "active" | "disabled";
|
|
791
|
+
/**
|
|
792
|
+
* Filter by scope.
|
|
793
|
+
*/
|
|
794
|
+
scope?: DomainsCustomTrackingScope;
|
|
795
|
+
/**
|
|
796
|
+
* The maximum number of domains to return. Possible values are `1` to `1000`.
|
|
797
|
+
* @default 100
|
|
798
|
+
*/
|
|
799
|
+
limit?: number;
|
|
800
|
+
/**
|
|
801
|
+
* The number of domains to skip before returning results. The default is `0`.
|
|
802
|
+
* @default 0
|
|
803
|
+
*/
|
|
804
|
+
offset?: number;
|
|
805
|
+
}
|
|
806
|
+
type DomainsCustomTrackingListResponse = DataResponse<{
|
|
807
|
+
/**
|
|
808
|
+
* List of custom tracking domains matching the filter criteria.
|
|
809
|
+
*/
|
|
810
|
+
customTrackingDomains: DomainsCustomTrackingDomain[];
|
|
811
|
+
/**
|
|
812
|
+
* Total number of custom tracking domains.
|
|
813
|
+
*/
|
|
814
|
+
total: number;
|
|
815
|
+
}>;
|
|
816
|
+
interface DomainsCustomTrackingUpdateOptions {
|
|
817
|
+
/**
|
|
818
|
+
* New label for this custom tracking domain. Maximum length is `64` characters. Must match the pattern `^[a-z0-9-]+$`.
|
|
819
|
+
*/
|
|
820
|
+
name?: string;
|
|
821
|
+
/**
|
|
822
|
+
* New status. Re-activation requires DNS verification — add the TXT record and CNAME record described in the response body, then retry.
|
|
823
|
+
*/
|
|
824
|
+
status?: "active" | "disabled";
|
|
825
|
+
}
|
|
826
|
+
type DomainsCustomTrackingUpdateResponse = DataResponse<DomainsCustomTrackingWithDnsSetupRequired>;
|
|
827
|
+
declare class DomainsCustomTracking {
|
|
828
|
+
private mailchannels;
|
|
829
|
+
private static readonly SCOPE_VALUES;
|
|
830
|
+
constructor(mailchannels: MailChannelsClient);
|
|
831
|
+
/**
|
|
832
|
+
* Retrieve all custom tracking domains registered under your account.
|
|
833
|
+
* Optional filters include domain name, status, scope, limit and offset.
|
|
834
|
+
* @param options - Optional filter options.
|
|
835
|
+
* @example
|
|
836
|
+
* ```ts
|
|
837
|
+
* const mailchannels = new MailChannels('your-api-key')
|
|
838
|
+
* const { data, error } = await mailchannels.domains.customTracking.list({
|
|
839
|
+
* status: 'active'
|
|
840
|
+
* })
|
|
841
|
+
* ```
|
|
842
|
+
*/
|
|
843
|
+
list(options?: DomainsCustomTrackingListOptions): Promise<DomainsCustomTrackingListResponse>;
|
|
844
|
+
/**
|
|
845
|
+
* Register a custom branded domain for click tracking, open tracking, or unsubscribe handling. By default, MailChannels uses shared domains for these links. Using a custom domain improves brand consistency by replacing shared domains with your own (e.g., `click.example.com`). Once registered, select the domain at send time using its `name`.
|
|
846
|
+
*
|
|
847
|
+
* Before registration completes, two DNS records must be in place:
|
|
848
|
+
* 1. A TXT record at `_mailchannels-verify.<hostname>` containing the verification token (returned when DNS setup is required).
|
|
849
|
+
* 2. A CNAME record at `<hostname>` pointing to `links.mailchannels.net`.
|
|
850
|
+
* @param name - A unique label used to select this domain at message send time. Maximum length is `64` characters. Must match the pattern `^[a-z0-9-]+$`.
|
|
851
|
+
* @param hostname - The hostname to register as a custom tracking domain (e.g., `click.example.com`).
|
|
852
|
+
* @param scope - The event type this domain handles.
|
|
853
|
+
* @example
|
|
854
|
+
* ```ts
|
|
855
|
+
* const mailchannels = new MailChannels('your-api-key')
|
|
856
|
+
* const { data, error } = await mailchannels.domains.customTracking.create(
|
|
857
|
+
* 'clickdemo',
|
|
858
|
+
* 'click.example.com',
|
|
859
|
+
* 'click'
|
|
860
|
+
* )
|
|
861
|
+
* ```
|
|
862
|
+
*/
|
|
863
|
+
create(name: string, hostname: string, scope: DomainsCustomTrackingScope): Promise<DomainsCustomTrackingCreateResponse>;
|
|
864
|
+
/**
|
|
865
|
+
* Update an existing custom tracking domain by its hostname and scope. Supports updating the custom tracking domain's name or toggling its active status.
|
|
866
|
+
* @param hostname - The hostname of the custom tracking domain to update.
|
|
867
|
+
* @param scope - The scope of the custom tracking domain to update.
|
|
868
|
+
* @param options - The options for updating the custom tracking domain.
|
|
869
|
+
* @example
|
|
870
|
+
* ```ts
|
|
871
|
+
* const mailchannels = new MailChannels('your-api-key')
|
|
872
|
+
* const { data, error } = await mailchannels.domains.customTracking.update('click.example.com', 'click', {
|
|
873
|
+
* name: 'newclickname',
|
|
874
|
+
* status: 'active'
|
|
875
|
+
* })
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
update(hostname: string, scope: DomainsCustomTrackingScope, options: DomainsCustomTrackingUpdateOptions): Promise<DomainsCustomTrackingUpdateResponse>;
|
|
879
|
+
/**
|
|
880
|
+
* Permanently delete an existing custom tracking domain for the given hostname and scope. The domain can be re-registered if needed.
|
|
881
|
+
*
|
|
882
|
+
* WARNING: Any tracking links or unsubscribe URLs in previously sent emails using this domain will stop working immediately.
|
|
883
|
+
* @param hostname - The hostname of the custom tracking domain to delete.
|
|
884
|
+
* @param scope - The scope of the custom tracking domain to delete.
|
|
885
|
+
* @example
|
|
886
|
+
* ```ts
|
|
887
|
+
* const mailchannels = new MailChannels('your-api-key')
|
|
888
|
+
* const { success, error } = await mailchannels.domains.customTracking.delete('click.example.com', 'click')
|
|
889
|
+
* ```
|
|
890
|
+
*/
|
|
891
|
+
delete(hostname: string, scope: DomainsCustomTrackingScope): Promise<SuccessResponse>;
|
|
892
|
+
}
|
|
700
893
|
interface DomainsCheck {
|
|
701
894
|
/**
|
|
702
895
|
* Domain used for DKIM signing.
|
|
@@ -792,6 +985,7 @@ type DomainsCheckResponse = DataResponse<{
|
|
|
792
985
|
declare class Domains {
|
|
793
986
|
protected mailchannels: MailChannelsClient;
|
|
794
987
|
readonly dkim: DomainsDkim;
|
|
988
|
+
readonly customTracking: DomainsCustomTracking;
|
|
795
989
|
constructor(mailchannels: MailChannelsClient);
|
|
796
990
|
/**
|
|
797
991
|
* Validates a domain's email authentication setup by retrieving its DKIM, SPF, and Domain Lockdown status. This endpoint checks whether the domain is properly configured for secure email delivery.
|
|
@@ -1259,25 +1453,74 @@ interface MetricsEngagement {
|
|
|
1259
1453
|
clickTrackingDelivered: MetricsBucket[];
|
|
1260
1454
|
open: MetricsBucket[];
|
|
1261
1455
|
openTrackingDelivered: MetricsBucket[];
|
|
1456
|
+
uniqueClick?: MetricsBucket[];
|
|
1457
|
+
uniqueClickTrackingDelivered?: MetricsBucket[];
|
|
1458
|
+
uniqueOpen?: MetricsBucket[];
|
|
1459
|
+
uniqueOpenTrackingDelivered?: MetricsBucket[];
|
|
1262
1460
|
};
|
|
1461
|
+
/**
|
|
1462
|
+
* Count of click events by recipients.
|
|
1463
|
+
*/
|
|
1263
1464
|
click: number;
|
|
1465
|
+
/**
|
|
1466
|
+
* Count of recipients of delivered messages with HTML content that contains tracked click URLs, where click tracking is enabled in the send request.
|
|
1467
|
+
*/
|
|
1264
1468
|
clickTrackingDelivered: number;
|
|
1469
|
+
/**
|
|
1470
|
+
* The end of the time range for retrieving message engagement metrics (exclusive).
|
|
1471
|
+
*/
|
|
1265
1472
|
endTime: string;
|
|
1473
|
+
/**
|
|
1474
|
+
* Count of open events by recipients.
|
|
1475
|
+
*/
|
|
1266
1476
|
open: number;
|
|
1477
|
+
/**
|
|
1478
|
+
* Count of recipients of delivered messages with HTML content where open tracking was enabled in the send request.
|
|
1479
|
+
*/
|
|
1267
1480
|
openTrackingDelivered: number;
|
|
1481
|
+
/**
|
|
1482
|
+
* The beginning of the time range for retrieving message engagement metrics (inclusive).
|
|
1483
|
+
*/
|
|
1268
1484
|
startTime: string;
|
|
1485
|
+
/**
|
|
1486
|
+
* Count of distinct messages that had at least one click event.
|
|
1487
|
+
* Unlike `click`, each message is counted at most once regardless of how many links were clicked or how many times.
|
|
1488
|
+
* Use this to compute click rates without exceeding 100%.
|
|
1489
|
+
*/
|
|
1490
|
+
uniqueClick?: number;
|
|
1491
|
+
/**
|
|
1492
|
+
* Count of distinct messages delivered with click tracking enabled (message-level, not recipient-level).
|
|
1493
|
+
* Use as the denominator when computing unique click rates.
|
|
1494
|
+
*/
|
|
1495
|
+
uniqueClickTrackingDelivered?: number;
|
|
1496
|
+
/**
|
|
1497
|
+
* Count of distinct messages that had at least one open event.
|
|
1498
|
+
* Unlike `open`, each message is counted at most once regardless of how many times its tracking pixel was fired.
|
|
1499
|
+
* Use this to compute open rates without exceeding 100%.
|
|
1500
|
+
*/
|
|
1501
|
+
uniqueOpen?: number;
|
|
1502
|
+
/**
|
|
1503
|
+
* Count of distinct messages delivered with open tracking enabled (message-level, not recipient-level).
|
|
1504
|
+
* Use as the denominator when computing unique open rates.
|
|
1505
|
+
*/
|
|
1506
|
+
uniqueOpenTrackingDelivered?: number;
|
|
1269
1507
|
}
|
|
1270
1508
|
type MetricsEngagementResponse = DataResponse<MetricsEngagement>;
|
|
1271
1509
|
interface MetricsPerformance {
|
|
1272
1510
|
/**
|
|
1273
|
-
* Count of messages bounced during the specified time range.
|
|
1511
|
+
* Count of messages hard-bounced during the specified time range.
|
|
1274
1512
|
*/
|
|
1275
1513
|
bounced: number;
|
|
1514
|
+
/**
|
|
1515
|
+
* Count of messages complained during the specified time range.
|
|
1516
|
+
*/
|
|
1517
|
+
complained: number;
|
|
1276
1518
|
/**
|
|
1277
1519
|
* A series of metrics aggregations bucketed by time interval (e.g. hour, day).
|
|
1278
1520
|
*/
|
|
1279
1521
|
buckets: {
|
|
1280
1522
|
bounced: MetricsBucket[];
|
|
1523
|
+
complained: MetricsBucket[];
|
|
1281
1524
|
delivered: MetricsBucket[];
|
|
1282
1525
|
processed: MetricsBucket[];
|
|
1283
1526
|
};
|
|
@@ -1727,7 +1970,7 @@ declare class Metrics {
|
|
|
1727
1970
|
*/
|
|
1728
1971
|
engagement(options?: MetricsOptions): Promise<MetricsEngagementResponse>;
|
|
1729
1972
|
/**
|
|
1730
|
-
* Retrieve performance metrics for messages sent from your account, including counts of processed, delivered, hard-bounced events. Supports optional filters for time range, and campaign ID.
|
|
1973
|
+
* Retrieve performance metrics for messages sent from your account, including counts of processed, delivered, hard-bounced, and complained events. Supports optional filters for time range, and campaign ID.
|
|
1731
1974
|
* @param options - Options to filter and customize the performance metrics retrieval.
|
|
1732
1975
|
* @example
|
|
1733
1976
|
* ```ts
|
|
@@ -1827,4 +2070,4 @@ declare class MailChannels extends MailChannelsClient {
|
|
|
1827
2070
|
readonly suppressions: Suppressions;
|
|
1828
2071
|
constructor(key: string, options?: MailChannelsClientOptions);
|
|
1829
2072
|
}
|
|
1830
|
-
export { Attachment, type DataResponse, Domains, type DomainsCheckOptions, type DomainsCheckResponse, type DomainsCheckVerdict, type DomainsDkimCreateOptions, type DomainsDkimCreateResponse, type DomainsDkimKey, type DomainsDkimKeyStatus, type DomainsDkimListOptions, type DomainsDkimListResponse, type DomainsDkimRotateOptions, type DomainsDkimRotateResponse, type DomainsDkimUpdateStatusOptions, Emails, type EmailsQueueResponse, type EmailsSendAsyncResponse, type EmailsSendAttachment, type EmailsSendContent, type EmailsSendDkim, type EmailsSendOptions, type EmailsSendPersonalization, type EmailsSendRecipient, type EmailsSendRecipientInput, type EmailsSendResponse, type EmailsSendTemplate, type EmailsSendTemplateType, type EmailsSendTemplateValue, type EmailsSendTracking, type ErrorResponse, type ErrorType, MailChannels, MailChannelsClient, type MailChannelsClientOptions, Metrics, type MetricsBucket, type MetricsEngagement, type MetricsEngagementResponse, type MetricsOptions, type MetricsPerformance, type MetricsPerformanceResponse, type MetricsRecipientBehaviour, type MetricsRecipientBehaviourResponse, type MetricsSenders, type MetricsSendersOptions, type MetricsSendersResponse, type MetricsSendersType, type MetricsUsageResponse, type MetricsVolume, type MetricsVolumeResponse, type SubAccount, SubAccounts, type SubAccountsAccount, type SubAccountsApiKey, type SubAccountsApiKeysCreateResponse, type SubAccountsApiKeysListOptions, type SubAccountsApiKeysListResponse, type SubAccountsCreateApiKeyResponse, type SubAccountsCreateResponse, type SubAccountsCreateSmtpPasswordResponse, type SubAccountsLimit, type SubAccountsLimitResponse, type SubAccountsLimitsGetResponse, type SubAccountsLimitsSetOptions, type SubAccountsListApiKeyOptions, type SubAccountsListApiKeyResponse, type SubAccountsListOptions, type SubAccountsListResponse, type SubAccountsListSmtpPasswordResponse, type SubAccountsSmtpPassword, type SubAccountsSmtpPasswordsCreateResponse, type SubAccountsSmtpPasswordsListResponse, type SubAccountsUsage, type SubAccountsUsageResponse, type SuccessResponse, Suppressions, type SuppressionsCreateOptions, type SuppressionsListEntry, type SuppressionsListOptions, type SuppressionsListResponse, type SuppressionsSource, type SuppressionsTypes, type WebhookEvent, type WebhookEventClick, type WebhookEventComplained, type WebhookEventDelivered, type WebhookEventDropped, type WebhookEventHardBounced, type WebhookEventOpen, type WebhookEventProcessed, type WebhookEventSoftBounced, type WebhookEventTest, type WebhookEventType, type WebhookEventUnsubscribed, Webhooks, type WebhooksBatch, type WebhooksBatchResponseStatus, type WebhooksBatchStatus, type WebhooksBatchesOptions, type WebhooksBatchesResponse, type WebhooksListResponse, type WebhooksResendBatch, type WebhooksResendBatchResponse, type WebhooksSigningKeyResponse, type WebhooksValidateResponse, type WebhooksVerifyOptions, type WebhooksVerifyResponse };
|
|
2073
|
+
export { Attachment, type DataResponse, Domains, type DomainsCheckOptions, type DomainsCheckResponse, type DomainsCheckVerdict, type DomainsCustomTrackingCreateResponse, type DomainsCustomTrackingDnsSetupRequired, type DomainsCustomTrackingDomain, type DomainsCustomTrackingListOptions, type DomainsCustomTrackingListResponse, type DomainsCustomTrackingScope, type DomainsCustomTrackingUpdateOptions, type DomainsCustomTrackingUpdateResponse, type DomainsCustomTrackingWithDnsSetupRequired, type DomainsDkimCreateOptions, type DomainsDkimCreateResponse, type DomainsDkimKey, type DomainsDkimKeyStatus, type DomainsDkimListOptions, type DomainsDkimListResponse, type DomainsDkimRotateOptions, type DomainsDkimRotateResponse, type DomainsDkimUpdateStatusOptions, Emails, type EmailsQueueResponse, type EmailsSendAsyncResponse, type EmailsSendAttachment, type EmailsSendContent, type EmailsSendDkim, type EmailsSendOptions, type EmailsSendPersonalization, type EmailsSendRecipient, type EmailsSendRecipientInput, type EmailsSendResponse, type EmailsSendTemplate, type EmailsSendTemplateType, type EmailsSendTemplateValue, type EmailsSendTracking, type ErrorResponse, type ErrorType, MailChannels, MailChannelsClient, type MailChannelsClientOptions, Metrics, type MetricsBucket, type MetricsEngagement, type MetricsEngagementResponse, type MetricsOptions, type MetricsPerformance, type MetricsPerformanceResponse, type MetricsRecipientBehaviour, type MetricsRecipientBehaviourResponse, type MetricsSenders, type MetricsSendersOptions, type MetricsSendersResponse, type MetricsSendersType, type MetricsUsageResponse, type MetricsVolume, type MetricsVolumeResponse, type SubAccount, SubAccounts, type SubAccountsAccount, type SubAccountsApiKey, type SubAccountsApiKeysCreateResponse, type SubAccountsApiKeysListOptions, type SubAccountsApiKeysListResponse, type SubAccountsCreateApiKeyResponse, type SubAccountsCreateResponse, type SubAccountsCreateSmtpPasswordResponse, type SubAccountsLimit, type SubAccountsLimitResponse, type SubAccountsLimitsGetResponse, type SubAccountsLimitsSetOptions, type SubAccountsListApiKeyOptions, type SubAccountsListApiKeyResponse, type SubAccountsListOptions, type SubAccountsListResponse, type SubAccountsListSmtpPasswordResponse, type SubAccountsSmtpPassword, type SubAccountsSmtpPasswordsCreateResponse, type SubAccountsSmtpPasswordsListResponse, type SubAccountsUsage, type SubAccountsUsageResponse, type SuccessResponse, Suppressions, type SuppressionsCreateOptions, type SuppressionsListEntry, type SuppressionsListOptions, type SuppressionsListResponse, type SuppressionsSource, type SuppressionsTypes, type WebhookEvent, type WebhookEventClick, type WebhookEventComplained, type WebhookEventDelivered, type WebhookEventDropped, type WebhookEventHardBounced, type WebhookEventOpen, type WebhookEventProcessed, type WebhookEventSoftBounced, type WebhookEventTest, type WebhookEventType, type WebhookEventUnsubscribed, Webhooks, type WebhooksBatch, type WebhooksBatchResponseStatus, type WebhooksBatchStatus, type WebhooksBatchesOptions, type WebhooksBatchesResponse, type WebhooksListResponse, type WebhooksResendBatch, type WebhooksResendBatchResponse, type WebhooksSigningKeyResponse, type WebhooksValidateResponse, type WebhooksVerifyOptions, type WebhooksVerifyResponse };
|
package/dist/mailchannels.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { $fetch } from "ofetch";
|
|
|
2
2
|
import { subtle } from "node:crypto";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import mime from "mime";
|
|
5
|
-
var version = "1.
|
|
5
|
+
var version = "1.2.0";
|
|
6
6
|
var MailChannelsClient = class MailChannelsClient {
|
|
7
7
|
static DEFAULT_BASE_URL = "https://api.mailchannels.net";
|
|
8
8
|
static DEFAULT_TIMEOUT = 12e4;
|
|
@@ -79,21 +79,26 @@ const STATUS_ERROR_TYPE_MAP = {
|
|
|
79
79
|
[429]: "rate_limit_error",
|
|
80
80
|
[500]: "internal_server_error"
|
|
81
81
|
};
|
|
82
|
-
const createError = (message, statusCode = null, type) => {
|
|
82
|
+
const createError = (message, statusCode = null, type, response = null) => {
|
|
83
83
|
return {
|
|
84
84
|
message,
|
|
85
85
|
statusCode,
|
|
86
|
-
type
|
|
86
|
+
type,
|
|
87
|
+
response
|
|
87
88
|
};
|
|
88
89
|
};
|
|
89
90
|
const getStatusError = (response, errors = {}) => {
|
|
90
91
|
const statusText = errors[response.status] || "Unknown error.";
|
|
91
92
|
const payload = response._data ?? response.data;
|
|
92
93
|
let details;
|
|
94
|
+
let errorResponse = null;
|
|
93
95
|
if (typeof payload === "string") details = payload;
|
|
94
|
-
else if (payload
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
else if (typeof payload === "object" && payload !== null) {
|
|
97
|
+
if (typeof payload.message === "string") details = payload.message;
|
|
98
|
+
else if (Array.isArray(payload.errors) && payload.errors.length) details = payload.errors.join(", ");
|
|
99
|
+
errorResponse = payload;
|
|
100
|
+
}
|
|
101
|
+
return createError(details ? `${statusText} ${details}` : statusText, response.status ?? null, STATUS_ERROR_TYPE_MAP[response.status] || "api_error", errorResponse);
|
|
97
102
|
};
|
|
98
103
|
const getResultError = (e, fallback) => {
|
|
99
104
|
return createError(e instanceof Error ? e.message : fallback, null, "application_error");
|
|
@@ -107,6 +112,12 @@ const validatePagination = (pagination = {}) => {
|
|
|
107
112
|
if (typeof offset === "number" && offset < 0) return createValidationError("Offset must be greater than or equal to 0.");
|
|
108
113
|
return null;
|
|
109
114
|
};
|
|
115
|
+
const CUSTOM_TRACKING_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
116
|
+
const validateCustomTrackingName = (name) => {
|
|
117
|
+
if (!name || name.length < 1 || name.length > 64) return createValidationError("The custom tracking domain name must be between 1 and 64 characters.");
|
|
118
|
+
if (!CUSTOM_TRACKING_NAME_PATTERN.test(name)) return createValidationError("The custom tracking domain name must match ^[a-z0-9-]+$");
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
110
121
|
const clean = (data) => {
|
|
111
122
|
if (Array.isArray(data)) {
|
|
112
123
|
const result = [];
|
|
@@ -305,6 +316,18 @@ const buildSendPayload = async (options) => {
|
|
|
305
316
|
return getRecipientCount(personalization.to) + getRecipientCount(personalization.cc) + getRecipientCount(personalization.bcc) !== 1;
|
|
306
317
|
})) return "Non-transactional messages must have exactly one recipient per personalization.";
|
|
307
318
|
}
|
|
319
|
+
if (options.tracking?.click?.customDomainName !== void 0) {
|
|
320
|
+
const clickCustomTrackingNameError = validateCustomTrackingName(options.tracking.click.customDomainName);
|
|
321
|
+
if (clickCustomTrackingNameError) return `Invalid click tracking: ${clickCustomTrackingNameError.message}`;
|
|
322
|
+
}
|
|
323
|
+
if (options.tracking?.open?.customDomainName !== void 0) {
|
|
324
|
+
const openCustomTrackingNameError = validateCustomTrackingName(options.tracking.open.customDomainName);
|
|
325
|
+
if (openCustomTrackingNameError) return `Invalid open tracking: ${openCustomTrackingNameError.message}`;
|
|
326
|
+
}
|
|
327
|
+
if (options.unsubscribe?.customDomainName !== void 0) {
|
|
328
|
+
const unsubscribeCustomTrackingNameError = validateCustomTrackingName(options.unsubscribe.customDomainName);
|
|
329
|
+
if (unsubscribeCustomTrackingNameError) return `Invalid unsubscribe settings: ${unsubscribeCustomTrackingNameError.message}`;
|
|
330
|
+
}
|
|
308
331
|
const content = [];
|
|
309
332
|
const template_type = options.template?.type;
|
|
310
333
|
if (text) content.push({
|
|
@@ -335,11 +358,18 @@ const buildSendPayload = async (options) => {
|
|
|
335
358
|
from: parsedFrom,
|
|
336
359
|
subject: options.subject,
|
|
337
360
|
content,
|
|
338
|
-
tracking_settings: options.tracking
|
|
339
|
-
click_tracking: options.tracking.click
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
tracking_settings: options.tracking && {
|
|
362
|
+
click_tracking: options.tracking.click && {
|
|
363
|
+
custom_domain_name: options.tracking.click.customDomainName,
|
|
364
|
+
enable: options.tracking.click.enable
|
|
365
|
+
},
|
|
366
|
+
open_tracking: options.tracking.open && {
|
|
367
|
+
custom_domain_name: options.tracking.open.customDomainName,
|
|
368
|
+
enable: options.tracking.open.enable
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
transactional: options.transactional,
|
|
372
|
+
unsubscribe_settings: options.unsubscribe && { custom_domain_name: options.unsubscribe.customDomainName }
|
|
343
373
|
};
|
|
344
374
|
};
|
|
345
375
|
var Emails = class {
|
|
@@ -603,12 +633,245 @@ var DomainsDkim = class {
|
|
|
603
633
|
};
|
|
604
634
|
}
|
|
605
635
|
};
|
|
636
|
+
var DomainsCustomTracking = class DomainsCustomTracking {
|
|
637
|
+
mailchannels;
|
|
638
|
+
static SCOPE_VALUES = /* @__PURE__ */ new Set([
|
|
639
|
+
"click",
|
|
640
|
+
"open",
|
|
641
|
+
"unsubscribe"
|
|
642
|
+
]);
|
|
643
|
+
constructor(mailchannels) {
|
|
644
|
+
this.mailchannels = mailchannels;
|
|
645
|
+
}
|
|
646
|
+
async list(options) {
|
|
647
|
+
let error = null;
|
|
648
|
+
error = validatePagination({
|
|
649
|
+
...options,
|
|
650
|
+
max: 1e3
|
|
651
|
+
});
|
|
652
|
+
if (error) return {
|
|
653
|
+
data: null,
|
|
654
|
+
error
|
|
655
|
+
};
|
|
656
|
+
const response = await this.mailchannels.get("/tx/v1/custom-tracking-domains", {
|
|
657
|
+
query: options,
|
|
658
|
+
onResponseError: async ({ response }) => {
|
|
659
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
660
|
+
}
|
|
661
|
+
}).catch((e) => {
|
|
662
|
+
error ||= getResultError(e, "Failed to fetch custom tracking domains.");
|
|
663
|
+
return null;
|
|
664
|
+
});
|
|
665
|
+
if (!response) return {
|
|
666
|
+
data: null,
|
|
667
|
+
error
|
|
668
|
+
};
|
|
669
|
+
return {
|
|
670
|
+
data: clean({
|
|
671
|
+
customTrackingDomains: response.custom_tracking_domains.map((d) => ({
|
|
672
|
+
name: d.name,
|
|
673
|
+
hostname: d.hostname,
|
|
674
|
+
scope: d.scope,
|
|
675
|
+
status: d.status,
|
|
676
|
+
createdAt: d.created_at
|
|
677
|
+
})),
|
|
678
|
+
total: response.total
|
|
679
|
+
}),
|
|
680
|
+
error: null
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async create(name, hostname, scope) {
|
|
684
|
+
let error = null;
|
|
685
|
+
error = validateCustomTrackingName(name);
|
|
686
|
+
if (error) return {
|
|
687
|
+
data: null,
|
|
688
|
+
error
|
|
689
|
+
};
|
|
690
|
+
if (!hostname) {
|
|
691
|
+
error = createValidationError("Hostname is required.");
|
|
692
|
+
return {
|
|
693
|
+
data: null,
|
|
694
|
+
error
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
698
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
699
|
+
return {
|
|
700
|
+
data: null,
|
|
701
|
+
error
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const payload = {
|
|
705
|
+
name,
|
|
706
|
+
hostname,
|
|
707
|
+
scope
|
|
708
|
+
};
|
|
709
|
+
let statusCode = null;
|
|
710
|
+
const response = await this.mailchannels.post("/tx/v1/custom-tracking-domains", {
|
|
711
|
+
body: payload,
|
|
712
|
+
onResponse: async ({ response }) => {
|
|
713
|
+
statusCode = response.status;
|
|
714
|
+
},
|
|
715
|
+
onResponseError: async ({ response }) => {
|
|
716
|
+
error = getStatusError(response, {
|
|
717
|
+
[400]: "Invalid request body.",
|
|
718
|
+
[403]: "No permission to register this domain.",
|
|
719
|
+
[409]: "A domain with the same name already exists, or the hostname and scope combination is already registered.",
|
|
720
|
+
[422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}).catch((e) => {
|
|
724
|
+
error ||= getResultError(e, "Failed to create custom tracking domain.");
|
|
725
|
+
return null;
|
|
726
|
+
});
|
|
727
|
+
if (!response) return {
|
|
728
|
+
data: null,
|
|
729
|
+
error
|
|
730
|
+
};
|
|
731
|
+
if (statusCode === 202) {
|
|
732
|
+
const dnsSetupRequiredResponse = response;
|
|
733
|
+
return {
|
|
734
|
+
data: clean({
|
|
735
|
+
dnsSetupRequired: true,
|
|
736
|
+
token: dnsSetupRequiredResponse.token,
|
|
737
|
+
txtRecordName: dnsSetupRequiredResponse.txt_record_name,
|
|
738
|
+
txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
|
|
739
|
+
instructions: dnsSetupRequiredResponse.instructions
|
|
740
|
+
}),
|
|
741
|
+
error: null
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const createResponse = response;
|
|
745
|
+
return {
|
|
746
|
+
data: clean({
|
|
747
|
+
dnsSetupRequired: false,
|
|
748
|
+
name: createResponse.name,
|
|
749
|
+
hostname: createResponse.hostname,
|
|
750
|
+
scope: createResponse.scope,
|
|
751
|
+
status: createResponse.status,
|
|
752
|
+
createdAt: createResponse.created_at
|
|
753
|
+
}),
|
|
754
|
+
error: null
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async update(hostname, scope, options) {
|
|
758
|
+
let error = null;
|
|
759
|
+
if (!hostname) {
|
|
760
|
+
error = createValidationError("Hostname is required.");
|
|
761
|
+
return {
|
|
762
|
+
data: null,
|
|
763
|
+
error
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
767
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
768
|
+
return {
|
|
769
|
+
data: null,
|
|
770
|
+
error
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (options.name !== void 0) {
|
|
774
|
+
error = validateCustomTrackingName(options.name);
|
|
775
|
+
if (error) return {
|
|
776
|
+
data: null,
|
|
777
|
+
error
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (options.name === void 0 && options.status === void 0) {
|
|
781
|
+
error = createValidationError("At least one of 'name' or 'status' must be provided.");
|
|
782
|
+
return {
|
|
783
|
+
data: null,
|
|
784
|
+
error
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const payload = {
|
|
788
|
+
name: options.name,
|
|
789
|
+
status: options.status
|
|
790
|
+
};
|
|
791
|
+
let statusCode = null;
|
|
792
|
+
const response = await this.mailchannels.patch(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, {
|
|
793
|
+
body: payload,
|
|
794
|
+
onResponse: async ({ response }) => {
|
|
795
|
+
statusCode = response.status;
|
|
796
|
+
},
|
|
797
|
+
onResponseError: async ({ response }) => {
|
|
798
|
+
error = getStatusError(response, {
|
|
799
|
+
[400]: "Bad Request.",
|
|
800
|
+
[403]: "No permission to update this domain.",
|
|
801
|
+
[404]: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.`,
|
|
802
|
+
[409]: "Name already used by another domain.",
|
|
803
|
+
[422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}).catch((e) => {
|
|
807
|
+
error ||= getResultError(e, "Failed to update custom tracking domain.");
|
|
808
|
+
return null;
|
|
809
|
+
});
|
|
810
|
+
if (!response) return {
|
|
811
|
+
data: null,
|
|
812
|
+
error
|
|
813
|
+
};
|
|
814
|
+
if (statusCode === 202) {
|
|
815
|
+
const dnsSetupRequiredResponse = response;
|
|
816
|
+
return {
|
|
817
|
+
data: clean({
|
|
818
|
+
dnsSetupRequired: true,
|
|
819
|
+
token: dnsSetupRequiredResponse.token,
|
|
820
|
+
txtRecordName: dnsSetupRequiredResponse.txt_record_name,
|
|
821
|
+
txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
|
|
822
|
+
instructions: dnsSetupRequiredResponse.instructions
|
|
823
|
+
}),
|
|
824
|
+
error: null
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const updateResponse = response;
|
|
828
|
+
return {
|
|
829
|
+
data: clean({
|
|
830
|
+
dnsSetupRequired: false,
|
|
831
|
+
name: updateResponse.name,
|
|
832
|
+
hostname: updateResponse.hostname,
|
|
833
|
+
scope: updateResponse.scope,
|
|
834
|
+
status: updateResponse.status,
|
|
835
|
+
createdAt: updateResponse.created_at
|
|
836
|
+
}),
|
|
837
|
+
error: null
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async delete(hostname, scope) {
|
|
841
|
+
let error = null;
|
|
842
|
+
if (!hostname) {
|
|
843
|
+
error = createValidationError("Hostname is required.");
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
error
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
850
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
await this.mailchannels.delete(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, { onResponseError: async ({ response }) => {
|
|
857
|
+
error = getStatusError(response, { [400]: "Invalid hostname or scope value" });
|
|
858
|
+
} }).catch((e) => {
|
|
859
|
+
error ||= getResultError(e, "Failed to delete custom tracking domain.");
|
|
860
|
+
});
|
|
861
|
+
return {
|
|
862
|
+
success: !error,
|
|
863
|
+
error
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
};
|
|
606
867
|
var Domains = class {
|
|
607
868
|
mailchannels;
|
|
608
869
|
dkim;
|
|
870
|
+
customTracking;
|
|
609
871
|
constructor(mailchannels) {
|
|
610
872
|
this.mailchannels = mailchannels;
|
|
611
873
|
this.dkim = new DomainsDkim(mailchannels);
|
|
874
|
+
this.customTracking = new DomainsCustomTracking(mailchannels);
|
|
612
875
|
}
|
|
613
876
|
async check(domain, options) {
|
|
614
877
|
let error = null;
|
|
@@ -1558,14 +1821,22 @@ var Metrics = class {
|
|
|
1558
1821
|
click: response.buckets.click.map(mapBucket),
|
|
1559
1822
|
clickTrackingDelivered: response.buckets.click_tracking_delivered.map(mapBucket),
|
|
1560
1823
|
open: response.buckets.open.map(mapBucket),
|
|
1561
|
-
openTrackingDelivered: response.buckets.open_tracking_delivered.map(mapBucket)
|
|
1824
|
+
openTrackingDelivered: response.buckets.open_tracking_delivered.map(mapBucket),
|
|
1825
|
+
uniqueClick: response.buckets.unique_click?.map(mapBucket),
|
|
1826
|
+
uniqueClickTrackingDelivered: response.buckets.unique_click_tracking_delivered?.map(mapBucket),
|
|
1827
|
+
uniqueOpen: response.buckets.unique_open?.map(mapBucket),
|
|
1828
|
+
uniqueOpenTrackingDelivered: response.buckets.unique_open_tracking_delivered?.map(mapBucket)
|
|
1562
1829
|
},
|
|
1563
1830
|
click: response.click,
|
|
1564
1831
|
clickTrackingDelivered: response.click_tracking_delivered,
|
|
1565
1832
|
endTime: response.end_time,
|
|
1566
1833
|
open: response.open,
|
|
1567
1834
|
openTrackingDelivered: response.open_tracking_delivered,
|
|
1568
|
-
startTime: response.start_time
|
|
1835
|
+
startTime: response.start_time,
|
|
1836
|
+
uniqueClick: response.unique_click,
|
|
1837
|
+
uniqueClickTrackingDelivered: response.unique_click_tracking_delivered,
|
|
1838
|
+
uniqueOpen: response.unique_open,
|
|
1839
|
+
uniqueOpenTrackingDelivered: response.unique_open_tracking_delivered
|
|
1569
1840
|
}),
|
|
1570
1841
|
error: null
|
|
1571
1842
|
};
|
|
@@ -1601,8 +1872,10 @@ var Metrics = class {
|
|
|
1601
1872
|
return {
|
|
1602
1873
|
data: clean({
|
|
1603
1874
|
bounced: response.bounced,
|
|
1875
|
+
complained: response.complained,
|
|
1604
1876
|
buckets: {
|
|
1605
1877
|
bounced: response.buckets.bounced.map(mapBucket),
|
|
1878
|
+
complained: response.buckets.complained.map(mapBucket),
|
|
1606
1879
|
delivered: response.buckets.delivered.map(mapBucket),
|
|
1607
1880
|
processed: response.buckets.processed.map(mapBucket)
|
|
1608
1881
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailchannels-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Node.js SDK to integrate MailChannels Email API into your JavaScript or TypeScript server-side applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,17 +43,17 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
45
45
|
"@types/markdown-it": "^14.1.2",
|
|
46
|
-
"@types/node": "^
|
|
47
|
-
"@vitest/coverage-v8": "^4.1.
|
|
46
|
+
"@types/node": "^26.1.1",
|
|
47
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
48
48
|
"changelogen": "^0.6.2",
|
|
49
|
-
"obuild": "^0.4.
|
|
50
|
-
"oxlint": "^1.
|
|
49
|
+
"obuild": "^0.4.37",
|
|
50
|
+
"oxlint": "^1.73.0",
|
|
51
51
|
"scule": "^1.3.0",
|
|
52
52
|
"typescript": "^6.0.3",
|
|
53
|
-
"vitepress": "^2.0.0-alpha.
|
|
53
|
+
"vitepress": "^2.0.0-alpha.18",
|
|
54
54
|
"vitepress-plugin-group-icons": "^1.7.5",
|
|
55
|
-
"vitepress-plugin-llms": "^1.13.
|
|
56
|
-
"vitest": "^4.1.
|
|
55
|
+
"vitepress-plugin-llms": "^1.13.2",
|
|
56
|
+
"vitest": "^4.1.10"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=20"
|