mailchannels-sdk 1.1.0 → 1.3.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/.agents/skills/mailchannels-js/resources/suppressions.md +7 -7
- package/README.md +6 -4
- package/dist/_chunks/simulator.mjs +127 -24
- package/dist/mailchannels.d.mts +289 -23
- package/dist/mailchannels.mjs +294 -18
- 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
|
|
|
@@ -10,8 +10,8 @@ import { MailChannels } from 'mailchannels-sdk'
|
|
|
10
10
|
|
|
11
11
|
const mc = new MailChannels('YOUR-API-KEY')
|
|
12
12
|
|
|
13
|
-
const { success, error } = await mc.suppressions.create(
|
|
14
|
-
|
|
13
|
+
const { success, error } = await mc.suppressions.create(
|
|
14
|
+
[
|
|
15
15
|
{
|
|
16
16
|
recipient: 'out@example.net',
|
|
17
17
|
types: ['non-transactional'], // optional; defaults to non-transactional
|
|
@@ -22,8 +22,8 @@ const { success, error } = await mc.suppressions.create({
|
|
|
22
22
|
types: ['transactional', 'non-transactional']
|
|
23
23
|
}
|
|
24
24
|
],
|
|
25
|
-
addToSubAccounts: true // parent only; copies entries to every sub-account
|
|
26
|
-
|
|
25
|
+
{ addToSubAccounts: true } // parent only; copies entries to every sub-account
|
|
26
|
+
)
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Constraints:
|
|
@@ -83,9 +83,9 @@ that recipient regardless of origin.
|
|
|
83
83
|
|
|
84
84
|
### Patterns
|
|
85
85
|
|
|
86
|
-
- **Preference center opt-out**: set `
|
|
87
|
-
emails, `transactional` for order updates, and so on.
|
|
86
|
+
- **Preference center opt-out**: set `types` according to the email category, e.g. `non-transactional` for marketing
|
|
87
|
+
emails, `transactional` for order updates, and so on.
|
|
88
88
|
Configure `addToSubAccounts` depending on whether the preference applies to all sub-accounts or just the parent account.
|
|
89
89
|
Add a note to indicate the source, e.g. "Opted out via preference center".
|
|
90
|
-
- **Migrating from another ESP**: bulk-create with `addToSubAccounts: true` so every tenant
|
|
90
|
+
- **Migrating from another ESP**: bulk-create with option `addToSubAccounts: true` so every tenant
|
|
91
91
|
inherits the list.
|
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.5.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,8 @@ const createMetricsBuckets = (count) => [{
|
|
|
55
55
|
const createAccountState = (apiKey) => ({
|
|
56
56
|
apiKey,
|
|
57
57
|
customerHandle: createId("customer"),
|
|
58
|
+
limit: { sends: 1e5 },
|
|
59
|
+
customTrackingDomains: [],
|
|
58
60
|
dkimKeysByDomain: /* @__PURE__ */ new Map(),
|
|
59
61
|
messages: [],
|
|
60
62
|
subAccounts: /* @__PURE__ */ new Map(),
|
|
@@ -101,6 +103,16 @@ const createDkimKey = (domain, selector, overrides = {}) => {
|
|
|
101
103
|
};
|
|
102
104
|
};
|
|
103
105
|
const listDkimKeys = (account, domain) => account.dkimKeysByDomain.get(domain) || [];
|
|
106
|
+
const createCustomTrackingDomain = ({ hostname, name, scope, status = "active" }) => ({
|
|
107
|
+
created_at: currentTimestamp(),
|
|
108
|
+
hostname,
|
|
109
|
+
name,
|
|
110
|
+
scope,
|
|
111
|
+
status
|
|
112
|
+
});
|
|
113
|
+
const findCustomTrackingDomain = (account, hostname, scope) => {
|
|
114
|
+
return account.customTrackingDomains.find((domain) => domain.hostname === hostname && domain.scope === scope);
|
|
115
|
+
};
|
|
104
116
|
const recordWebhookBatch = (account, webhook, eventCount, status = "2xx_response", statusCode = 200) => {
|
|
105
117
|
account.webhookBatches.unshift({
|
|
106
118
|
batch_id: account.nextIds.batch++,
|
|
@@ -125,6 +137,7 @@ const collectMessages = (account, filters = {}) => {
|
|
|
125
137
|
};
|
|
126
138
|
const summarizeMessages = (messages) => ({
|
|
127
139
|
bounced: messages.reduce((total, message) => total + message.bounced, 0),
|
|
140
|
+
complained: messages.reduce((total, message) => total + message.complained, 0),
|
|
128
141
|
click: messages.reduce((total, message) => total + message.click, 0),
|
|
129
142
|
clickTrackingDelivered: messages.reduce((total, message) => total + message.clickTrackingDelivered, 0),
|
|
130
143
|
delivered: messages.reduce((total, message) => total + message.delivered, 0),
|
|
@@ -198,6 +211,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
198
211
|
account.messages.push({
|
|
199
212
|
bounced: 0,
|
|
200
213
|
campaignId: body?.campaign_id || "uncategorized",
|
|
214
|
+
complained: 0,
|
|
201
215
|
click: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
|
|
202
216
|
clickTrackingDelivered: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
|
|
203
217
|
delivered: 1,
|
|
@@ -240,11 +254,12 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
240
254
|
}
|
|
241
255
|
if (method === "POST" && url.pathname === "/tx/v1/check-domain") {
|
|
242
256
|
const domain = body?.domain || "example.com";
|
|
257
|
+
const dkimSettings = body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
|
|
258
|
+
dkim_domain: key.domain,
|
|
259
|
+
dkim_selector: key.selector
|
|
260
|
+
}));
|
|
243
261
|
sendJson(response, 200, { check_results: {
|
|
244
|
-
dkim:
|
|
245
|
-
dkim_domain: key.domain,
|
|
246
|
-
dkim_selector: key.selector
|
|
247
|
-
}))).map((setting) => ({
|
|
262
|
+
dkim: dkimSettings.map((setting) => ({
|
|
248
263
|
dkim_domain: setting.dkim_domain || domain,
|
|
249
264
|
dkim_key_status: setting.dkim_private_key ? "provided" : listDkimKeys(account, setting.dkim_domain || domain).find((key) => key.selector === setting.dkim_selector)?.status || "active",
|
|
250
265
|
dkim_selector: setting.dkim_selector || "default",
|
|
@@ -321,6 +336,71 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
321
336
|
sendNoContent(response);
|
|
322
337
|
return;
|
|
323
338
|
}
|
|
339
|
+
if (url.pathname === "/tx/v1/custom-tracking-domains") {
|
|
340
|
+
if (method === "POST") {
|
|
341
|
+
const { hostname, name, scope } = body || {};
|
|
342
|
+
if (!hostname || !name || !scope) {
|
|
343
|
+
sendJson(response, 400, { error: "Invalid request body." });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const hasDuplicateName = account.customTrackingDomains.some((domain) => domain.name === name);
|
|
347
|
+
const hasDuplicateHostnameAndScope = Boolean(findCustomTrackingDomain(account, hostname, scope));
|
|
348
|
+
if (hasDuplicateName || hasDuplicateHostnameAndScope) {
|
|
349
|
+
sendJson(response, 409, { error: "Custom tracking domain already exists." });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const domain = createCustomTrackingDomain({
|
|
353
|
+
hostname,
|
|
354
|
+
name,
|
|
355
|
+
scope
|
|
356
|
+
});
|
|
357
|
+
account.customTrackingDomains.push(domain);
|
|
358
|
+
sendJson(response, 201, clone(domain));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (method === "GET") {
|
|
362
|
+
const name = url.searchParams.get("name");
|
|
363
|
+
const status = url.searchParams.get("status");
|
|
364
|
+
const scope = url.searchParams.get("scope");
|
|
365
|
+
const offset = Number(url.searchParams.get("offset") || "0");
|
|
366
|
+
const limit = Number(url.searchParams.get("limit") || "100");
|
|
367
|
+
let domains = account.customTrackingDomains;
|
|
368
|
+
if (name) domains = domains.filter((domain) => domain.name === name);
|
|
369
|
+
if (status) domains = domains.filter((domain) => domain.status === status);
|
|
370
|
+
if (scope) domains = domains.filter((domain) => domain.scope === scope);
|
|
371
|
+
sendJson(response, 200, {
|
|
372
|
+
custom_tracking_domains: clone(domains.slice(offset, offset + limit)),
|
|
373
|
+
total: domains.length
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const customTrackingDomainMatch = url.pathname.match(/^\/tx\/v1\/custom-tracking-domains\/([^/]+)\/([^/]+)$/);
|
|
379
|
+
if (customTrackingDomainMatch) {
|
|
380
|
+
const [, hostname, scope] = customTrackingDomainMatch;
|
|
381
|
+
const targetDomain = findCustomTrackingDomain(account, hostname, scope);
|
|
382
|
+
if (!targetDomain) {
|
|
383
|
+
sendJson(response, 404, { error: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.` });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (method === "PATCH") {
|
|
387
|
+
if (body?.name) {
|
|
388
|
+
if (account.customTrackingDomains.some((domain) => domain !== targetDomain && domain.name === body.name)) {
|
|
389
|
+
sendJson(response, 409, { error: "Name already used by another domain." });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
targetDomain.name = body.name;
|
|
393
|
+
}
|
|
394
|
+
if (body?.status) targetDomain.status = body.status;
|
|
395
|
+
sendJson(response, 200, clone(targetDomain));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (method === "DELETE") {
|
|
399
|
+
account.customTrackingDomains = account.customTrackingDomains.filter((domain) => domain !== targetDomain);
|
|
400
|
+
sendNoContent(response);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
324
404
|
if (url.pathname === "/tx/v1/webhook" && method === "POST") {
|
|
325
405
|
const endpoint = url.searchParams.get("endpoint");
|
|
326
406
|
if (!endpoint) {
|
|
@@ -360,19 +440,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
360
440
|
sendJson(response, 404, { error: "No webhooks found for the account." });
|
|
361
441
|
return;
|
|
362
442
|
}
|
|
443
|
+
const results = Array.from(account.webhooks).map((webhook) => {
|
|
444
|
+
recordWebhookBatch(account, webhook, 1);
|
|
445
|
+
return {
|
|
446
|
+
result: "passed",
|
|
447
|
+
webhook,
|
|
448
|
+
response: {
|
|
449
|
+
body: "simulated validation ok",
|
|
450
|
+
status: 200
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
});
|
|
363
454
|
sendJson(response, 200, {
|
|
364
455
|
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
|
-
})
|
|
456
|
+
results
|
|
376
457
|
});
|
|
377
458
|
return;
|
|
378
459
|
}
|
|
@@ -383,7 +464,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
383
464
|
const webhook = url.searchParams.get("webhook");
|
|
384
465
|
const limit = Number(url.searchParams.get("limit") || "500");
|
|
385
466
|
const offset = Number(url.searchParams.get("offset") || "0");
|
|
386
|
-
|
|
467
|
+
const batches = account.webhookBatches.filter((batch) => {
|
|
387
468
|
if (createdAfter && batch.created_at < createdAfter) return false;
|
|
388
469
|
if (createdBefore && batch.created_at >= createdBefore) return false;
|
|
389
470
|
if (webhook && batch.webhook !== webhook) return false;
|
|
@@ -391,7 +472,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
391
472
|
if (!statuses.map((status) => `${status}_response`.replace("no_response_response", "no_response")).includes(batch.status)) return false;
|
|
392
473
|
}
|
|
393
474
|
return true;
|
|
394
|
-
})
|
|
475
|
+
});
|
|
476
|
+
sendJson(response, 200, { webhook_batches: batches.slice(offset, offset + limit) });
|
|
395
477
|
return;
|
|
396
478
|
}
|
|
397
479
|
const webhookBatchResendMatch = url.pathname.match(/^\/tx\/v1\/webhook-batch\/(\d+)\/resend$/);
|
|
@@ -519,16 +601,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
519
601
|
return;
|
|
520
602
|
}
|
|
521
603
|
if (suffix === "usage" && method === "GET") {
|
|
604
|
+
const subLimitSends = subAccount.limit?.sends ?? -1;
|
|
605
|
+
const monthlyLimit = subLimitSends === -1 ? account.limit.sends : subLimitSends;
|
|
522
606
|
sendJson(response, 200, {
|
|
523
607
|
period_end_date: currentTimestamp(),
|
|
524
608
|
period_start_date: new Date((/* @__PURE__ */ new Date()).setDate(1)).toISOString(),
|
|
525
|
-
total_usage: subAccount.usage
|
|
609
|
+
total_usage: subAccount.usage,
|
|
610
|
+
monthly_limit: monthlyLimit
|
|
526
611
|
});
|
|
527
612
|
return;
|
|
528
613
|
}
|
|
529
614
|
}
|
|
530
615
|
if (url.pathname === "/tx/v1/metrics/engagement" && method === "GET") {
|
|
531
|
-
const
|
|
616
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
617
|
+
const summary = summarizeMessages(messages);
|
|
532
618
|
sendJson(response, 200, {
|
|
533
619
|
buckets: {
|
|
534
620
|
click: createMetricsBuckets(summary.click),
|
|
@@ -546,11 +632,14 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
546
632
|
return;
|
|
547
633
|
}
|
|
548
634
|
if (url.pathname === "/tx/v1/metrics/performance" && method === "GET") {
|
|
549
|
-
const
|
|
635
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
636
|
+
const summary = summarizeMessages(messages);
|
|
550
637
|
sendJson(response, 200, {
|
|
551
638
|
bounced: summary.bounced,
|
|
639
|
+
complained: summary.complained,
|
|
552
640
|
buckets: {
|
|
553
641
|
bounced: createMetricsBuckets(summary.bounced),
|
|
642
|
+
complained: createMetricsBuckets(summary.complained),
|
|
554
643
|
delivered: createMetricsBuckets(summary.delivered),
|
|
555
644
|
processed: createMetricsBuckets(summary.processed)
|
|
556
645
|
},
|
|
@@ -562,7 +651,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
562
651
|
return;
|
|
563
652
|
}
|
|
564
653
|
if (url.pathname === "/tx/v1/metrics/recipient-behaviour" && method === "GET") {
|
|
565
|
-
const
|
|
654
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
655
|
+
const summary = summarizeMessages(messages);
|
|
566
656
|
sendJson(response, 200, {
|
|
567
657
|
buckets: {
|
|
568
658
|
unsubscribe_delivered: createMetricsBuckets(summary.unsubscribeDelivered),
|
|
@@ -576,7 +666,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
576
666
|
return;
|
|
577
667
|
}
|
|
578
668
|
if (url.pathname === "/tx/v1/metrics/volume" && method === "GET") {
|
|
579
|
-
const
|
|
669
|
+
const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
|
|
670
|
+
const summary = summarizeMessages(messages);
|
|
580
671
|
sendJson(response, 200, {
|
|
581
672
|
buckets: {
|
|
582
673
|
delivered: createMetricsBuckets(summary.delivered),
|
|
@@ -633,10 +724,21 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
633
724
|
return;
|
|
634
725
|
}
|
|
635
726
|
if (url.pathname === "/tx/v1/usage" && method === "GET") {
|
|
727
|
+
let totalUsage = account.messages.length;
|
|
728
|
+
let monthlyLimit = account.limit.sends;
|
|
729
|
+
if (scopeHandle) {
|
|
730
|
+
const subAccount = account.subAccounts.get(scopeHandle);
|
|
731
|
+
if (subAccount) {
|
|
732
|
+
totalUsage = subAccount.usage;
|
|
733
|
+
const subLimitSends = subAccount.limit?.sends ?? -1;
|
|
734
|
+
monthlyLimit = subLimitSends === -1 ? account.limit.sends : subLimitSends;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
636
737
|
sendJson(response, 200, {
|
|
637
738
|
period_end_date: currentTimestamp(),
|
|
638
739
|
period_start_date: new Date((/* @__PURE__ */ new Date()).setDate(1)).toISOString(),
|
|
639
|
-
total_usage:
|
|
740
|
+
total_usage: totalUsage,
|
|
741
|
+
monthly_limit: monthlyLimit
|
|
640
742
|
});
|
|
641
743
|
return;
|
|
642
744
|
}
|
|
@@ -678,7 +780,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
|
|
|
678
780
|
}
|
|
679
781
|
notFound(response);
|
|
680
782
|
} catch (error) {
|
|
681
|
-
|
|
783
|
+
const message = error instanceof Error ? error.message : "Simulator error";
|
|
784
|
+
sendText(response, 500, message);
|
|
682
785
|
}
|
|
683
786
|
};
|
|
684
787
|
return {
|