mailchannels-sdk 1.0.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/SKILL.md +91 -0
- package/.agents/skills/mailchannels-js/resources/attachments.md +113 -0
- package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +98 -0
- package/.agents/skills/mailchannels-js/resources/custom-headers.md +64 -0
- package/.agents/skills/mailchannels-js/resources/custom-tracking-domains.md +83 -0
- package/.agents/skills/mailchannels-js/resources/dkim.md +148 -0
- package/.agents/skills/mailchannels-js/resources/domain-checks.md +100 -0
- package/.agents/skills/mailchannels-js/resources/error-handling.md +143 -0
- package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +114 -0
- package/.agents/skills/mailchannels-js/resources/overview.md +91 -0
- package/.agents/skills/mailchannels-js/resources/sending.md +221 -0
- package/.agents/skills/mailchannels-js/resources/sub-accounts.md +129 -0
- package/.agents/skills/mailchannels-js/resources/suppressions.md +91 -0
- package/.agents/skills/mailchannels-js/resources/templates.md +97 -0
- package/.agents/skills/mailchannels-js/resources/testing.md +95 -0
- package/.agents/skills/mailchannels-js/resources/unsubscribe.md +73 -0
- package/.agents/skills/mailchannels-js/resources/webhooks.md +174 -0
- package/README.md +70 -4
- package/dist/_chunks/simulator.mjs +111 -23
- package/dist/mailchannels.d.mts +853 -561
- package/dist/mailchannels.mjs +861 -457
- package/package.json +10 -9
|
@@ -55,13 +55,14 @@ 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(),
|
|
61
62
|
suppressionEntries: [],
|
|
62
63
|
webhookBatches: [],
|
|
63
64
|
webhooks: /* @__PURE__ */ new Set(),
|
|
64
|
-
webhookSigningKeys: new Map([[SIMULATOR_SIGNING_KEY_ID, "SIMULATOR_PUBLIC_SIGNING_KEY"]]),
|
|
65
|
+
webhookSigningKeys: /* @__PURE__ */ new Map([[SIMULATOR_SIGNING_KEY_ID, "SIMULATOR_PUBLIC_SIGNING_KEY"]]),
|
|
65
66
|
nextIds: {
|
|
66
67
|
apiKey: 1,
|
|
67
68
|
batch: 1,
|
|
@@ -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 {
|