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
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 = [];
|
|
@@ -160,7 +171,7 @@ const parseArrayRecipients = (recipients) => {
|
|
|
160
171
|
return filtered.length > 0 ? filtered : void 0;
|
|
161
172
|
};
|
|
162
173
|
const stripPemHeaders = (pem) => pem.replace(/-----[^-]+-----|\s|#.*$/gm, "");
|
|
163
|
-
const RESERVED_HEADER_NAMES = new Set([
|
|
174
|
+
const RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
164
175
|
"authentication-results",
|
|
165
176
|
"bcc",
|
|
166
177
|
"cc",
|
|
@@ -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 {
|
|
@@ -424,80 +454,6 @@ const mapDkimKey = (key) => ({
|
|
|
424
454
|
status: key.status,
|
|
425
455
|
statusModifiedAt: key.status_modified_at
|
|
426
456
|
});
|
|
427
|
-
var Domains = class {
|
|
428
|
-
mailchannels;
|
|
429
|
-
dkim;
|
|
430
|
-
constructor(mailchannels) {
|
|
431
|
-
this.mailchannels = mailchannels;
|
|
432
|
-
this.dkim = new DomainsDkim(mailchannels);
|
|
433
|
-
}
|
|
434
|
-
async check(domain, options) {
|
|
435
|
-
let error = null;
|
|
436
|
-
if (!domain) {
|
|
437
|
-
error = createValidationError("No domain provided.");
|
|
438
|
-
return {
|
|
439
|
-
data: null,
|
|
440
|
-
error
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
const dkimOptions = options?.dkim ? Array.isArray(options.dkim) ? options.dkim : [options.dkim] : void 0;
|
|
444
|
-
if (dkimOptions && dkimOptions.length > 10) {
|
|
445
|
-
error = createValidationError("A maximum of 10 DKIM settings can be provided.");
|
|
446
|
-
return {
|
|
447
|
-
data: null,
|
|
448
|
-
error
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
if (dkimOptions?.find(({ privateKey, selector }) => privateKey && !selector)) {
|
|
452
|
-
error = createValidationError("DKIM settings with a privateKey must also include a selector.");
|
|
453
|
-
return {
|
|
454
|
-
data: null,
|
|
455
|
-
error
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
const payload = {
|
|
459
|
-
dkim_settings: dkimOptions?.map((dkim) => ({
|
|
460
|
-
dkim_domain: dkim.domain,
|
|
461
|
-
dkim_private_key: dkim.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
|
|
462
|
-
dkim_selector: dkim.selector
|
|
463
|
-
})),
|
|
464
|
-
domain,
|
|
465
|
-
sender_id: options?.senderId
|
|
466
|
-
};
|
|
467
|
-
const response = await this.mailchannels.post("/tx/v1/check-domain", {
|
|
468
|
-
body: payload,
|
|
469
|
-
onResponseError: async ({ response }) => {
|
|
470
|
-
error = getStatusError(response, {
|
|
471
|
-
[400]: "Bad Request.",
|
|
472
|
-
[403]: "User does not have access to this feature."
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
}).catch((e) => {
|
|
476
|
-
error ||= getResultError(e, "Failed to check domain.");
|
|
477
|
-
return null;
|
|
478
|
-
});
|
|
479
|
-
if (!response) return {
|
|
480
|
-
data: null,
|
|
481
|
-
error
|
|
482
|
-
};
|
|
483
|
-
return {
|
|
484
|
-
data: clean({
|
|
485
|
-
dkim: response.check_results.dkim.map((dkimResults) => ({
|
|
486
|
-
domain: dkimResults.dkim_domain,
|
|
487
|
-
keyStatus: dkimResults.dkim_key_status,
|
|
488
|
-
selector: dkimResults.dkim_selector,
|
|
489
|
-
reason: dkimResults.reason,
|
|
490
|
-
verdict: dkimResults.verdict
|
|
491
|
-
})),
|
|
492
|
-
domainLockdown: response.check_results.domain_lockdown,
|
|
493
|
-
senderDomain: response.check_results.sender_domain,
|
|
494
|
-
spf: response.check_results.spf,
|
|
495
|
-
references: response.references
|
|
496
|
-
}),
|
|
497
|
-
error: null
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
457
|
var DomainsDkim = class {
|
|
502
458
|
mailchannels;
|
|
503
459
|
constructor(mailchannels) {
|
|
@@ -524,7 +480,7 @@ var DomainsDkim = class {
|
|
|
524
480
|
key_length: options.length,
|
|
525
481
|
selector: options.selector
|
|
526
482
|
};
|
|
527
|
-
const response = await this.mailchannels.post(`/tx/v1/domains/${domain}/dkim-keys`, {
|
|
483
|
+
const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
|
|
528
484
|
body: payload,
|
|
529
485
|
onResponseError: async ({ response }) => {
|
|
530
486
|
error = getStatusError(response, {
|
|
@@ -576,7 +532,7 @@ var DomainsDkim = class {
|
|
|
576
532
|
limit: options?.limit,
|
|
577
533
|
include_dns_record: options?.includeDnsRecord
|
|
578
534
|
};
|
|
579
|
-
const response = await this.mailchannels.get(`/tx/v1/domains/${domain}/dkim-keys`, {
|
|
535
|
+
const response = await this.mailchannels.get(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
|
|
580
536
|
query: payload,
|
|
581
537
|
onResponseError: async ({ response }) => {
|
|
582
538
|
error = getStatusError(response, { [400]: "Bad Request." });
|
|
@@ -611,7 +567,7 @@ var DomainsDkim = class {
|
|
|
611
567
|
};
|
|
612
568
|
}
|
|
613
569
|
const payload = { status: options.status };
|
|
614
|
-
await this.mailchannels.patch(`/tx/v1/domains/${domain}/dkim-keys/${options.selector}`, {
|
|
570
|
+
await this.mailchannels.patch(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(options.selector)}`, {
|
|
615
571
|
body: payload,
|
|
616
572
|
onResponseError: async ({ response }) => {
|
|
617
573
|
error = getStatusError(response, {
|
|
@@ -651,7 +607,7 @@ var DomainsDkim = class {
|
|
|
651
607
|
};
|
|
652
608
|
}
|
|
653
609
|
const payload = { new_key: { selector: options.newKey.selector } };
|
|
654
|
-
const response = await this.mailchannels.post(`/tx/v1/domains/${domain}/dkim-keys/${selector}/rotate`, {
|
|
610
|
+
const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(selector)}/rotate`, {
|
|
655
611
|
body: payload,
|
|
656
612
|
onResponseError: async ({ response }) => {
|
|
657
613
|
error = getStatusError(response, {
|
|
@@ -677,118 +633,33 @@ var DomainsDkim = class {
|
|
|
677
633
|
};
|
|
678
634
|
}
|
|
679
635
|
};
|
|
680
|
-
|
|
681
|
-
name: "HMAC",
|
|
682
|
-
hash: "SHA-256"
|
|
683
|
-
};
|
|
684
|
-
const ED25519 = {
|
|
685
|
-
name: "Ed25519",
|
|
686
|
-
namedCurve: "Ed25519"
|
|
687
|
-
};
|
|
688
|
-
const encoder = new TextEncoder();
|
|
689
|
-
const HEADER_CONTENT_DIGEST = "content-digest";
|
|
690
|
-
const HEADER_SIGNATURE = "signature";
|
|
691
|
-
const HEADER_SIGNATURE_INPUT = "signature-input";
|
|
692
|
-
const signingKeyCache = /* @__PURE__ */ new Map();
|
|
693
|
-
const validateContentDigest = async (header, body) => {
|
|
694
|
-
const match = header.match(/^(.*?)=:(.*?):$/);
|
|
695
|
-
if (!match) return false;
|
|
696
|
-
const [, algorithm, hash] = match;
|
|
697
|
-
if (!algorithm || !hash) return false;
|
|
698
|
-
const normalizedAlgorithm = algorithm.replace("-", "").toLowerCase();
|
|
699
|
-
if (!["sha256"].includes(normalizedAlgorithm)) return false;
|
|
700
|
-
const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, encoder.encode(body));
|
|
701
|
-
return Buffer.from(signatureBuffer).toString("base64") === hash;
|
|
702
|
-
};
|
|
703
|
-
const extractSignature = (signatureHeader) => {
|
|
704
|
-
const signatureMatch = signatureHeader.match(/sig_\d+=:([^:]+):/);
|
|
705
|
-
return signatureMatch && signatureMatch[1] ? signatureMatch[1] : null;
|
|
706
|
-
};
|
|
707
|
-
const extractInputValues = (header) => {
|
|
708
|
-
const match = header.match(/^(\w+)=\(([^)]+)\);created=(\d+);alg="([^"]+)";keyid="([^"]+)"$/);
|
|
709
|
-
if (!match) return null;
|
|
710
|
-
return {
|
|
711
|
-
name: match[1],
|
|
712
|
-
timestamp: Number.parseInt(match[3], 10),
|
|
713
|
-
algorithm: match[4],
|
|
714
|
-
keyId: match[5]
|
|
715
|
-
};
|
|
716
|
-
};
|
|
717
|
-
async function isValidWebhook(options) {
|
|
718
|
-
const { payload, headers, cache = true } = options;
|
|
719
|
-
const contentDigest = headers[HEADER_CONTENT_DIGEST];
|
|
720
|
-
const messageSignature = headers[HEADER_SIGNATURE];
|
|
721
|
-
const signatureInput = headers[HEADER_SIGNATURE_INPUT];
|
|
722
|
-
if (!payload || !contentDigest || !messageSignature || !signatureInput || !await validateContentDigest(contentDigest, payload)) return false;
|
|
723
|
-
const signature = extractSignature(messageSignature);
|
|
724
|
-
if (!signature) return false;
|
|
725
|
-
const values = extractInputValues(signatureInput);
|
|
726
|
-
if (!values) return false;
|
|
727
|
-
if (Math.floor(Date.now() / 1e3) - values.timestamp > 300) return false;
|
|
728
|
-
const signingString = `"content-digest": ${contentDigest}
|
|
729
|
-
"@signature-params": ("content-digest");created=${values.timestamp};alg="${values.algorithm}";keyid="${values.keyId}"`;
|
|
730
|
-
let publicKey = options.publicKey;
|
|
731
|
-
if (!publicKey && values.keyId) {
|
|
732
|
-
if (cache) publicKey = signingKeyCache.get(values.keyId);
|
|
733
|
-
if (!publicKey) {
|
|
734
|
-
const publicKeyResponse = await $fetch("/tx/v1/webhook/public-key", {
|
|
735
|
-
baseURL: "https://api.mailchannels.net",
|
|
736
|
-
query: { id: values.keyId }
|
|
737
|
-
}).catch(() => null);
|
|
738
|
-
if (!publicKeyResponse) return false;
|
|
739
|
-
publicKey = publicKeyResponse.key;
|
|
740
|
-
if (cache && publicKey) signingKeyCache.set(values.keyId, publicKey);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (!publicKey) return false;
|
|
744
|
-
publicKey = stripPemHeaders(publicKey);
|
|
745
|
-
const encoding = "base64";
|
|
746
|
-
const format = "spki";
|
|
747
|
-
const publicKeyBuffer = Buffer.from(publicKey, encoding);
|
|
748
|
-
const webhookSignatureBuffer = Buffer.from(signature, encoding);
|
|
749
|
-
const key = await subtle.importKey(format, publicKeyBuffer, ED25519, false, ["verify"]);
|
|
750
|
-
return subtle.verify(ED25519.name, key, webhookSignatureBuffer, encoder.encode(signingString));
|
|
751
|
-
}
|
|
752
|
-
var Webhooks = class Webhooks {
|
|
636
|
+
var DomainsCustomTracking = class DomainsCustomTracking {
|
|
753
637
|
mailchannels;
|
|
638
|
+
static SCOPE_VALUES = /* @__PURE__ */ new Set([
|
|
639
|
+
"click",
|
|
640
|
+
"open",
|
|
641
|
+
"unsubscribe"
|
|
642
|
+
]);
|
|
754
643
|
constructor(mailchannels) {
|
|
755
644
|
this.mailchannels = mailchannels;
|
|
756
645
|
}
|
|
757
|
-
async
|
|
646
|
+
async list(options) {
|
|
758
647
|
let error = null;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
success: false,
|
|
763
|
-
error
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
if (endpoint.length > 8e3) {
|
|
767
|
-
error = createValidationError("The endpoint exceeds the maximum length of 8000 characters.");
|
|
768
|
-
return {
|
|
769
|
-
success: false,
|
|
770
|
-
error
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
await this.mailchannels.post("/tx/v1/webhook", {
|
|
774
|
-
query: { endpoint },
|
|
775
|
-
onResponseError: async ({ response }) => {
|
|
776
|
-
error = getStatusError(response, { [409]: `Endpoint '${endpoint}' is already enrolled to receive notifications.` });
|
|
777
|
-
}
|
|
778
|
-
}).catch((e) => {
|
|
779
|
-
error ||= getResultError(e, "Failed to create webhook.");
|
|
648
|
+
error = validatePagination({
|
|
649
|
+
...options,
|
|
650
|
+
max: 1e3
|
|
780
651
|
});
|
|
781
|
-
return {
|
|
782
|
-
|
|
652
|
+
if (error) return {
|
|
653
|
+
data: null,
|
|
783
654
|
error
|
|
784
655
|
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
error ||= getResultError(e, "Failed to fetch
|
|
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.");
|
|
792
663
|
return null;
|
|
793
664
|
});
|
|
794
665
|
if (!response) return {
|
|
@@ -796,68 +667,489 @@ var Webhooks = class Webhooks {
|
|
|
796
667
|
error
|
|
797
668
|
};
|
|
798
669
|
return {
|
|
799
|
-
data: clean(
|
|
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
|
+
}),
|
|
800
680
|
error: null
|
|
801
681
|
};
|
|
802
682
|
}
|
|
803
|
-
async
|
|
683
|
+
async create(name, hostname, scope) {
|
|
804
684
|
let error = null;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
error ||= getResultError(e, "Failed to delete webhooks.");
|
|
809
|
-
});
|
|
810
|
-
return {
|
|
811
|
-
success: !error,
|
|
685
|
+
error = validateCustomTrackingName(name);
|
|
686
|
+
if (error) return {
|
|
687
|
+
data: null,
|
|
812
688
|
error
|
|
813
689
|
};
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
+
},
|
|
819
715
|
onResponseError: async ({ response }) => {
|
|
820
716
|
error = getStatusError(response, {
|
|
821
|
-
[400]: "
|
|
822
|
-
[
|
|
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."
|
|
823
721
|
});
|
|
824
722
|
}
|
|
825
723
|
}).catch((e) => {
|
|
826
|
-
error ||= getResultError(e, "Failed to
|
|
724
|
+
error ||= getResultError(e, "Failed to create custom tracking domain.");
|
|
827
725
|
return null;
|
|
828
726
|
});
|
|
829
727
|
if (!response) return {
|
|
830
728
|
data: null,
|
|
831
729
|
error
|
|
832
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;
|
|
833
745
|
return {
|
|
834
746
|
data: clean({
|
|
835
|
-
|
|
836
|
-
|
|
747
|
+
dnsSetupRequired: false,
|
|
748
|
+
name: createResponse.name,
|
|
749
|
+
hostname: createResponse.hostname,
|
|
750
|
+
scope: createResponse.scope,
|
|
751
|
+
status: createResponse.status,
|
|
752
|
+
createdAt: createResponse.created_at
|
|
837
753
|
}),
|
|
838
754
|
error: null
|
|
839
755
|
};
|
|
840
756
|
}
|
|
841
|
-
async
|
|
757
|
+
async update(hostname, scope, options) {
|
|
842
758
|
let error = null;
|
|
843
|
-
if (
|
|
844
|
-
error = createValidationError("
|
|
759
|
+
if (!hostname) {
|
|
760
|
+
error = createValidationError("Hostname is required.");
|
|
845
761
|
return {
|
|
846
762
|
data: null,
|
|
847
763
|
error
|
|
848
764
|
};
|
|
849
765
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
+
};
|
|
867
|
+
var Domains = class {
|
|
868
|
+
mailchannels;
|
|
869
|
+
dkim;
|
|
870
|
+
customTracking;
|
|
871
|
+
constructor(mailchannels) {
|
|
872
|
+
this.mailchannels = mailchannels;
|
|
873
|
+
this.dkim = new DomainsDkim(mailchannels);
|
|
874
|
+
this.customTracking = new DomainsCustomTracking(mailchannels);
|
|
875
|
+
}
|
|
876
|
+
async check(domain, options) {
|
|
877
|
+
let error = null;
|
|
878
|
+
if (!domain) {
|
|
879
|
+
error = createValidationError("No domain provided.");
|
|
880
|
+
return {
|
|
881
|
+
data: null,
|
|
882
|
+
error
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const dkimOptions = options?.dkim ? Array.isArray(options.dkim) ? options.dkim : [options.dkim] : void 0;
|
|
886
|
+
if (dkimOptions && dkimOptions.length > 10) {
|
|
887
|
+
error = createValidationError("A maximum of 10 DKIM settings can be provided.");
|
|
888
|
+
return {
|
|
889
|
+
data: null,
|
|
890
|
+
error
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (dkimOptions?.find(({ privateKey, selector }) => privateKey && !selector)) {
|
|
894
|
+
error = createValidationError("DKIM settings with a privateKey must also include a selector.");
|
|
895
|
+
return {
|
|
896
|
+
data: null,
|
|
897
|
+
error
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const payload = {
|
|
901
|
+
dkim_settings: dkimOptions?.map((dkim) => ({
|
|
902
|
+
dkim_domain: dkim.domain,
|
|
903
|
+
dkim_private_key: dkim.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
|
|
904
|
+
dkim_selector: dkim.selector
|
|
905
|
+
})),
|
|
906
|
+
domain,
|
|
907
|
+
sender_id: options?.senderId
|
|
908
|
+
};
|
|
909
|
+
const response = await this.mailchannels.post("/tx/v1/check-domain", {
|
|
910
|
+
body: payload,
|
|
911
|
+
onResponseError: async ({ response }) => {
|
|
912
|
+
error = getStatusError(response, {
|
|
913
|
+
[400]: "Bad Request.",
|
|
914
|
+
[403]: "User does not have access to this feature."
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}).catch((e) => {
|
|
918
|
+
error ||= getResultError(e, "Failed to check domain.");
|
|
919
|
+
return null;
|
|
920
|
+
});
|
|
921
|
+
if (!response) return {
|
|
922
|
+
data: null,
|
|
923
|
+
error
|
|
924
|
+
};
|
|
925
|
+
return {
|
|
926
|
+
data: clean({
|
|
927
|
+
dkim: response.check_results.dkim.map((dkimResults) => ({
|
|
928
|
+
domain: dkimResults.dkim_domain,
|
|
929
|
+
keyStatus: dkimResults.dkim_key_status,
|
|
930
|
+
selector: dkimResults.dkim_selector,
|
|
931
|
+
reason: dkimResults.reason,
|
|
932
|
+
verdict: dkimResults.verdict
|
|
933
|
+
})),
|
|
934
|
+
domainLockdown: response.check_results.domain_lockdown,
|
|
935
|
+
senderDomain: response.check_results.sender_domain,
|
|
936
|
+
spf: response.check_results.spf,
|
|
937
|
+
references: response.references
|
|
938
|
+
}),
|
|
939
|
+
error: null
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const HMAC_SHA256 = {
|
|
944
|
+
name: "HMAC",
|
|
945
|
+
hash: "SHA-256"
|
|
946
|
+
};
|
|
947
|
+
const ED25519 = {
|
|
948
|
+
name: "Ed25519",
|
|
949
|
+
namedCurve: "Ed25519"
|
|
950
|
+
};
|
|
951
|
+
const encoder = new TextEncoder();
|
|
952
|
+
const HEADER_CONTENT_DIGEST = "content-digest";
|
|
953
|
+
const HEADER_SIGNATURE = "signature";
|
|
954
|
+
const HEADER_SIGNATURE_INPUT = "signature-input";
|
|
955
|
+
const signingKeyCache = /* @__PURE__ */ new Map();
|
|
956
|
+
const validateContentDigest = async (header, body) => {
|
|
957
|
+
const match = header.match(/^(.*?)=:(.*?):$/);
|
|
958
|
+
if (!match) return false;
|
|
959
|
+
const [, algorithm, hash] = match;
|
|
960
|
+
if (!algorithm || !hash) return false;
|
|
961
|
+
const normalizedAlgorithm = algorithm.replace("-", "").toLowerCase();
|
|
962
|
+
if (!["sha256"].includes(normalizedAlgorithm)) return false;
|
|
963
|
+
const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, encoder.encode(body));
|
|
964
|
+
return Buffer.from(signatureBuffer).toString("base64") === hash;
|
|
965
|
+
};
|
|
966
|
+
const extractSignature = (signatureHeader) => {
|
|
967
|
+
const signatureMatch = signatureHeader.match(/sig_\d+=:([^:]+):/);
|
|
968
|
+
return signatureMatch && signatureMatch[1] ? signatureMatch[1] : null;
|
|
969
|
+
};
|
|
970
|
+
const extractInputValues = (header) => {
|
|
971
|
+
const match = header.match(/^(\w+)=\(([^)]+)\);created=(\d+);alg="([^"]+)";keyid="([^"]+)"$/);
|
|
972
|
+
if (!match) return null;
|
|
973
|
+
return {
|
|
974
|
+
name: match[1],
|
|
975
|
+
timestamp: Number.parseInt(match[3], 10),
|
|
976
|
+
algorithm: match[4],
|
|
977
|
+
keyId: match[5]
|
|
978
|
+
};
|
|
979
|
+
};
|
|
980
|
+
async function isValidWebhook(options) {
|
|
981
|
+
const { payload, headers, cache = true } = options;
|
|
982
|
+
const contentDigest = headers[HEADER_CONTENT_DIGEST];
|
|
983
|
+
const messageSignature = headers[HEADER_SIGNATURE];
|
|
984
|
+
const signatureInput = headers[HEADER_SIGNATURE_INPUT];
|
|
985
|
+
if (!payload || !contentDigest || !messageSignature || !signatureInput || !await validateContentDigest(contentDigest, payload)) return false;
|
|
986
|
+
const signature = extractSignature(messageSignature);
|
|
987
|
+
if (!signature) return false;
|
|
988
|
+
const values = extractInputValues(signatureInput);
|
|
989
|
+
if (!values) return false;
|
|
990
|
+
if (Math.floor(Date.now() / 1e3) - values.timestamp > 300) return false;
|
|
991
|
+
const signingString = `"content-digest": ${contentDigest}
|
|
992
|
+
"@signature-params": ("content-digest");created=${values.timestamp};alg="${values.algorithm}";keyid="${values.keyId}"`;
|
|
993
|
+
let publicKey = options.publicKey;
|
|
994
|
+
if (!publicKey && values.keyId) {
|
|
995
|
+
if (cache) publicKey = signingKeyCache.get(values.keyId);
|
|
996
|
+
if (!publicKey) {
|
|
997
|
+
const publicKeyResponse = await $fetch("/tx/v1/webhook/public-key", {
|
|
998
|
+
baseURL: "https://api.mailchannels.net",
|
|
999
|
+
query: { id: values.keyId }
|
|
1000
|
+
}).catch(() => null);
|
|
1001
|
+
if (!publicKeyResponse) return false;
|
|
1002
|
+
publicKey = publicKeyResponse.key;
|
|
1003
|
+
if (cache && publicKey) signingKeyCache.set(values.keyId, publicKey);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!publicKey) return false;
|
|
1007
|
+
publicKey = stripPemHeaders(publicKey);
|
|
1008
|
+
const encoding = "base64";
|
|
1009
|
+
const format = "spki";
|
|
1010
|
+
const publicKeyBuffer = Buffer.from(publicKey, encoding);
|
|
1011
|
+
const webhookSignatureBuffer = Buffer.from(signature, encoding);
|
|
1012
|
+
const key = await subtle.importKey(format, publicKeyBuffer, ED25519, false, ["verify"]);
|
|
1013
|
+
return subtle.verify(ED25519.name, key, webhookSignatureBuffer, encoder.encode(signingString));
|
|
1014
|
+
}
|
|
1015
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
1016
|
+
const ISO_DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
1017
|
+
const formatDateInput = (value) => {
|
|
1018
|
+
if (value instanceof Date) {
|
|
1019
|
+
if (Number.isNaN(value.getTime())) return null;
|
|
1020
|
+
return value.toISOString();
|
|
1021
|
+
}
|
|
1022
|
+
if (typeof value === "string") {
|
|
1023
|
+
if (!ISO_DATE_REGEX.test(value) && !ISO_DATE_ONLY_REGEX.test(value) || Number.isNaN(Date.parse(value))) return null;
|
|
1024
|
+
if (ISO_DATE_REGEX.test(value)) return new Date(value).toISOString();
|
|
1025
|
+
}
|
|
1026
|
+
return value;
|
|
1027
|
+
};
|
|
1028
|
+
const parseDateInputs = (options) => {
|
|
1029
|
+
const dates = {};
|
|
1030
|
+
const keys = Object.keys(options);
|
|
1031
|
+
for (const key of keys) {
|
|
1032
|
+
const formatted = formatDateInput(options?.[key]);
|
|
1033
|
+
if (formatted === null) return {
|
|
1034
|
+
dates: null,
|
|
1035
|
+
error: createValidationError(`The '${key}' value is not a valid date.`)
|
|
1036
|
+
};
|
|
1037
|
+
dates[key] = formatted;
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
dates,
|
|
1041
|
+
error: null
|
|
1042
|
+
};
|
|
1043
|
+
};
|
|
1044
|
+
var Webhooks = class Webhooks {
|
|
1045
|
+
mailchannels;
|
|
1046
|
+
constructor(mailchannels) {
|
|
1047
|
+
this.mailchannels = mailchannels;
|
|
1048
|
+
}
|
|
1049
|
+
async create(endpoint) {
|
|
1050
|
+
let error = null;
|
|
1051
|
+
if (!endpoint) {
|
|
1052
|
+
error = createValidationError("No endpoint provided.");
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
error
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (endpoint.length > 8e3) {
|
|
1059
|
+
error = createValidationError("The endpoint exceeds the maximum length of 8000 characters.");
|
|
1060
|
+
return {
|
|
1061
|
+
success: false,
|
|
1062
|
+
error
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
await this.mailchannels.post("/tx/v1/webhook", {
|
|
1066
|
+
query: { endpoint },
|
|
1067
|
+
onResponseError: async ({ response }) => {
|
|
1068
|
+
error = getStatusError(response, { [409]: `Endpoint '${endpoint}' is already enrolled to receive notifications.` });
|
|
1069
|
+
}
|
|
1070
|
+
}).catch((e) => {
|
|
1071
|
+
error ||= getResultError(e, "Failed to create webhook.");
|
|
1072
|
+
});
|
|
1073
|
+
return {
|
|
1074
|
+
success: !error,
|
|
1075
|
+
error
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async list() {
|
|
1079
|
+
let error = null;
|
|
1080
|
+
const response = await this.mailchannels.get("/tx/v1/webhook", { onResponseError: async ({ response }) => {
|
|
1081
|
+
error = getStatusError(response);
|
|
1082
|
+
} }).catch((e) => {
|
|
1083
|
+
error ||= getResultError(e, "Failed to fetch webhooks.");
|
|
1084
|
+
return null;
|
|
1085
|
+
});
|
|
1086
|
+
if (!response) return {
|
|
1087
|
+
data: null,
|
|
1088
|
+
error
|
|
1089
|
+
};
|
|
1090
|
+
return {
|
|
1091
|
+
data: clean(response.map((webhooks) => ({ webhook: webhooks.webhook }))),
|
|
1092
|
+
error: null
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
async deleteAll() {
|
|
1096
|
+
let error = null;
|
|
1097
|
+
await this.mailchannels.delete("/tx/v1/webhook", { onResponseError: async ({ response }) => {
|
|
1098
|
+
error = getStatusError(response);
|
|
1099
|
+
} }).catch((e) => {
|
|
1100
|
+
error ||= getResultError(e, "Failed to delete webhooks.");
|
|
1101
|
+
});
|
|
1102
|
+
return {
|
|
1103
|
+
success: !error,
|
|
1104
|
+
error
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async getSigningKey(id) {
|
|
1108
|
+
let error = null;
|
|
1109
|
+
const response = await this.mailchannels.get("/tx/v1/webhook/public-key", {
|
|
1110
|
+
query: { id },
|
|
1111
|
+
onResponseError: async ({ response }) => {
|
|
1112
|
+
error = getStatusError(response, {
|
|
1113
|
+
[400]: "Bad Request.",
|
|
1114
|
+
[404]: `The key '${id}' is not found.`
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}).catch((e) => {
|
|
1118
|
+
error ||= getResultError(e, "Failed to get signing key.");
|
|
1119
|
+
return null;
|
|
1120
|
+
});
|
|
1121
|
+
if (!response) return {
|
|
1122
|
+
data: null,
|
|
1123
|
+
error
|
|
1124
|
+
};
|
|
1125
|
+
return {
|
|
1126
|
+
data: clean({
|
|
1127
|
+
id: response.id,
|
|
1128
|
+
key: response.key
|
|
1129
|
+
}),
|
|
1130
|
+
error: null
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
async validate(requestId) {
|
|
1134
|
+
let error = null;
|
|
1135
|
+
if (requestId && requestId.length > 28) {
|
|
1136
|
+
error = createValidationError("The request id should not exceed 28 characters.");
|
|
1137
|
+
return {
|
|
1138
|
+
data: null,
|
|
1139
|
+
error
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
const response = await this.mailchannels.post("/tx/v1/webhook/validate", {
|
|
1143
|
+
body: { request_id: requestId },
|
|
1144
|
+
onResponseError: async ({ response }) => {
|
|
1145
|
+
error = getStatusError(response, {
|
|
1146
|
+
[400]: "Bad Request.",
|
|
1147
|
+
[404]: "No webhooks found for the account."
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}).catch((e) => {
|
|
1151
|
+
error ||= getResultError(e, "Failed to validate webhooks.");
|
|
1152
|
+
return null;
|
|
861
1153
|
});
|
|
862
1154
|
if (!response) return {
|
|
863
1155
|
data: null,
|
|
@@ -936,17 +1228,17 @@ var Webhooks = class Webhooks {
|
|
|
936
1228
|
data: null,
|
|
937
1229
|
error: createValidationError("Status filters must be unique.")
|
|
938
1230
|
};
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
};
|
|
943
|
-
if (
|
|
1231
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1232
|
+
createdAfter: options?.createdAfter,
|
|
1233
|
+
createdBefore: options?.createdBefore
|
|
1234
|
+
});
|
|
1235
|
+
if (!dates || dateError) return {
|
|
944
1236
|
data: null,
|
|
945
|
-
error:
|
|
1237
|
+
error: dateError
|
|
946
1238
|
};
|
|
947
|
-
if (
|
|
948
|
-
const createdAfter = Date.parse(
|
|
949
|
-
const createdBefore = Date.parse(
|
|
1239
|
+
if (dates.createdAfter && dates.createdBefore) {
|
|
1240
|
+
const createdAfter = Date.parse(dates.createdAfter);
|
|
1241
|
+
const createdBefore = Date.parse(dates.createdBefore);
|
|
950
1242
|
const maxRangeMs = 744 * 60 * 60 * 1e3;
|
|
951
1243
|
if (createdBefore <= createdAfter) return {
|
|
952
1244
|
data: null,
|
|
@@ -959,8 +1251,8 @@ var Webhooks = class Webhooks {
|
|
|
959
1251
|
}
|
|
960
1252
|
const response = await this.mailchannels.get("/tx/v1/webhook-batch", {
|
|
961
1253
|
query: {
|
|
962
|
-
created_after:
|
|
963
|
-
created_before:
|
|
1254
|
+
created_after: dates.createdAfter,
|
|
1255
|
+
created_before: dates.createdBefore,
|
|
964
1256
|
statuses: options?.statuses,
|
|
965
1257
|
webhook: options?.webhook,
|
|
966
1258
|
limit: options?.limit,
|
|
@@ -972,200 +1264,60 @@ var Webhooks = class Webhooks {
|
|
|
972
1264
|
}).catch((e) => {
|
|
973
1265
|
error ||= getResultError(e, "Failed to fetch webhook batches.");
|
|
974
1266
|
return null;
|
|
975
|
-
});
|
|
976
|
-
if (!response) return {
|
|
977
|
-
data: null,
|
|
978
|
-
error
|
|
979
|
-
};
|
|
980
|
-
return {
|
|
981
|
-
data: clean(response.webhook_batches.map((batch) => ({
|
|
982
|
-
batchId: batch.batch_id,
|
|
983
|
-
createdAt: batch.created_at,
|
|
984
|
-
customerHandle: batch.customer_handle,
|
|
985
|
-
duration: batch.duration,
|
|
986
|
-
eventCount: batch.event_count,
|
|
987
|
-
status: batch.status,
|
|
988
|
-
statusCode: batch.status_code,
|
|
989
|
-
webhook: batch.webhook
|
|
990
|
-
}))),
|
|
991
|
-
error: null
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
async resendBatch(batchId) {
|
|
995
|
-
let error = null;
|
|
996
|
-
const response = await this.mailchannels.post(`/tx/v1/webhook-batch/${batchId}/resend`, { onResponseError: async ({ response }) => {
|
|
997
|
-
error = getStatusError(response, {
|
|
998
|
-
[400]: "Bad Request. The batch ID is invalid.",
|
|
999
|
-
[404]: `The batch '${batchId}' is not found for the customer.`
|
|
1000
|
-
});
|
|
1001
|
-
} }).catch((e) => {
|
|
1002
|
-
error ||= getResultError(e, "Failed to resend webhook batch.");
|
|
1003
|
-
return null;
|
|
1004
|
-
});
|
|
1005
|
-
if (!response) return {
|
|
1006
|
-
data: null,
|
|
1007
|
-
error
|
|
1008
|
-
};
|
|
1009
|
-
return {
|
|
1010
|
-
data: clean({
|
|
1011
|
-
batchId: response.batch_id,
|
|
1012
|
-
customerHandle: response.customer_handle,
|
|
1013
|
-
webhook: response.webhook,
|
|
1014
|
-
createdAt: response.created_at,
|
|
1015
|
-
eventCount: response.event_count,
|
|
1016
|
-
duration: response.duration_in_ms,
|
|
1017
|
-
statusCode: response.status_code
|
|
1018
|
-
}),
|
|
1019
|
-
error: null
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
var SubAccounts = class SubAccounts {
|
|
1024
|
-
mailchannels;
|
|
1025
|
-
static COMPANY_PATTERN = /^.{3,128}$/;
|
|
1026
|
-
static HANDLE_PATTERN = /^[a-z0-9]{3,128}$/;
|
|
1027
|
-
constructor(mailchannels) {
|
|
1028
|
-
this.mailchannels = mailchannels;
|
|
1029
|
-
}
|
|
1030
|
-
async create(companyName, handle) {
|
|
1031
|
-
let error = null;
|
|
1032
|
-
if (!SubAccounts.COMPANY_PATTERN.test(companyName)) {
|
|
1033
|
-
error = createValidationError("Invalid company name. Company name must be between 3 and 128 characters.");
|
|
1034
|
-
return {
|
|
1035
|
-
data: null,
|
|
1036
|
-
error
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
if (handle) {
|
|
1040
|
-
if (!SubAccounts.HANDLE_PATTERN.test(handle)) {
|
|
1041
|
-
error = createValidationError("Invalid handle. Sub-account handle must be between 3 and 128 characters and contain only lowercase letters and numbers.");
|
|
1042
|
-
return {
|
|
1043
|
-
data: null,
|
|
1044
|
-
error
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
const response = await this.mailchannels.post("/tx/v1/sub-account", {
|
|
1049
|
-
body: {
|
|
1050
|
-
company_name: companyName,
|
|
1051
|
-
handle
|
|
1052
|
-
},
|
|
1053
|
-
onResponseError: async ({ response }) => {
|
|
1054
|
-
error = getStatusError(response, {
|
|
1055
|
-
[403]: "The parent account does not have permission to create sub-accounts.",
|
|
1056
|
-
[409]: `Sub-account with handle '${handle}' already exists.`
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
}).catch((e) => {
|
|
1060
|
-
error ||= getResultError(e, "Failed to create sub-account.");
|
|
1061
|
-
return null;
|
|
1062
|
-
});
|
|
1063
|
-
if (!response) return {
|
|
1064
|
-
data: null,
|
|
1065
|
-
error
|
|
1066
|
-
};
|
|
1067
|
-
return {
|
|
1068
|
-
data: clean({
|
|
1069
|
-
companyName: response.company_name,
|
|
1070
|
-
enabled: response.enabled,
|
|
1071
|
-
handle: response.handle
|
|
1072
|
-
}),
|
|
1073
|
-
error: null
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
async list(options) {
|
|
1077
|
-
let error = null;
|
|
1078
|
-
error = validatePagination({
|
|
1079
|
-
...options,
|
|
1080
|
-
max: 1e3
|
|
1081
|
-
});
|
|
1082
|
-
if (error) return {
|
|
1083
|
-
data: null,
|
|
1084
|
-
error
|
|
1085
|
-
};
|
|
1086
|
-
const response = await this.mailchannels.get("/tx/v1/sub-account", {
|
|
1087
|
-
query: options,
|
|
1088
|
-
onResponseError: async ({ response }) => {
|
|
1089
|
-
error = getStatusError(response);
|
|
1090
|
-
}
|
|
1091
|
-
}).catch((e) => {
|
|
1092
|
-
error ||= getResultError(e, "Failed to fetch sub-accounts.");
|
|
1093
|
-
return null;
|
|
1094
|
-
});
|
|
1095
|
-
if (!response) return {
|
|
1096
|
-
data: null,
|
|
1097
|
-
error
|
|
1098
|
-
};
|
|
1099
|
-
return {
|
|
1100
|
-
data: clean(response.map((account) => ({
|
|
1101
|
-
companyName: account.company_name,
|
|
1102
|
-
enabled: account.enabled,
|
|
1103
|
-
handle: account.handle
|
|
1104
|
-
}))),
|
|
1105
|
-
error: null
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
async delete(handle) {
|
|
1109
|
-
let error = null;
|
|
1110
|
-
if (!handle) {
|
|
1111
|
-
error = createValidationError("No handle provided.");
|
|
1112
|
-
return {
|
|
1113
|
-
success: false,
|
|
1114
|
-
error
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
await this.mailchannels.delete(`/tx/v1/sub-account/${handle}`, { onResponseError: async ({ response }) => {
|
|
1118
|
-
error = getStatusError(response);
|
|
1119
|
-
} }).catch((e) => {
|
|
1120
|
-
error ||= getResultError(e, "Failed to delete sub-account.");
|
|
1121
|
-
});
|
|
1122
|
-
return {
|
|
1123
|
-
success: !error,
|
|
1267
|
+
});
|
|
1268
|
+
if (!response) return {
|
|
1269
|
+
data: null,
|
|
1124
1270
|
error
|
|
1125
1271
|
};
|
|
1126
|
-
}
|
|
1127
|
-
async suspend(handle) {
|
|
1128
|
-
let error = null;
|
|
1129
|
-
if (!handle) {
|
|
1130
|
-
error = createValidationError("No handle provided.");
|
|
1131
|
-
return {
|
|
1132
|
-
success: false,
|
|
1133
|
-
error
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
await this.mailchannels.post(`/tx/v1/sub-account/${handle}/suspend`, { onResponseError: async ({ response }) => {
|
|
1137
|
-
error = getStatusError(response, { [404]: `The specified sub-account '${handle}' does not exist.` });
|
|
1138
|
-
} }).catch((e) => {
|
|
1139
|
-
error ||= getResultError(e, "Failed to suspend sub-account.");
|
|
1140
|
-
});
|
|
1141
1272
|
return {
|
|
1142
|
-
|
|
1143
|
-
|
|
1273
|
+
data: clean(response.webhook_batches.map((batch) => ({
|
|
1274
|
+
batchId: batch.batch_id,
|
|
1275
|
+
createdAt: batch.created_at,
|
|
1276
|
+
customerHandle: batch.customer_handle,
|
|
1277
|
+
duration: batch.duration,
|
|
1278
|
+
eventCount: batch.event_count,
|
|
1279
|
+
status: batch.status,
|
|
1280
|
+
statusCode: batch.status_code,
|
|
1281
|
+
webhook: batch.webhook
|
|
1282
|
+
}))),
|
|
1283
|
+
error: null
|
|
1144
1284
|
};
|
|
1145
1285
|
}
|
|
1146
|
-
async
|
|
1286
|
+
async resendBatch(batchId) {
|
|
1147
1287
|
let error = null;
|
|
1148
|
-
|
|
1149
|
-
error = createValidationError("No handle provided.");
|
|
1150
|
-
return {
|
|
1151
|
-
success: false,
|
|
1152
|
-
error
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
await this.mailchannels.post(`/tx/v1/sub-account/${handle}/activate`, { onResponseError: async ({ response }) => {
|
|
1288
|
+
const response = await this.mailchannels.post(`/tx/v1/webhook-batch/${encodeURIComponent(batchId)}/resend`, { onResponseError: async ({ response }) => {
|
|
1156
1289
|
error = getStatusError(response, {
|
|
1157
|
-
[
|
|
1158
|
-
[404]: `The
|
|
1290
|
+
[400]: "Bad Request. The batch ID is invalid.",
|
|
1291
|
+
[404]: `The batch '${batchId}' is not found for the customer.`
|
|
1159
1292
|
});
|
|
1160
1293
|
} }).catch((e) => {
|
|
1161
|
-
error ||= getResultError(e, "Failed to
|
|
1294
|
+
error ||= getResultError(e, "Failed to resend webhook batch.");
|
|
1295
|
+
return null;
|
|
1162
1296
|
});
|
|
1163
|
-
return {
|
|
1164
|
-
|
|
1297
|
+
if (!response) return {
|
|
1298
|
+
data: null,
|
|
1165
1299
|
error
|
|
1166
1300
|
};
|
|
1301
|
+
return {
|
|
1302
|
+
data: clean({
|
|
1303
|
+
batchId: response.batch_id,
|
|
1304
|
+
customerHandle: response.customer_handle,
|
|
1305
|
+
webhook: response.webhook,
|
|
1306
|
+
createdAt: response.created_at,
|
|
1307
|
+
eventCount: response.event_count,
|
|
1308
|
+
duration: response.duration_in_ms,
|
|
1309
|
+
statusCode: response.status_code
|
|
1310
|
+
}),
|
|
1311
|
+
error: null
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
var SubAccountsApiKeys = class {
|
|
1316
|
+
mailchannels;
|
|
1317
|
+
constructor(mailchannels) {
|
|
1318
|
+
this.mailchannels = mailchannels;
|
|
1167
1319
|
}
|
|
1168
|
-
async
|
|
1320
|
+
async create(handle) {
|
|
1169
1321
|
let error = null;
|
|
1170
1322
|
if (!handle) {
|
|
1171
1323
|
error = createValidationError("No handle provided.");
|
|
@@ -1174,7 +1326,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1174
1326
|
error
|
|
1175
1327
|
};
|
|
1176
1328
|
}
|
|
1177
|
-
const response = await this.mailchannels.post(`/tx/v1/sub-account/${handle}/api-key`, { onResponseError: async ({ response }) => {
|
|
1329
|
+
const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key`, { onResponseError: async ({ response }) => {
|
|
1178
1330
|
error = getStatusError(response, {
|
|
1179
1331
|
[403]: "You can't create API keys for this sub-account.",
|
|
1180
1332
|
[404]: `Sub-account with handle '${handle}' not found.`,
|
|
@@ -1193,7 +1345,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1193
1345
|
error: null
|
|
1194
1346
|
};
|
|
1195
1347
|
}
|
|
1196
|
-
async
|
|
1348
|
+
async list(handle, options) {
|
|
1197
1349
|
let error = null;
|
|
1198
1350
|
if (!handle) {
|
|
1199
1351
|
error = createValidationError("No handle provided.");
|
|
@@ -1210,9 +1362,12 @@ var SubAccounts = class SubAccounts {
|
|
|
1210
1362
|
data: null,
|
|
1211
1363
|
error
|
|
1212
1364
|
};
|
|
1213
|
-
const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/api-key`, {
|
|
1214
|
-
|
|
1215
|
-
|
|
1365
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key`, {
|
|
1366
|
+
query: options,
|
|
1367
|
+
onResponseError: async ({ response }) => {
|
|
1368
|
+
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1369
|
+
}
|
|
1370
|
+
}).catch((e) => {
|
|
1216
1371
|
error ||= getResultError(e, "Failed to fetch sub-account API keys.");
|
|
1217
1372
|
return null;
|
|
1218
1373
|
});
|
|
@@ -1228,7 +1383,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1228
1383
|
error: null
|
|
1229
1384
|
};
|
|
1230
1385
|
}
|
|
1231
|
-
async
|
|
1386
|
+
async delete(handle, id) {
|
|
1232
1387
|
let error = null;
|
|
1233
1388
|
if (!handle) {
|
|
1234
1389
|
error = createValidationError("No handle provided.");
|
|
@@ -1237,7 +1392,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1237
1392
|
error
|
|
1238
1393
|
};
|
|
1239
1394
|
}
|
|
1240
|
-
await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/api-key/${id}`, { onResponseError: async ({ response }) => {
|
|
1395
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
|
|
1241
1396
|
error = getStatusError(response, { [400]: "Missing or invalid API key ID." });
|
|
1242
1397
|
} }).catch((e) => {
|
|
1243
1398
|
error ||= getResultError(e, "Failed to delete sub-account API key.");
|
|
@@ -1247,7 +1402,13 @@ var SubAccounts = class SubAccounts {
|
|
|
1247
1402
|
error
|
|
1248
1403
|
};
|
|
1249
1404
|
}
|
|
1250
|
-
|
|
1405
|
+
};
|
|
1406
|
+
var SubAccountsSmtpPasswords = class {
|
|
1407
|
+
mailchannels;
|
|
1408
|
+
constructor(mailchannels) {
|
|
1409
|
+
this.mailchannels = mailchannels;
|
|
1410
|
+
}
|
|
1411
|
+
async create(handle) {
|
|
1251
1412
|
let error = null;
|
|
1252
1413
|
if (!handle) {
|
|
1253
1414
|
error = createValidationError("No handle provided.");
|
|
@@ -1256,7 +1417,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1256
1417
|
error
|
|
1257
1418
|
};
|
|
1258
1419
|
}
|
|
1259
|
-
const response = await this.mailchannels.post(`/tx/v1/sub-account/${handle}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1420
|
+
const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1260
1421
|
error = getStatusError(response, {
|
|
1261
1422
|
[403]: "You can't create SMTP passwords for this sub-account.",
|
|
1262
1423
|
[404]: `Sub-account with handle '${handle}' not found.`,
|
|
@@ -1279,7 +1440,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1279
1440
|
error: null
|
|
1280
1441
|
};
|
|
1281
1442
|
}
|
|
1282
|
-
async
|
|
1443
|
+
async list(handle) {
|
|
1283
1444
|
let error = null;
|
|
1284
1445
|
if (!handle) {
|
|
1285
1446
|
error = createValidationError("No handle provided.");
|
|
@@ -1288,7 +1449,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1288
1449
|
error
|
|
1289
1450
|
};
|
|
1290
1451
|
}
|
|
1291
|
-
const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1452
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1292
1453
|
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1293
1454
|
} }).catch((e) => {
|
|
1294
1455
|
error ||= getResultError(e, "Failed to fetch sub-account SMTP passwords.");
|
|
@@ -1307,7 +1468,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1307
1468
|
error: null
|
|
1308
1469
|
};
|
|
1309
1470
|
}
|
|
1310
|
-
async
|
|
1471
|
+
async delete(handle, id) {
|
|
1311
1472
|
let error = null;
|
|
1312
1473
|
if (!handle) {
|
|
1313
1474
|
error = createValidationError("No handle provided.");
|
|
@@ -1316,7 +1477,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1316
1477
|
error
|
|
1317
1478
|
};
|
|
1318
1479
|
}
|
|
1319
|
-
await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/smtp-password/${id}`, { onResponseError: async ({ response }) => {
|
|
1480
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
|
|
1320
1481
|
error = getStatusError(response, { [400]: "Missing or invalid SMTP password ID." });
|
|
1321
1482
|
} }).catch((e) => {
|
|
1322
1483
|
error ||= getResultError(e, "Failed to delete sub-account SMTP password.");
|
|
@@ -1326,7 +1487,13 @@ var SubAccounts = class SubAccounts {
|
|
|
1326
1487
|
error
|
|
1327
1488
|
};
|
|
1328
1489
|
}
|
|
1329
|
-
|
|
1490
|
+
};
|
|
1491
|
+
var SubAccountsLimits = class {
|
|
1492
|
+
mailchannels;
|
|
1493
|
+
constructor(mailchannels) {
|
|
1494
|
+
this.mailchannels = mailchannels;
|
|
1495
|
+
}
|
|
1496
|
+
async get(handle) {
|
|
1330
1497
|
let error = null;
|
|
1331
1498
|
if (!handle) {
|
|
1332
1499
|
error = createValidationError("No handle provided.");
|
|
@@ -1335,7 +1502,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1335
1502
|
error
|
|
1336
1503
|
};
|
|
1337
1504
|
}
|
|
1338
|
-
const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/limit`, { onResponseError: async ({ response }) => {
|
|
1505
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
|
|
1339
1506
|
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1340
1507
|
} }).catch((e) => {
|
|
1341
1508
|
error ||= getResultError(e, "Failed to fetch sub-account limit.");
|
|
@@ -1350,7 +1517,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1350
1517
|
error: null
|
|
1351
1518
|
};
|
|
1352
1519
|
}
|
|
1353
|
-
async
|
|
1520
|
+
async set(handle, options) {
|
|
1354
1521
|
let error = null;
|
|
1355
1522
|
if (!handle) {
|
|
1356
1523
|
error = createValidationError("No handle provided.");
|
|
@@ -1359,15 +1526,15 @@ var SubAccounts = class SubAccounts {
|
|
|
1359
1526
|
error
|
|
1360
1527
|
};
|
|
1361
1528
|
}
|
|
1362
|
-
if (
|
|
1529
|
+
if (options.sends < 0) {
|
|
1363
1530
|
error = createValidationError("The sends value must be at least 0.");
|
|
1364
1531
|
return {
|
|
1365
1532
|
success: false,
|
|
1366
1533
|
error
|
|
1367
1534
|
};
|
|
1368
1535
|
}
|
|
1369
|
-
await this.mailchannels.put(`/tx/v1/sub-account/${handle}/limit`, {
|
|
1370
|
-
body:
|
|
1536
|
+
await this.mailchannels.put(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, {
|
|
1537
|
+
body: options,
|
|
1371
1538
|
onResponseError: async ({ response }) => {
|
|
1372
1539
|
error = getStatusError(response, {
|
|
1373
1540
|
[400]: "Bad Request.",
|
|
@@ -1382,7 +1549,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1382
1549
|
error
|
|
1383
1550
|
};
|
|
1384
1551
|
}
|
|
1385
|
-
async
|
|
1552
|
+
async delete(handle) {
|
|
1386
1553
|
let error = null;
|
|
1387
1554
|
if (!handle) {
|
|
1388
1555
|
error = createValidationError("No handle provided.");
|
|
@@ -1391,7 +1558,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1391
1558
|
error
|
|
1392
1559
|
};
|
|
1393
1560
|
}
|
|
1394
|
-
await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/limit`, { onResponseError: async ({ response }) => {
|
|
1561
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
|
|
1395
1562
|
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1396
1563
|
} }).catch((e) => {
|
|
1397
1564
|
error ||= getResultError(e, "Failed to delete sub-account limit.");
|
|
@@ -1401,6 +1568,158 @@ var SubAccounts = class SubAccounts {
|
|
|
1401
1568
|
error
|
|
1402
1569
|
};
|
|
1403
1570
|
}
|
|
1571
|
+
};
|
|
1572
|
+
var SubAccounts = class SubAccounts {
|
|
1573
|
+
mailchannels;
|
|
1574
|
+
static COMPANY_PATTERN = /^.{3,128}$/;
|
|
1575
|
+
static HANDLE_PATTERN = /^[a-z0-9]{3,128}$/;
|
|
1576
|
+
apiKeys;
|
|
1577
|
+
smtpPasswords;
|
|
1578
|
+
limits;
|
|
1579
|
+
constructor(mailchannels) {
|
|
1580
|
+
this.mailchannels = mailchannels;
|
|
1581
|
+
this.apiKeys = new SubAccountsApiKeys(mailchannels);
|
|
1582
|
+
this.smtpPasswords = new SubAccountsSmtpPasswords(mailchannels);
|
|
1583
|
+
this.limits = new SubAccountsLimits(mailchannels);
|
|
1584
|
+
}
|
|
1585
|
+
async create(companyName, handle) {
|
|
1586
|
+
let error = null;
|
|
1587
|
+
if (!SubAccounts.COMPANY_PATTERN.test(companyName)) {
|
|
1588
|
+
error = createValidationError("Invalid company name. Company name must be between 3 and 128 characters.");
|
|
1589
|
+
return {
|
|
1590
|
+
data: null,
|
|
1591
|
+
error
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (handle) {
|
|
1595
|
+
if (!SubAccounts.HANDLE_PATTERN.test(handle)) {
|
|
1596
|
+
error = createValidationError("Invalid handle. Sub-account handle must be between 3 and 128 characters and contain only lowercase letters and numbers.");
|
|
1597
|
+
return {
|
|
1598
|
+
data: null,
|
|
1599
|
+
error
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const response = await this.mailchannels.post("/tx/v1/sub-account", {
|
|
1604
|
+
body: {
|
|
1605
|
+
company_name: companyName,
|
|
1606
|
+
handle
|
|
1607
|
+
},
|
|
1608
|
+
onResponseError: async ({ response }) => {
|
|
1609
|
+
error = getStatusError(response, {
|
|
1610
|
+
[403]: "The parent account does not have permission to create sub-accounts.",
|
|
1611
|
+
[409]: `Sub-account with handle '${handle}' already exists.`
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}).catch((e) => {
|
|
1615
|
+
error ||= getResultError(e, "Failed to create sub-account.");
|
|
1616
|
+
return null;
|
|
1617
|
+
});
|
|
1618
|
+
if (!response) return {
|
|
1619
|
+
data: null,
|
|
1620
|
+
error
|
|
1621
|
+
};
|
|
1622
|
+
return {
|
|
1623
|
+
data: clean({
|
|
1624
|
+
companyName: response.company_name,
|
|
1625
|
+
enabled: response.enabled,
|
|
1626
|
+
handle: response.handle
|
|
1627
|
+
}),
|
|
1628
|
+
error: null
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
async list(options) {
|
|
1632
|
+
let error = null;
|
|
1633
|
+
error = validatePagination({
|
|
1634
|
+
...options,
|
|
1635
|
+
max: 1e3
|
|
1636
|
+
});
|
|
1637
|
+
if (error) return {
|
|
1638
|
+
data: null,
|
|
1639
|
+
error
|
|
1640
|
+
};
|
|
1641
|
+
const response = await this.mailchannels.get("/tx/v1/sub-account", {
|
|
1642
|
+
query: options,
|
|
1643
|
+
onResponseError: async ({ response }) => {
|
|
1644
|
+
error = getStatusError(response);
|
|
1645
|
+
}
|
|
1646
|
+
}).catch((e) => {
|
|
1647
|
+
error ||= getResultError(e, "Failed to fetch sub-accounts.");
|
|
1648
|
+
return null;
|
|
1649
|
+
});
|
|
1650
|
+
if (!response) return {
|
|
1651
|
+
data: null,
|
|
1652
|
+
error
|
|
1653
|
+
};
|
|
1654
|
+
return {
|
|
1655
|
+
data: clean(response.map((account) => ({
|
|
1656
|
+
companyName: account.company_name,
|
|
1657
|
+
enabled: account.enabled,
|
|
1658
|
+
handle: account.handle
|
|
1659
|
+
}))),
|
|
1660
|
+
error: null
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async delete(handle) {
|
|
1664
|
+
let error = null;
|
|
1665
|
+
if (!handle) {
|
|
1666
|
+
error = createValidationError("No handle provided.");
|
|
1667
|
+
return {
|
|
1668
|
+
success: false,
|
|
1669
|
+
error
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}`, { onResponseError: async ({ response }) => {
|
|
1673
|
+
error = getStatusError(response);
|
|
1674
|
+
} }).catch((e) => {
|
|
1675
|
+
error ||= getResultError(e, "Failed to delete sub-account.");
|
|
1676
|
+
});
|
|
1677
|
+
return {
|
|
1678
|
+
success: !error,
|
|
1679
|
+
error
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
async suspend(handle) {
|
|
1683
|
+
let error = null;
|
|
1684
|
+
if (!handle) {
|
|
1685
|
+
error = createValidationError("No handle provided.");
|
|
1686
|
+
return {
|
|
1687
|
+
success: false,
|
|
1688
|
+
error
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/suspend`, { onResponseError: async ({ response }) => {
|
|
1692
|
+
error = getStatusError(response, { [404]: `The specified sub-account '${handle}' does not exist.` });
|
|
1693
|
+
} }).catch((e) => {
|
|
1694
|
+
error ||= getResultError(e, "Failed to suspend sub-account.");
|
|
1695
|
+
});
|
|
1696
|
+
return {
|
|
1697
|
+
success: !error,
|
|
1698
|
+
error
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
async activate(handle) {
|
|
1702
|
+
let error = null;
|
|
1703
|
+
if (!handle) {
|
|
1704
|
+
error = createValidationError("No handle provided.");
|
|
1705
|
+
return {
|
|
1706
|
+
success: false,
|
|
1707
|
+
error
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/activate`, { onResponseError: async ({ response }) => {
|
|
1711
|
+
error = getStatusError(response, {
|
|
1712
|
+
[403]: "The parent account does not have permission to activate the sub-account.",
|
|
1713
|
+
[404]: `The specified sub-account '${handle}' does not exist.`
|
|
1714
|
+
});
|
|
1715
|
+
} }).catch((e) => {
|
|
1716
|
+
error ||= getResultError(e, "Failed to activate sub-account.");
|
|
1717
|
+
});
|
|
1718
|
+
return {
|
|
1719
|
+
success: !error,
|
|
1720
|
+
error
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1404
1723
|
async getUsage(handle) {
|
|
1405
1724
|
let error = null;
|
|
1406
1725
|
if (!handle) {
|
|
@@ -1410,7 +1729,7 @@ var SubAccounts = class SubAccounts {
|
|
|
1410
1729
|
error
|
|
1411
1730
|
};
|
|
1412
1731
|
}
|
|
1413
|
-
const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/usage`, { onResponseError: async ({ response }) => {
|
|
1732
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/usage`, { onResponseError: async ({ response }) => {
|
|
1414
1733
|
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1415
1734
|
} }).catch((e) => {
|
|
1416
1735
|
error ||= getResultError(e, "Failed to fetch sub-account usage.");
|
|
@@ -1429,6 +1748,33 @@ var SubAccounts = class SubAccounts {
|
|
|
1429
1748
|
error: null
|
|
1430
1749
|
};
|
|
1431
1750
|
}
|
|
1751
|
+
createApiKey(...args) {
|
|
1752
|
+
return this.apiKeys.create(...args);
|
|
1753
|
+
}
|
|
1754
|
+
listApiKeys(...args) {
|
|
1755
|
+
return this.apiKeys.list(...args);
|
|
1756
|
+
}
|
|
1757
|
+
deleteApiKey(...args) {
|
|
1758
|
+
return this.apiKeys.delete(...args);
|
|
1759
|
+
}
|
|
1760
|
+
createSmtpPassword(...args) {
|
|
1761
|
+
return this.smtpPasswords.create(...args);
|
|
1762
|
+
}
|
|
1763
|
+
listSmtpPasswords(...args) {
|
|
1764
|
+
return this.smtpPasswords.list(...args);
|
|
1765
|
+
}
|
|
1766
|
+
deleteSmtpPassword(...args) {
|
|
1767
|
+
return this.smtpPasswords.delete(...args);
|
|
1768
|
+
}
|
|
1769
|
+
setLimit(...args) {
|
|
1770
|
+
return this.limits.set(...args);
|
|
1771
|
+
}
|
|
1772
|
+
getLimit(...args) {
|
|
1773
|
+
return this.limits.get(...args);
|
|
1774
|
+
}
|
|
1775
|
+
deleteLimit(...args) {
|
|
1776
|
+
return this.limits.delete(...args);
|
|
1777
|
+
}
|
|
1432
1778
|
};
|
|
1433
1779
|
const mapBucket = (bucket) => {
|
|
1434
1780
|
return {
|
|
@@ -1443,10 +1789,18 @@ var Metrics = class {
|
|
|
1443
1789
|
}
|
|
1444
1790
|
async engagement(options) {
|
|
1445
1791
|
let error = null;
|
|
1792
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1793
|
+
startTime: options?.startTime,
|
|
1794
|
+
endTime: options?.endTime
|
|
1795
|
+
});
|
|
1796
|
+
if (!dates || dateError) return {
|
|
1797
|
+
data: null,
|
|
1798
|
+
error: dateError
|
|
1799
|
+
};
|
|
1446
1800
|
const response = await this.mailchannels.get("/tx/v1/metrics/engagement", {
|
|
1447
1801
|
query: {
|
|
1448
|
-
start_time:
|
|
1449
|
-
end_time:
|
|
1802
|
+
start_time: dates.startTime,
|
|
1803
|
+
end_time: dates.endTime,
|
|
1450
1804
|
campaign_id: options?.campaignId,
|
|
1451
1805
|
interval: options?.interval
|
|
1452
1806
|
},
|
|
@@ -1467,24 +1821,40 @@ var Metrics = class {
|
|
|
1467
1821
|
click: response.buckets.click.map(mapBucket),
|
|
1468
1822
|
clickTrackingDelivered: response.buckets.click_tracking_delivered.map(mapBucket),
|
|
1469
1823
|
open: response.buckets.open.map(mapBucket),
|
|
1470
|
-
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)
|
|
1471
1829
|
},
|
|
1472
1830
|
click: response.click,
|
|
1473
1831
|
clickTrackingDelivered: response.click_tracking_delivered,
|
|
1474
1832
|
endTime: response.end_time,
|
|
1475
1833
|
open: response.open,
|
|
1476
1834
|
openTrackingDelivered: response.open_tracking_delivered,
|
|
1477
|
-
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
|
|
1478
1840
|
}),
|
|
1479
1841
|
error: null
|
|
1480
1842
|
};
|
|
1481
1843
|
}
|
|
1482
1844
|
async performance(options) {
|
|
1483
1845
|
let error = null;
|
|
1846
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1847
|
+
startTime: options?.startTime,
|
|
1848
|
+
endTime: options?.endTime
|
|
1849
|
+
});
|
|
1850
|
+
if (!dates || dateError) return {
|
|
1851
|
+
data: null,
|
|
1852
|
+
error: dateError
|
|
1853
|
+
};
|
|
1484
1854
|
const response = await this.mailchannels.get("/tx/v1/metrics/performance", {
|
|
1485
1855
|
query: {
|
|
1486
|
-
start_time:
|
|
1487
|
-
end_time:
|
|
1856
|
+
start_time: dates.startTime,
|
|
1857
|
+
end_time: dates.endTime,
|
|
1488
1858
|
campaign_id: options?.campaignId,
|
|
1489
1859
|
interval: options?.interval
|
|
1490
1860
|
},
|
|
@@ -1502,8 +1872,10 @@ var Metrics = class {
|
|
|
1502
1872
|
return {
|
|
1503
1873
|
data: clean({
|
|
1504
1874
|
bounced: response.bounced,
|
|
1875
|
+
complained: response.complained,
|
|
1505
1876
|
buckets: {
|
|
1506
1877
|
bounced: response.buckets.bounced.map(mapBucket),
|
|
1878
|
+
complained: response.buckets.complained.map(mapBucket),
|
|
1507
1879
|
delivered: response.buckets.delivered.map(mapBucket),
|
|
1508
1880
|
processed: response.buckets.processed.map(mapBucket)
|
|
1509
1881
|
},
|
|
@@ -1517,10 +1889,18 @@ var Metrics = class {
|
|
|
1517
1889
|
}
|
|
1518
1890
|
async recipientBehaviour(options) {
|
|
1519
1891
|
let error = null;
|
|
1892
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1893
|
+
startTime: options?.startTime,
|
|
1894
|
+
endTime: options?.endTime
|
|
1895
|
+
});
|
|
1896
|
+
if (!dates || dateError) return {
|
|
1897
|
+
data: null,
|
|
1898
|
+
error: dateError
|
|
1899
|
+
};
|
|
1520
1900
|
const response = await this.mailchannels.get("/tx/v1/metrics/recipient-behaviour", {
|
|
1521
1901
|
query: {
|
|
1522
|
-
start_time:
|
|
1523
|
-
end_time:
|
|
1902
|
+
start_time: dates.startTime,
|
|
1903
|
+
end_time: dates.endTime,
|
|
1524
1904
|
campaign_id: options?.campaignId,
|
|
1525
1905
|
interval: options?.interval
|
|
1526
1906
|
},
|
|
@@ -1551,10 +1931,18 @@ var Metrics = class {
|
|
|
1551
1931
|
}
|
|
1552
1932
|
async volume(options) {
|
|
1553
1933
|
let error = null;
|
|
1934
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1935
|
+
startTime: options?.startTime,
|
|
1936
|
+
endTime: options?.endTime
|
|
1937
|
+
});
|
|
1938
|
+
if (!dates || dateError) return {
|
|
1939
|
+
data: null,
|
|
1940
|
+
error: dateError
|
|
1941
|
+
};
|
|
1554
1942
|
const response = await this.mailchannels.get("/tx/v1/metrics/volume", {
|
|
1555
1943
|
query: {
|
|
1556
|
-
start_time:
|
|
1557
|
-
end_time:
|
|
1944
|
+
start_time: dates.startTime,
|
|
1945
|
+
end_time: dates.endTime,
|
|
1558
1946
|
campaign_id: options?.campaignId,
|
|
1559
1947
|
interval: options?.interval
|
|
1560
1948
|
},
|
|
@@ -1608,6 +1996,14 @@ var Metrics = class {
|
|
|
1608
1996
|
}
|
|
1609
1997
|
async senders(type, options) {
|
|
1610
1998
|
let error = null;
|
|
1999
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
2000
|
+
startTime: options?.startTime,
|
|
2001
|
+
endTime: options?.endTime
|
|
2002
|
+
});
|
|
2003
|
+
if (!dates || dateError) return {
|
|
2004
|
+
data: null,
|
|
2005
|
+
error: dateError
|
|
2006
|
+
};
|
|
1611
2007
|
error = validatePagination({
|
|
1612
2008
|
...options,
|
|
1613
2009
|
max: 1e3
|
|
@@ -1616,10 +2012,10 @@ var Metrics = class {
|
|
|
1616
2012
|
data: null,
|
|
1617
2013
|
error
|
|
1618
2014
|
};
|
|
1619
|
-
const response = await this.mailchannels.get(`/tx/v1/metrics/senders/${type}`, {
|
|
2015
|
+
const response = await this.mailchannels.get(`/tx/v1/metrics/senders/${encodeURIComponent(type)}`, {
|
|
1620
2016
|
query: {
|
|
1621
|
-
start_time:
|
|
1622
|
-
end_time:
|
|
2017
|
+
start_time: dates.startTime,
|
|
2018
|
+
end_time: dates.endTime,
|
|
1623
2019
|
limit: options?.limit,
|
|
1624
2020
|
offset: options?.offset,
|
|
1625
2021
|
sort_order: options?.sortOrder
|
|
@@ -1690,7 +2086,7 @@ var Suppressions = class {
|
|
|
1690
2086
|
}
|
|
1691
2087
|
async delete(recipient, source) {
|
|
1692
2088
|
let error = null;
|
|
1693
|
-
await this.mailchannels.delete(`/tx/v1/suppression-list/recipients/${recipient}`, {
|
|
2089
|
+
await this.mailchannels.delete(`/tx/v1/suppression-list/recipients/${encodeURIComponent(recipient)}`, {
|
|
1694
2090
|
query: { source },
|
|
1695
2091
|
onResponseError: async ({ response }) => {
|
|
1696
2092
|
error = getStatusError(response, { [400]: "Bad Request." });
|
|
@@ -1712,6 +2108,14 @@ var Suppressions = class {
|
|
|
1712
2108
|
error
|
|
1713
2109
|
};
|
|
1714
2110
|
}
|
|
2111
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
2112
|
+
createdBefore: options?.createdBefore,
|
|
2113
|
+
createdAfter: options?.createdAfter
|
|
2114
|
+
});
|
|
2115
|
+
if (!dates || dateError) return {
|
|
2116
|
+
data: null,
|
|
2117
|
+
error: dateError
|
|
2118
|
+
};
|
|
1715
2119
|
error = validatePagination({
|
|
1716
2120
|
...options,
|
|
1717
2121
|
max: 1e3
|
|
@@ -1723,8 +2127,8 @@ var Suppressions = class {
|
|
|
1723
2127
|
const payload = {
|
|
1724
2128
|
recipient: options?.recipient,
|
|
1725
2129
|
source: options?.source,
|
|
1726
|
-
created_before:
|
|
1727
|
-
created_after:
|
|
2130
|
+
created_before: dates.createdBefore,
|
|
2131
|
+
created_after: dates.createdAfter,
|
|
1728
2132
|
limit: options?.limit,
|
|
1729
2133
|
offset: options?.offset
|
|
1730
2134
|
};
|