mailchannels-sdk 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mailchannels-js/SKILL.md +4 -0
- package/.agents/skills/mailchannels-js/resources/attachments.md +8 -10
- package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +5 -5
- package/.agents/skills/mailchannels-js/resources/custom-headers.md +4 -4
- package/.agents/skills/mailchannels-js/resources/dkim.md +10 -10
- package/.agents/skills/mailchannels-js/resources/domain-checks.md +6 -6
- package/.agents/skills/mailchannels-js/resources/error-handling.md +5 -5
- package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +9 -9
- package/.agents/skills/mailchannels-js/resources/overview.md +5 -5
- package/.agents/skills/mailchannels-js/resources/plugins/nodemailer.md +112 -0
- package/.agents/skills/mailchannels-js/resources/sending.md +10 -10
- package/.agents/skills/mailchannels-js/resources/sub-accounts.md +7 -9
- package/.agents/skills/mailchannels-js/resources/suppressions.md +4 -4
- package/.agents/skills/mailchannels-js/resources/templates.md +5 -5
- package/.agents/skills/mailchannels-js/resources/testing.md +52 -5
- package/.agents/skills/mailchannels-js/resources/unsubscribe.md +3 -3
- package/.agents/skills/mailchannels-js/resources/webhooks.md +8 -8
- package/README.md +73 -2
- package/dist/_chunks/mailchannels.d.mts +2094 -0
- package/dist/_chunks/mailchannels.mjs +2201 -0
- package/dist/_chunks/simulator.mjs +16 -8
- package/dist/cli/index.mjs +268 -0
- package/dist/mailchannels.d.mts +1 -2095
- package/dist/mailchannels.mjs +1 -2201
- package/dist/plugins/nodemailer/index.d.mts +33 -0
- package/dist/plugins/nodemailer/index.mjs +145 -0
- package/dist/simulator/index.d.mts +20 -0
- package/dist/simulator/index.mjs +2 -0
- package/package.json +21 -9
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -50
|
@@ -0,0 +1,2201 @@
|
|
|
1
|
+
import { $fetch } from "ofetch";
|
|
2
|
+
import { subtle } from "node:crypto";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import mime from "mime";
|
|
5
|
+
var name = "mailchannels-sdk";
|
|
6
|
+
var version = "1.4.0";
|
|
7
|
+
var MailChannelsClient = class MailChannelsClient {
|
|
8
|
+
static DEFAULT_BASE_URL = "https://api.mailchannels.net";
|
|
9
|
+
static DEFAULT_TIMEOUT = 12e4;
|
|
10
|
+
options;
|
|
11
|
+
#headers;
|
|
12
|
+
constructor(key, options = {}) {
|
|
13
|
+
if (!key) throw new Error("Missing MailChannels API key.");
|
|
14
|
+
this.options = {
|
|
15
|
+
baseUrl: options.baseUrl || MailChannelsClient.DEFAULT_BASE_URL,
|
|
16
|
+
retry: options.retry ?? false,
|
|
17
|
+
signal: options.signal,
|
|
18
|
+
timeout: options.timeout ?? MailChannelsClient.DEFAULT_TIMEOUT
|
|
19
|
+
};
|
|
20
|
+
this.#headers = {
|
|
21
|
+
"X-API-Key": key,
|
|
22
|
+
"Accept": "application/json",
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"User-Agent": `mailchannels-node/${version}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async _fetch(path, options) {
|
|
28
|
+
const { headers, signal = this.options.signal, ...fetchOptions } = options;
|
|
29
|
+
return $fetch(path, {
|
|
30
|
+
baseURL: this.options.baseUrl,
|
|
31
|
+
retry: this.options.retry,
|
|
32
|
+
signal,
|
|
33
|
+
timeout: this.options.timeout === false ? void 0 : this.options.timeout,
|
|
34
|
+
...fetchOptions,
|
|
35
|
+
headers: {
|
|
36
|
+
...this.#headers,
|
|
37
|
+
...headers
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async post(path, options) {
|
|
42
|
+
return this._fetch(path, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
...options
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async get(path, options) {
|
|
48
|
+
return this._fetch(path, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
...options
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async delete(path, options) {
|
|
54
|
+
return this._fetch(path, {
|
|
55
|
+
method: "DELETE",
|
|
56
|
+
...options
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async put(path, options) {
|
|
60
|
+
return this._fetch(path, {
|
|
61
|
+
method: "PUT",
|
|
62
|
+
...options
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async patch(path, options) {
|
|
66
|
+
return this._fetch(path, {
|
|
67
|
+
method: "PATCH",
|
|
68
|
+
...options
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const STATUS_ERROR_TYPE_MAP = {
|
|
73
|
+
[400]: "invalid_request_error",
|
|
74
|
+
[401]: "authentication_error",
|
|
75
|
+
[403]: "permission_error",
|
|
76
|
+
[404]: "not_found",
|
|
77
|
+
[409]: "conflict_error",
|
|
78
|
+
[413]: "payload_too_large_error",
|
|
79
|
+
[422]: "unprocessable_entity_error",
|
|
80
|
+
[429]: "rate_limit_error",
|
|
81
|
+
[500]: "internal_server_error"
|
|
82
|
+
};
|
|
83
|
+
const createError = (message, statusCode = null, type, response = null) => {
|
|
84
|
+
return {
|
|
85
|
+
message,
|
|
86
|
+
statusCode,
|
|
87
|
+
type,
|
|
88
|
+
response
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const getStatusError = (response, errors = {}) => {
|
|
92
|
+
const statusText = errors[response.status] || "Unknown error.";
|
|
93
|
+
const payload = response._data ?? response.data;
|
|
94
|
+
let details;
|
|
95
|
+
let errorResponse = null;
|
|
96
|
+
if (typeof payload === "string") details = payload;
|
|
97
|
+
else if (typeof payload === "object" && payload !== null) {
|
|
98
|
+
if (typeof payload.message === "string") details = payload.message;
|
|
99
|
+
else if (Array.isArray(payload.errors) && payload.errors.length) details = payload.errors.join(", ");
|
|
100
|
+
errorResponse = payload;
|
|
101
|
+
}
|
|
102
|
+
return createError(details ? `${statusText} ${details}` : statusText, response.status ?? null, STATUS_ERROR_TYPE_MAP[response.status] || "api_error", errorResponse);
|
|
103
|
+
};
|
|
104
|
+
const getResultError = (e, fallback) => {
|
|
105
|
+
return createError(e instanceof Error ? e.message : fallback, null, "application_error");
|
|
106
|
+
};
|
|
107
|
+
const createValidationError = (message) => {
|
|
108
|
+
return createError(message, null, "validation_error");
|
|
109
|
+
};
|
|
110
|
+
const validatePagination = (pagination = {}) => {
|
|
111
|
+
const { limit, offset, max } = pagination;
|
|
112
|
+
if (typeof limit === "number" && (limit < 1 || max && limit > max)) return createValidationError("The limit value " + (max ? `must be between 1 and ${max}.` : "is invalid. Only positive values are allowed."));
|
|
113
|
+
if (typeof offset === "number" && offset < 0) return createValidationError("Offset must be greater than or equal to 0.");
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
const CUSTOM_TRACKING_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
117
|
+
const validateCustomTrackingName = (name) => {
|
|
118
|
+
if (!name || name.length < 1 || name.length > 64) return createValidationError("The custom tracking domain name must be between 1 and 64 characters.");
|
|
119
|
+
if (!CUSTOM_TRACKING_NAME_PATTERN.test(name)) return createValidationError("The custom tracking domain name must match ^[a-z0-9-]+$");
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
const clean = (data) => {
|
|
123
|
+
if (Array.isArray(data)) {
|
|
124
|
+
const result = [];
|
|
125
|
+
for (let i = 0; i < data.length; i++) {
|
|
126
|
+
const cleaned = clean(data[i]);
|
|
127
|
+
if (cleaned !== void 0) result.push(cleaned);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
if (data && typeof data === "object" && data.constructor === Object) {
|
|
132
|
+
const result = {};
|
|
133
|
+
const obj = data;
|
|
134
|
+
const keys = Object.keys(obj);
|
|
135
|
+
for (let i = 0; i < keys.length; i++) {
|
|
136
|
+
const key = keys[i];
|
|
137
|
+
const cleaned = clean(obj[key]);
|
|
138
|
+
if (cleaned !== void 0) result[key] = cleaned;
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
};
|
|
144
|
+
const isValidEmail = (email) => {
|
|
145
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
146
|
+
};
|
|
147
|
+
const parseRecipientString = (input) => {
|
|
148
|
+
const trimmed = input.trim();
|
|
149
|
+
const match = trimmed.match(/^([^<]*)<([^>]*)>$/);
|
|
150
|
+
if (match) {
|
|
151
|
+
const [, name, email] = match;
|
|
152
|
+
if (!email?.trim() || !isValidEmail(email.trim())) return void 0;
|
|
153
|
+
return {
|
|
154
|
+
email: email.trim(),
|
|
155
|
+
name: name?.trim()
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (!isValidEmail(trimmed)) return void 0;
|
|
159
|
+
return { email: trimmed };
|
|
160
|
+
};
|
|
161
|
+
const parseRecipient = (recipient) => {
|
|
162
|
+
if (typeof recipient === "string") return parseRecipientString(recipient);
|
|
163
|
+
if (!recipient?.email || !isValidEmail(recipient.email)) return void 0;
|
|
164
|
+
return {
|
|
165
|
+
email: recipient.email,
|
|
166
|
+
name: recipient.name
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
const parseArrayRecipients = (recipients) => {
|
|
170
|
+
if (!recipients) return void 0;
|
|
171
|
+
const filtered = (typeof recipients === "string" ? [parseRecipientString(recipients)] : Array.isArray(recipients) ? recipients.map(parseRecipient) : [recipients]).filter((recipient) => Boolean(recipient));
|
|
172
|
+
return filtered.length > 0 ? filtered : void 0;
|
|
173
|
+
};
|
|
174
|
+
const stripPemHeaders = (pem) => pem.replace(/-----[^-]+-----|\s|#.*$/gm, "");
|
|
175
|
+
const RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
176
|
+
"authentication-results",
|
|
177
|
+
"bcc",
|
|
178
|
+
"cc",
|
|
179
|
+
"content-transfer-encoding",
|
|
180
|
+
"content-type",
|
|
181
|
+
"dkim-signature",
|
|
182
|
+
"from",
|
|
183
|
+
"message-id",
|
|
184
|
+
"received",
|
|
185
|
+
"reply-to",
|
|
186
|
+
"subject",
|
|
187
|
+
"to"
|
|
188
|
+
]);
|
|
189
|
+
const MAX_ATTACHMENTS = 1e3;
|
|
190
|
+
const MAX_PERSONALIZATIONS = 1e3;
|
|
191
|
+
const MAX_RECIPIENTS = 1e3;
|
|
192
|
+
const MAX_CAMPAIGN_ID_LENGTH = 48;
|
|
193
|
+
const getRecipientCount = (recipients) => recipients?.length || 0;
|
|
194
|
+
const isValidTemplateValue = (value) => {
|
|
195
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") return true;
|
|
196
|
+
if (Array.isArray(value)) return value.every(isValidTemplateValue);
|
|
197
|
+
if (value !== null && typeof value === "object") return Object.values(value).every(isValidTemplateValue);
|
|
198
|
+
return false;
|
|
199
|
+
};
|
|
200
|
+
const validateTemplateData = (data, label) => {
|
|
201
|
+
if (data === void 0) return null;
|
|
202
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") return `${label} template data must be a key/value object.`;
|
|
203
|
+
for (const [key, value] of Object.entries(data)) if (!isValidTemplateValue(value)) return `${label} template data key '${key}' has an invalid value. Values must be strings, booleans, numbers, lists, or maps.`;
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
const validateHeaderMap = (headers, label) => {
|
|
207
|
+
if (!headers) return null;
|
|
208
|
+
for (const [headerName, headerValue] of Object.entries(headers)) {
|
|
209
|
+
if (typeof headerValue !== "string") return `${label} header '${headerName}' must have a string value.`;
|
|
210
|
+
if (RESERVED_HEADER_NAMES.has(headerName.toLowerCase())) return `${label} headers cannot include the reserved header '${headerName}'.`;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
};
|
|
214
|
+
const validateSendDkim = (dkim, label) => {
|
|
215
|
+
if (!dkim) return null;
|
|
216
|
+
if (dkim.domain && !dkim.selector) return `${label} DKIM domain requires a selector.`;
|
|
217
|
+
if (dkim.privateKey && (!dkim.domain || !dkim.selector)) return `${label} DKIM privateKey requires both a domain and selector.`;
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
const mapDkim = (dkim) => ({
|
|
221
|
+
dkim_domain: dkim?.domain,
|
|
222
|
+
dkim_private_key: dkim?.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
|
|
223
|
+
dkim_selector: dkim?.selector
|
|
224
|
+
});
|
|
225
|
+
const mapAttachment = (attachment) => ({
|
|
226
|
+
content: attachment.content,
|
|
227
|
+
filename: attachment.filename,
|
|
228
|
+
type: attachment.type,
|
|
229
|
+
content_id: attachment.contentId
|
|
230
|
+
});
|
|
231
|
+
const mapPersonalization = (personalization, index, rootTemplateData) => {
|
|
232
|
+
const to = parseArrayRecipients(personalization.to);
|
|
233
|
+
if (!to || !to.length) return `Personalization at index ${index} must include at least one recipient in the 'to' field.`;
|
|
234
|
+
if (to.length > MAX_RECIPIENTS) return `Personalization at index ${index} cannot include more than ${MAX_RECIPIENTS} 'to' recipients.`;
|
|
235
|
+
const cc = parseArrayRecipients(personalization.cc);
|
|
236
|
+
if (cc && cc.length > MAX_RECIPIENTS) return `Personalization at index ${index} cannot include more than ${MAX_RECIPIENTS} 'cc' recipients.`;
|
|
237
|
+
const bcc = parseArrayRecipients(personalization.bcc);
|
|
238
|
+
if (bcc && bcc.length > MAX_RECIPIENTS) return `Personalization at index ${index} cannot include more than ${MAX_RECIPIENTS} 'bcc' recipients.`;
|
|
239
|
+
const headerError = validateHeaderMap(personalization.headers, `Personalization at index ${index}`);
|
|
240
|
+
if (headerError) return headerError;
|
|
241
|
+
const dkimError = validateSendDkim(personalization.dkim, `Personalization at index ${index}`);
|
|
242
|
+
if (dkimError) return dkimError;
|
|
243
|
+
const templateDataError = validateTemplateData(personalization.template?.data, `Personalization at index ${index}`);
|
|
244
|
+
if (templateDataError) return templateDataError;
|
|
245
|
+
const dynamic_template_data = rootTemplateData || personalization.template?.data ? {
|
|
246
|
+
...rootTemplateData,
|
|
247
|
+
...personalization.template?.data
|
|
248
|
+
} : void 0;
|
|
249
|
+
return {
|
|
250
|
+
bcc,
|
|
251
|
+
cc,
|
|
252
|
+
...mapDkim(personalization.dkim),
|
|
253
|
+
dynamic_template_data,
|
|
254
|
+
envelope_from: parseRecipient(personalization.envelopeFrom),
|
|
255
|
+
from: parseRecipient(personalization.from),
|
|
256
|
+
headers: personalization.headers,
|
|
257
|
+
reply_to: parseRecipient(personalization.replyTo),
|
|
258
|
+
subject: personalization.subject,
|
|
259
|
+
to
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
const resolveAttachments = async (attachments) => {
|
|
263
|
+
if (!attachments) return;
|
|
264
|
+
return Promise.all(attachments.map((a) => Promise.resolve(a))).catch((e) => e.message);
|
|
265
|
+
};
|
|
266
|
+
const buildSendPayload = async (options) => {
|
|
267
|
+
const { from, html, text } = options;
|
|
268
|
+
const contentTypes = options.content ? new Set(options.content.map((item) => item.type.toLowerCase())) : void 0;
|
|
269
|
+
const parsedFrom = parseRecipient(from);
|
|
270
|
+
if (!parsedFrom || !parsedFrom.email) return "No sender provided. Use the 'from' option to specify a sender.";
|
|
271
|
+
if (!options.subject || typeof options.subject !== "string" || !options.subject.trim()) return "No subject provided. Use the 'subject' option to specify a subject.";
|
|
272
|
+
if (!text && !html && (!options.content || !options.content.length)) return "No email content provided";
|
|
273
|
+
if (html && contentTypes?.has("text/html")) return "Cannot provide both 'html' and a 'content' entry with type 'text/html'.";
|
|
274
|
+
if (text && contentTypes?.has("text/plain")) return "Cannot provide both 'text' and a 'content' entry with type 'text/plain'.";
|
|
275
|
+
if (options.attachments && options.attachments.length > MAX_ATTACHMENTS) return `The maximum number of attachments is ${MAX_ATTACHMENTS}.`;
|
|
276
|
+
if (options.campaignId && (options.campaignId.length > MAX_CAMPAIGN_ID_LENGTH || /\s/.test(options.campaignId))) return `campaignId must be ${MAX_CAMPAIGN_ID_LENGTH} characters or fewer and must not contain spaces.`;
|
|
277
|
+
const rootHeaderError = validateHeaderMap(options.headers, "Root");
|
|
278
|
+
if (rootHeaderError) return rootHeaderError;
|
|
279
|
+
const rootDkimError = validateSendDkim(options.dkim, "Root");
|
|
280
|
+
if (rootDkimError) return rootDkimError;
|
|
281
|
+
const rootTemplateDataError = validateTemplateData(options.template?.data, "Root");
|
|
282
|
+
if (rootTemplateDataError) return rootTemplateDataError;
|
|
283
|
+
if (options.personalizations?.some((p) => p.template?.data !== void 0) && !options.template?.type) return "A root template type is required when using per-personalization template data.";
|
|
284
|
+
let personalizations;
|
|
285
|
+
if (options.personalizations) {
|
|
286
|
+
if (!options.personalizations.length) return "At least one personalization must be provided.";
|
|
287
|
+
if (options.personalizations.length > MAX_PERSONALIZATIONS) return `The maximum number of personalizations is ${MAX_PERSONALIZATIONS}.`;
|
|
288
|
+
personalizations = [];
|
|
289
|
+
for (let index = 0; index < options.personalizations.length; index++) {
|
|
290
|
+
const mappedPersonalization = mapPersonalization(options.personalizations[index], index, options.template?.data);
|
|
291
|
+
if (typeof mappedPersonalization === "string") return mappedPersonalization;
|
|
292
|
+
personalizations.push(mappedPersonalization);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const parsedTo = parseArrayRecipients(options.to);
|
|
296
|
+
if (!parsedTo || !parsedTo.length) return "No recipients provided. Use the 'to' option to specify at least one recipient";
|
|
297
|
+
if (parsedTo.length > MAX_RECIPIENTS) return `The maximum number of 'to' recipients is ${MAX_RECIPIENTS}.`;
|
|
298
|
+
const parsedCc = parseArrayRecipients(options.cc);
|
|
299
|
+
if (parsedCc && parsedCc.length > MAX_RECIPIENTS) return `The maximum number of 'cc' recipients is ${MAX_RECIPIENTS}.`;
|
|
300
|
+
const parsedBcc = parseArrayRecipients(options.bcc);
|
|
301
|
+
if (parsedBcc && parsedBcc.length > MAX_RECIPIENTS) return `The maximum number of 'bcc' recipients is ${MAX_RECIPIENTS}.`;
|
|
302
|
+
personalizations = [{
|
|
303
|
+
bcc: parsedBcc,
|
|
304
|
+
cc: parsedCc,
|
|
305
|
+
dynamic_template_data: options.template?.data,
|
|
306
|
+
to: parsedTo
|
|
307
|
+
}];
|
|
308
|
+
}
|
|
309
|
+
if (options.transactional === false) {
|
|
310
|
+
if (!personalizations.every((personalization) => {
|
|
311
|
+
const personalizationDkim = personalization.dkim_selector;
|
|
312
|
+
const rootDkim = options.dkim?.selector;
|
|
313
|
+
return Boolean(personalizationDkim || rootDkim);
|
|
314
|
+
})) return "Non-transactional messages must be DKIM signed.";
|
|
315
|
+
if (personalizations.some((personalization) => {
|
|
316
|
+
return getRecipientCount(personalization.to) + getRecipientCount(personalization.cc) + getRecipientCount(personalization.bcc) !== 1;
|
|
317
|
+
})) return "Non-transactional messages must have exactly one recipient per personalization.";
|
|
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
|
+
}
|
|
331
|
+
const content = [];
|
|
332
|
+
const template_type = options.template?.type;
|
|
333
|
+
if (text) content.push({
|
|
334
|
+
type: "text/plain",
|
|
335
|
+
value: text,
|
|
336
|
+
template_type
|
|
337
|
+
});
|
|
338
|
+
if (html) content.push({
|
|
339
|
+
type: "text/html",
|
|
340
|
+
value: html,
|
|
341
|
+
template_type
|
|
342
|
+
});
|
|
343
|
+
if (options.content) for (const item of options.content) content.push({
|
|
344
|
+
type: item.type,
|
|
345
|
+
value: item.value,
|
|
346
|
+
template_type
|
|
347
|
+
});
|
|
348
|
+
const resolvedAttachments = await resolveAttachments(options.attachments);
|
|
349
|
+
if (typeof resolvedAttachments === "string") return resolvedAttachments;
|
|
350
|
+
return {
|
|
351
|
+
attachments: resolvedAttachments?.map((a) => mapAttachment(a)),
|
|
352
|
+
campaign_id: options.campaignId,
|
|
353
|
+
...mapDkim(options.dkim),
|
|
354
|
+
envelope_from: parseRecipient(options.envelopeFrom),
|
|
355
|
+
personalizations,
|
|
356
|
+
headers: options.headers,
|
|
357
|
+
reply_to: parseRecipient(options.replyTo),
|
|
358
|
+
from: parsedFrom,
|
|
359
|
+
subject: options.subject,
|
|
360
|
+
content,
|
|
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 }
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
var Emails = class {
|
|
376
|
+
mailchannels;
|
|
377
|
+
constructor(mailchannels) {
|
|
378
|
+
this.mailchannels = mailchannels;
|
|
379
|
+
}
|
|
380
|
+
async _sendEmail(options, flags) {
|
|
381
|
+
let error = null;
|
|
382
|
+
const payload = await buildSendPayload(options);
|
|
383
|
+
if (typeof payload === "string") {
|
|
384
|
+
error = createValidationError(payload);
|
|
385
|
+
return {
|
|
386
|
+
data: null,
|
|
387
|
+
error
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const endpoint = flags.async ? "/tx/v1/send-async" : "/tx/v1/send";
|
|
391
|
+
const response = await this.mailchannels.post(endpoint, {
|
|
392
|
+
query: { "dry-run": flags.dryRun },
|
|
393
|
+
body: payload,
|
|
394
|
+
onResponseError: async ({ response }) => {
|
|
395
|
+
error = getStatusError(response, {
|
|
396
|
+
[400]: "Bad Request.",
|
|
397
|
+
[403]: "User does not have access to this feature.",
|
|
398
|
+
[413]: "The total message size should not exceed 30MB. This includes the message itself, headers, and the combined size of any attachments."
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}).catch((e) => {
|
|
402
|
+
error ||= getResultError(e, flags.async ? "Failed to queue email." : "Failed to send email.");
|
|
403
|
+
return null;
|
|
404
|
+
});
|
|
405
|
+
if (!response) return {
|
|
406
|
+
data: null,
|
|
407
|
+
error
|
|
408
|
+
};
|
|
409
|
+
if (flags.async) {
|
|
410
|
+
const asyncResponse = response;
|
|
411
|
+
return {
|
|
412
|
+
data: clean({
|
|
413
|
+
queuedAt: asyncResponse.queued_at,
|
|
414
|
+
requestId: asyncResponse.request_id
|
|
415
|
+
}),
|
|
416
|
+
error: null
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const syncResponse = response;
|
|
420
|
+
return {
|
|
421
|
+
data: clean({
|
|
422
|
+
rendered: syncResponse.data,
|
|
423
|
+
requestId: syncResponse.request_id,
|
|
424
|
+
results: syncResponse.results?.map((result) => ({
|
|
425
|
+
index: result.index,
|
|
426
|
+
messageId: result.message_id,
|
|
427
|
+
reason: result.reason,
|
|
428
|
+
status: result.status
|
|
429
|
+
}))
|
|
430
|
+
}),
|
|
431
|
+
error: null
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async send(options, dryRun = false) {
|
|
435
|
+
return this._sendEmail(options, { dryRun });
|
|
436
|
+
}
|
|
437
|
+
async queue(options) {
|
|
438
|
+
return this._sendEmail(options, { async: true });
|
|
439
|
+
}
|
|
440
|
+
async sendAsync(options) {
|
|
441
|
+
return this.queue(options);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const mapDkimKey = (key) => ({
|
|
445
|
+
algorithm: key.algorithm,
|
|
446
|
+
createdAt: key.created_at,
|
|
447
|
+
dnsRecords: key.dkim_dns_records,
|
|
448
|
+
domain: key.domain,
|
|
449
|
+
gracePeriodExpiresAt: key.gracePeriodExpiresAt,
|
|
450
|
+
length: key.key_length,
|
|
451
|
+
publicKey: key.public_key,
|
|
452
|
+
retiresAt: key.retiresAt,
|
|
453
|
+
selector: key.selector,
|
|
454
|
+
status: key.status,
|
|
455
|
+
statusModifiedAt: key.status_modified_at
|
|
456
|
+
});
|
|
457
|
+
var DomainsDkim = class {
|
|
458
|
+
mailchannels;
|
|
459
|
+
constructor(mailchannels) {
|
|
460
|
+
this.mailchannels = mailchannels;
|
|
461
|
+
}
|
|
462
|
+
async create(domain, options) {
|
|
463
|
+
let error = null;
|
|
464
|
+
if (!domain) {
|
|
465
|
+
error = createValidationError("No domain provided.");
|
|
466
|
+
return {
|
|
467
|
+
data: null,
|
|
468
|
+
error
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (!options.selector || options.selector.length > 63) {
|
|
472
|
+
error = createValidationError("Selector must be between 1 and 63 characters.");
|
|
473
|
+
return {
|
|
474
|
+
data: null,
|
|
475
|
+
error
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const payload = {
|
|
479
|
+
algorithm: options.algorithm,
|
|
480
|
+
key_length: options.length,
|
|
481
|
+
selector: options.selector
|
|
482
|
+
};
|
|
483
|
+
const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
|
|
484
|
+
body: payload,
|
|
485
|
+
onResponseError: async ({ response }) => {
|
|
486
|
+
error = getStatusError(response, {
|
|
487
|
+
[400]: "Bad Request.",
|
|
488
|
+
[409]: "Key pair already created for domain, and selector."
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}).catch((e) => {
|
|
492
|
+
error ||= getResultError(e, "Failed to create DKIM key.");
|
|
493
|
+
return null;
|
|
494
|
+
});
|
|
495
|
+
if (!response) return {
|
|
496
|
+
data: null,
|
|
497
|
+
error
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
data: clean(mapDkimKey(response)),
|
|
501
|
+
error: null
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async list(domain, options) {
|
|
505
|
+
let error = null;
|
|
506
|
+
if (!domain) {
|
|
507
|
+
error = createValidationError("No domain provided.");
|
|
508
|
+
return {
|
|
509
|
+
data: null,
|
|
510
|
+
error
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (options?.selector && options.selector.length > 63) {
|
|
514
|
+
error = createValidationError("Selector must be between 1 and 63 characters.");
|
|
515
|
+
return {
|
|
516
|
+
data: null,
|
|
517
|
+
error
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
error = validatePagination({
|
|
521
|
+
...options,
|
|
522
|
+
max: 100
|
|
523
|
+
});
|
|
524
|
+
if (error) return {
|
|
525
|
+
data: null,
|
|
526
|
+
error
|
|
527
|
+
};
|
|
528
|
+
const payload = {
|
|
529
|
+
selector: options?.selector,
|
|
530
|
+
status: options?.status,
|
|
531
|
+
offset: options?.offset,
|
|
532
|
+
limit: options?.limit,
|
|
533
|
+
include_dns_record: options?.includeDnsRecord
|
|
534
|
+
};
|
|
535
|
+
const response = await this.mailchannels.get(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
|
|
536
|
+
query: payload,
|
|
537
|
+
onResponseError: async ({ response }) => {
|
|
538
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
539
|
+
}
|
|
540
|
+
}).catch((e) => {
|
|
541
|
+
error ||= getResultError(e, "Failed to fetch DKIM keys.");
|
|
542
|
+
return null;
|
|
543
|
+
});
|
|
544
|
+
if (!response) return {
|
|
545
|
+
data: null,
|
|
546
|
+
error
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
data: clean(response.keys.map(mapDkimKey)),
|
|
550
|
+
error: null
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async updateStatus(domain, options) {
|
|
554
|
+
let error = null;
|
|
555
|
+
if (!domain) {
|
|
556
|
+
error = createValidationError("No domain provided.");
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
error
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (!options.selector || options.selector.length > 63) {
|
|
563
|
+
error = createValidationError("Selector must be between 1 and 63 characters.");
|
|
564
|
+
return {
|
|
565
|
+
success: false,
|
|
566
|
+
error
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const payload = { status: options.status };
|
|
570
|
+
await this.mailchannels.patch(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(options.selector)}`, {
|
|
571
|
+
body: payload,
|
|
572
|
+
onResponseError: async ({ response }) => {
|
|
573
|
+
error = getStatusError(response, {
|
|
574
|
+
[400]: "Bad Request.",
|
|
575
|
+
[404]: "Specified key pair not found, or no active key for rotation. This may also occur if the DKIM domain or selector path parameter is missing."
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}).catch((e) => {
|
|
579
|
+
error ||= getResultError(e, "Failed to update status of DKIM key.");
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
success: !error,
|
|
583
|
+
error
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async rotate(domain, selector, options) {
|
|
587
|
+
let error = null;
|
|
588
|
+
if (!domain) {
|
|
589
|
+
error = createValidationError("No domain provided.");
|
|
590
|
+
return {
|
|
591
|
+
data: null,
|
|
592
|
+
error
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (!selector || selector.length > 63) {
|
|
596
|
+
error = createValidationError("Selector must be between 1 and 63 characters.");
|
|
597
|
+
return {
|
|
598
|
+
data: null,
|
|
599
|
+
error
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (!options.newKey.selector || options.newKey.selector.length > 63) {
|
|
603
|
+
error = createValidationError("New key selector must be between 1 and 63 characters.");
|
|
604
|
+
return {
|
|
605
|
+
data: null,
|
|
606
|
+
error
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const payload = { new_key: { selector: options.newKey.selector } };
|
|
610
|
+
const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(selector)}/rotate`, {
|
|
611
|
+
body: payload,
|
|
612
|
+
onResponseError: async ({ response }) => {
|
|
613
|
+
error = getStatusError(response, {
|
|
614
|
+
[400]: "Bad Request.",
|
|
615
|
+
[404]: "Specified key pair not found.",
|
|
616
|
+
[409]: "Key pair already created for domain, and provided new key selector."
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}).catch((e) => {
|
|
620
|
+
error ||= getResultError(e, "Failed to rotate DKIM key.");
|
|
621
|
+
return null;
|
|
622
|
+
});
|
|
623
|
+
if (!response) return {
|
|
624
|
+
data: null,
|
|
625
|
+
error
|
|
626
|
+
};
|
|
627
|
+
return {
|
|
628
|
+
data: clean({
|
|
629
|
+
new: mapDkimKey(response.new_key),
|
|
630
|
+
rotated: mapDkimKey(response.rotated_key)
|
|
631
|
+
}),
|
|
632
|
+
error: null
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var DomainsCustomTracking = class DomainsCustomTracking {
|
|
637
|
+
mailchannels;
|
|
638
|
+
static SCOPE_VALUES = /* @__PURE__ */ new Set([
|
|
639
|
+
"click",
|
|
640
|
+
"open",
|
|
641
|
+
"unsubscribe"
|
|
642
|
+
]);
|
|
643
|
+
constructor(mailchannels) {
|
|
644
|
+
this.mailchannels = mailchannels;
|
|
645
|
+
}
|
|
646
|
+
async list(options) {
|
|
647
|
+
let error = null;
|
|
648
|
+
error = validatePagination({
|
|
649
|
+
...options,
|
|
650
|
+
max: 1e3
|
|
651
|
+
});
|
|
652
|
+
if (error) return {
|
|
653
|
+
data: null,
|
|
654
|
+
error
|
|
655
|
+
};
|
|
656
|
+
const response = await this.mailchannels.get("/tx/v1/custom-tracking-domains", {
|
|
657
|
+
query: options,
|
|
658
|
+
onResponseError: async ({ response }) => {
|
|
659
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
660
|
+
}
|
|
661
|
+
}).catch((e) => {
|
|
662
|
+
error ||= getResultError(e, "Failed to fetch custom tracking domains.");
|
|
663
|
+
return null;
|
|
664
|
+
});
|
|
665
|
+
if (!response) return {
|
|
666
|
+
data: null,
|
|
667
|
+
error
|
|
668
|
+
};
|
|
669
|
+
return {
|
|
670
|
+
data: clean({
|
|
671
|
+
customTrackingDomains: response.custom_tracking_domains.map((d) => ({
|
|
672
|
+
name: d.name,
|
|
673
|
+
hostname: d.hostname,
|
|
674
|
+
scope: d.scope,
|
|
675
|
+
status: d.status,
|
|
676
|
+
createdAt: d.created_at
|
|
677
|
+
})),
|
|
678
|
+
total: response.total
|
|
679
|
+
}),
|
|
680
|
+
error: null
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async create(name, hostname, scope) {
|
|
684
|
+
let error = null;
|
|
685
|
+
error = validateCustomTrackingName(name);
|
|
686
|
+
if (error) return {
|
|
687
|
+
data: null,
|
|
688
|
+
error
|
|
689
|
+
};
|
|
690
|
+
if (!hostname) {
|
|
691
|
+
error = createValidationError("Hostname is required.");
|
|
692
|
+
return {
|
|
693
|
+
data: null,
|
|
694
|
+
error
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
698
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
699
|
+
return {
|
|
700
|
+
data: null,
|
|
701
|
+
error
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const payload = {
|
|
705
|
+
name,
|
|
706
|
+
hostname,
|
|
707
|
+
scope
|
|
708
|
+
};
|
|
709
|
+
let statusCode = null;
|
|
710
|
+
const response = await this.mailchannels.post("/tx/v1/custom-tracking-domains", {
|
|
711
|
+
body: payload,
|
|
712
|
+
onResponse: async ({ response }) => {
|
|
713
|
+
statusCode = response.status;
|
|
714
|
+
},
|
|
715
|
+
onResponseError: async ({ response }) => {
|
|
716
|
+
error = getStatusError(response, {
|
|
717
|
+
[400]: "Invalid request body.",
|
|
718
|
+
[403]: "No permission to register this domain.",
|
|
719
|
+
[409]: "A domain with the same name already exists, or the hostname and scope combination is already registered.",
|
|
720
|
+
[422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}).catch((e) => {
|
|
724
|
+
error ||= getResultError(e, "Failed to create custom tracking domain.");
|
|
725
|
+
return null;
|
|
726
|
+
});
|
|
727
|
+
if (!response) return {
|
|
728
|
+
data: null,
|
|
729
|
+
error
|
|
730
|
+
};
|
|
731
|
+
if (statusCode === 202) {
|
|
732
|
+
const dnsSetupRequiredResponse = response;
|
|
733
|
+
return {
|
|
734
|
+
data: clean({
|
|
735
|
+
dnsSetupRequired: true,
|
|
736
|
+
token: dnsSetupRequiredResponse.token,
|
|
737
|
+
txtRecordName: dnsSetupRequiredResponse.txt_record_name,
|
|
738
|
+
txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
|
|
739
|
+
instructions: dnsSetupRequiredResponse.instructions
|
|
740
|
+
}),
|
|
741
|
+
error: null
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const createResponse = response;
|
|
745
|
+
return {
|
|
746
|
+
data: clean({
|
|
747
|
+
dnsSetupRequired: false,
|
|
748
|
+
name: createResponse.name,
|
|
749
|
+
hostname: createResponse.hostname,
|
|
750
|
+
scope: createResponse.scope,
|
|
751
|
+
status: createResponse.status,
|
|
752
|
+
createdAt: createResponse.created_at
|
|
753
|
+
}),
|
|
754
|
+
error: null
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async update(hostname, scope, options) {
|
|
758
|
+
let error = null;
|
|
759
|
+
if (!hostname) {
|
|
760
|
+
error = createValidationError("Hostname is required.");
|
|
761
|
+
return {
|
|
762
|
+
data: null,
|
|
763
|
+
error
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
767
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
768
|
+
return {
|
|
769
|
+
data: null,
|
|
770
|
+
error
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (options.name !== void 0) {
|
|
774
|
+
error = validateCustomTrackingName(options.name);
|
|
775
|
+
if (error) return {
|
|
776
|
+
data: null,
|
|
777
|
+
error
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (options.name === void 0 && options.status === void 0) {
|
|
781
|
+
error = createValidationError("At least one of 'name' or 'status' must be provided.");
|
|
782
|
+
return {
|
|
783
|
+
data: null,
|
|
784
|
+
error
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const payload = {
|
|
788
|
+
name: options.name,
|
|
789
|
+
status: options.status
|
|
790
|
+
};
|
|
791
|
+
let statusCode = null;
|
|
792
|
+
const response = await this.mailchannels.patch(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, {
|
|
793
|
+
body: payload,
|
|
794
|
+
onResponse: async ({ response }) => {
|
|
795
|
+
statusCode = response.status;
|
|
796
|
+
},
|
|
797
|
+
onResponseError: async ({ response }) => {
|
|
798
|
+
error = getStatusError(response, {
|
|
799
|
+
[400]: "Bad Request.",
|
|
800
|
+
[403]: "No permission to update this domain.",
|
|
801
|
+
[404]: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.`,
|
|
802
|
+
[409]: "Name already used by another domain.",
|
|
803
|
+
[422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}).catch((e) => {
|
|
807
|
+
error ||= getResultError(e, "Failed to update custom tracking domain.");
|
|
808
|
+
return null;
|
|
809
|
+
});
|
|
810
|
+
if (!response) return {
|
|
811
|
+
data: null,
|
|
812
|
+
error
|
|
813
|
+
};
|
|
814
|
+
if (statusCode === 202) {
|
|
815
|
+
const dnsSetupRequiredResponse = response;
|
|
816
|
+
return {
|
|
817
|
+
data: clean({
|
|
818
|
+
dnsSetupRequired: true,
|
|
819
|
+
token: dnsSetupRequiredResponse.token,
|
|
820
|
+
txtRecordName: dnsSetupRequiredResponse.txt_record_name,
|
|
821
|
+
txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
|
|
822
|
+
instructions: dnsSetupRequiredResponse.instructions
|
|
823
|
+
}),
|
|
824
|
+
error: null
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const updateResponse = response;
|
|
828
|
+
return {
|
|
829
|
+
data: clean({
|
|
830
|
+
dnsSetupRequired: false,
|
|
831
|
+
name: updateResponse.name,
|
|
832
|
+
hostname: updateResponse.hostname,
|
|
833
|
+
scope: updateResponse.scope,
|
|
834
|
+
status: updateResponse.status,
|
|
835
|
+
createdAt: updateResponse.created_at
|
|
836
|
+
}),
|
|
837
|
+
error: null
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async delete(hostname, scope) {
|
|
841
|
+
let error = null;
|
|
842
|
+
if (!hostname) {
|
|
843
|
+
error = createValidationError("Hostname is required.");
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
error
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
|
|
850
|
+
error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
await this.mailchannels.delete(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, { onResponseError: async ({ response }) => {
|
|
857
|
+
error = getStatusError(response, { [400]: "Invalid hostname or scope value" });
|
|
858
|
+
} }).catch((e) => {
|
|
859
|
+
error ||= getResultError(e, "Failed to delete custom tracking domain.");
|
|
860
|
+
});
|
|
861
|
+
return {
|
|
862
|
+
success: !error,
|
|
863
|
+
error
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
};
|
|
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;
|
|
1153
|
+
});
|
|
1154
|
+
if (!response) return {
|
|
1155
|
+
data: null,
|
|
1156
|
+
error
|
|
1157
|
+
};
|
|
1158
|
+
return {
|
|
1159
|
+
data: clean({
|
|
1160
|
+
allPassed: response.all_passed,
|
|
1161
|
+
results: response.results
|
|
1162
|
+
}),
|
|
1163
|
+
error: null
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
static async verify(options) {
|
|
1167
|
+
let error = null;
|
|
1168
|
+
if (!await isValidWebhook(options).catch(() => false)) {
|
|
1169
|
+
error = createValidationError("Invalid webhook signature.");
|
|
1170
|
+
return {
|
|
1171
|
+
data: null,
|
|
1172
|
+
error
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
const payload = JSON.parse(options.payload);
|
|
1177
|
+
if (!Array.isArray(payload)) {
|
|
1178
|
+
error = createValidationError("Invalid webhook payload.");
|
|
1179
|
+
return {
|
|
1180
|
+
data: null,
|
|
1181
|
+
error
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
data: clean(payload.map((event) => ({
|
|
1186
|
+
email: event.email,
|
|
1187
|
+
customerHandle: event.customer_handle,
|
|
1188
|
+
timestamp: event.timestamp,
|
|
1189
|
+
smtpId: event.smtp_id,
|
|
1190
|
+
event: event.event,
|
|
1191
|
+
requestId: event.request_id,
|
|
1192
|
+
campaignId: "campaign_id" in event ? event.campaign_id : void 0,
|
|
1193
|
+
recipients: "recipients" in event ? event.recipients : void 0,
|
|
1194
|
+
userAgent: "user_agent" in event ? event.user_agent : void 0,
|
|
1195
|
+
ip: "ip" in event ? event.ip : void 0,
|
|
1196
|
+
url: "url" in event ? event.url : void 0,
|
|
1197
|
+
status: "status" in event ? event.status : void 0,
|
|
1198
|
+
reason: "reason" in event ? event.reason : void 0
|
|
1199
|
+
}))),
|
|
1200
|
+
error: null
|
|
1201
|
+
};
|
|
1202
|
+
} catch {
|
|
1203
|
+
error = createValidationError("Invalid webhook payload.");
|
|
1204
|
+
return {
|
|
1205
|
+
data: null,
|
|
1206
|
+
error
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async verify(options) {
|
|
1211
|
+
return Webhooks.verify(options);
|
|
1212
|
+
}
|
|
1213
|
+
async batches(options) {
|
|
1214
|
+
let error = null;
|
|
1215
|
+
error = validatePagination({
|
|
1216
|
+
...options,
|
|
1217
|
+
max: 500
|
|
1218
|
+
});
|
|
1219
|
+
if (error) return {
|
|
1220
|
+
data: null,
|
|
1221
|
+
error
|
|
1222
|
+
};
|
|
1223
|
+
if (options?.statuses && options.statuses.length > 6) return {
|
|
1224
|
+
data: null,
|
|
1225
|
+
error: createValidationError("A maximum of 6 status filters can be provided.")
|
|
1226
|
+
};
|
|
1227
|
+
if (options?.statuses && new Set(options.statuses).size !== options.statuses.length) return {
|
|
1228
|
+
data: null,
|
|
1229
|
+
error: createValidationError("Status filters must be unique.")
|
|
1230
|
+
};
|
|
1231
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1232
|
+
createdAfter: options?.createdAfter,
|
|
1233
|
+
createdBefore: options?.createdBefore
|
|
1234
|
+
});
|
|
1235
|
+
if (!dates || dateError) return {
|
|
1236
|
+
data: null,
|
|
1237
|
+
error: dateError
|
|
1238
|
+
};
|
|
1239
|
+
if (dates.createdAfter && dates.createdBefore) {
|
|
1240
|
+
const createdAfter = Date.parse(dates.createdAfter);
|
|
1241
|
+
const createdBefore = Date.parse(dates.createdBefore);
|
|
1242
|
+
const maxRangeMs = 26784e5;
|
|
1243
|
+
if (createdBefore <= createdAfter) return {
|
|
1244
|
+
data: null,
|
|
1245
|
+
error: createValidationError("createdBefore must be later than createdAfter.")
|
|
1246
|
+
};
|
|
1247
|
+
if (createdBefore - createdAfter > maxRangeMs) return {
|
|
1248
|
+
data: null,
|
|
1249
|
+
error: createValidationError("The time range between createdAfter and createdBefore must not exceed 31 days.")
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const response = await this.mailchannels.get("/tx/v1/webhook-batch", {
|
|
1253
|
+
query: {
|
|
1254
|
+
created_after: dates.createdAfter,
|
|
1255
|
+
created_before: dates.createdBefore,
|
|
1256
|
+
statuses: options?.statuses,
|
|
1257
|
+
webhook: options?.webhook,
|
|
1258
|
+
limit: options?.limit,
|
|
1259
|
+
offset: options?.offset
|
|
1260
|
+
},
|
|
1261
|
+
onResponseError: async ({ response }) => {
|
|
1262
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
1263
|
+
}
|
|
1264
|
+
}).catch((e) => {
|
|
1265
|
+
error ||= getResultError(e, "Failed to fetch webhook batches.");
|
|
1266
|
+
return null;
|
|
1267
|
+
});
|
|
1268
|
+
if (!response) return {
|
|
1269
|
+
data: null,
|
|
1270
|
+
error
|
|
1271
|
+
};
|
|
1272
|
+
return {
|
|
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
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
async resendBatch(batchId) {
|
|
1287
|
+
let error = null;
|
|
1288
|
+
const response = await this.mailchannels.post(`/tx/v1/webhook-batch/${encodeURIComponent(batchId)}/resend`, { onResponseError: async ({ response }) => {
|
|
1289
|
+
error = getStatusError(response, {
|
|
1290
|
+
[400]: "Bad Request. The batch ID is invalid.",
|
|
1291
|
+
[404]: `The batch '${batchId}' is not found for the customer.`
|
|
1292
|
+
});
|
|
1293
|
+
} }).catch((e) => {
|
|
1294
|
+
error ||= getResultError(e, "Failed to resend webhook batch.");
|
|
1295
|
+
return null;
|
|
1296
|
+
});
|
|
1297
|
+
if (!response) return {
|
|
1298
|
+
data: null,
|
|
1299
|
+
error
|
|
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;
|
|
1319
|
+
}
|
|
1320
|
+
async create(handle) {
|
|
1321
|
+
let error = null;
|
|
1322
|
+
if (!handle) {
|
|
1323
|
+
error = createValidationError("No handle provided.");
|
|
1324
|
+
return {
|
|
1325
|
+
data: null,
|
|
1326
|
+
error
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key`, { onResponseError: async ({ response }) => {
|
|
1330
|
+
error = getStatusError(response, {
|
|
1331
|
+
[403]: "You can't create API keys for this sub-account.",
|
|
1332
|
+
[404]: `Sub-account with handle '${handle}' not found.`,
|
|
1333
|
+
[422]: "You have reached the limit of API keys you can create for this sub-account."
|
|
1334
|
+
});
|
|
1335
|
+
} }).catch((e) => {
|
|
1336
|
+
error ||= getResultError(e, "Failed to create sub-account API key.");
|
|
1337
|
+
return null;
|
|
1338
|
+
});
|
|
1339
|
+
if (!response) return {
|
|
1340
|
+
data: null,
|
|
1341
|
+
error
|
|
1342
|
+
};
|
|
1343
|
+
return {
|
|
1344
|
+
data: clean(response),
|
|
1345
|
+
error: null
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
async list(handle, options) {
|
|
1349
|
+
let error = null;
|
|
1350
|
+
if (!handle) {
|
|
1351
|
+
error = createValidationError("No handle provided.");
|
|
1352
|
+
return {
|
|
1353
|
+
data: null,
|
|
1354
|
+
error
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
error = validatePagination({
|
|
1358
|
+
...options,
|
|
1359
|
+
max: 1e3
|
|
1360
|
+
});
|
|
1361
|
+
if (error) return {
|
|
1362
|
+
data: null,
|
|
1363
|
+
error
|
|
1364
|
+
};
|
|
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) => {
|
|
1371
|
+
error ||= getResultError(e, "Failed to fetch sub-account API keys.");
|
|
1372
|
+
return null;
|
|
1373
|
+
});
|
|
1374
|
+
if (!response) return {
|
|
1375
|
+
data: null,
|
|
1376
|
+
error
|
|
1377
|
+
};
|
|
1378
|
+
return {
|
|
1379
|
+
data: clean(response.map((key) => ({
|
|
1380
|
+
id: key.id,
|
|
1381
|
+
key: key.key
|
|
1382
|
+
}))),
|
|
1383
|
+
error: null
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
async delete(handle, id) {
|
|
1387
|
+
let error = null;
|
|
1388
|
+
if (!handle) {
|
|
1389
|
+
error = createValidationError("No handle provided.");
|
|
1390
|
+
return {
|
|
1391
|
+
success: false,
|
|
1392
|
+
error
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
|
|
1396
|
+
error = getStatusError(response, { [400]: "Missing or invalid API key ID." });
|
|
1397
|
+
} }).catch((e) => {
|
|
1398
|
+
error ||= getResultError(e, "Failed to delete sub-account API key.");
|
|
1399
|
+
});
|
|
1400
|
+
return {
|
|
1401
|
+
success: !error,
|
|
1402
|
+
error
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
var SubAccountsSmtpPasswords = class {
|
|
1407
|
+
mailchannels;
|
|
1408
|
+
constructor(mailchannels) {
|
|
1409
|
+
this.mailchannels = mailchannels;
|
|
1410
|
+
}
|
|
1411
|
+
async create(handle) {
|
|
1412
|
+
let error = null;
|
|
1413
|
+
if (!handle) {
|
|
1414
|
+
error = createValidationError("No handle provided.");
|
|
1415
|
+
return {
|
|
1416
|
+
data: null,
|
|
1417
|
+
error
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1421
|
+
error = getStatusError(response, {
|
|
1422
|
+
[403]: "You can't create SMTP passwords for this sub-account.",
|
|
1423
|
+
[404]: `Sub-account with handle '${handle}' not found.`,
|
|
1424
|
+
[422]: "You have reached the limit of SMTP passwords you can create for this sub-account."
|
|
1425
|
+
});
|
|
1426
|
+
} }).catch((e) => {
|
|
1427
|
+
error ||= getResultError(e, "Failed to create sub-account SMTP password.");
|
|
1428
|
+
return null;
|
|
1429
|
+
});
|
|
1430
|
+
if (!response) return {
|
|
1431
|
+
data: null,
|
|
1432
|
+
error
|
|
1433
|
+
};
|
|
1434
|
+
return {
|
|
1435
|
+
data: clean({
|
|
1436
|
+
enabled: response.enabled,
|
|
1437
|
+
id: response.id,
|
|
1438
|
+
smtpPassword: response.smtp_password
|
|
1439
|
+
}),
|
|
1440
|
+
error: null
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
async list(handle) {
|
|
1444
|
+
let error = null;
|
|
1445
|
+
if (!handle) {
|
|
1446
|
+
error = createValidationError("No handle provided.");
|
|
1447
|
+
return {
|
|
1448
|
+
data: null,
|
|
1449
|
+
error
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
|
|
1453
|
+
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1454
|
+
} }).catch((e) => {
|
|
1455
|
+
error ||= getResultError(e, "Failed to fetch sub-account SMTP passwords.");
|
|
1456
|
+
return null;
|
|
1457
|
+
});
|
|
1458
|
+
if (!response) return {
|
|
1459
|
+
data: null,
|
|
1460
|
+
error
|
|
1461
|
+
};
|
|
1462
|
+
return {
|
|
1463
|
+
data: clean(response.map((password) => ({
|
|
1464
|
+
enabled: password.enabled,
|
|
1465
|
+
id: password.id,
|
|
1466
|
+
smtpPassword: password.smtp_password
|
|
1467
|
+
}))),
|
|
1468
|
+
error: null
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
async delete(handle, id) {
|
|
1472
|
+
let error = null;
|
|
1473
|
+
if (!handle) {
|
|
1474
|
+
error = createValidationError("No handle provided.");
|
|
1475
|
+
return {
|
|
1476
|
+
success: false,
|
|
1477
|
+
error
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
|
|
1481
|
+
error = getStatusError(response, { [400]: "Missing or invalid SMTP password ID." });
|
|
1482
|
+
} }).catch((e) => {
|
|
1483
|
+
error ||= getResultError(e, "Failed to delete sub-account SMTP password.");
|
|
1484
|
+
});
|
|
1485
|
+
return {
|
|
1486
|
+
success: !error,
|
|
1487
|
+
error
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var SubAccountsLimits = class {
|
|
1492
|
+
mailchannels;
|
|
1493
|
+
constructor(mailchannels) {
|
|
1494
|
+
this.mailchannels = mailchannels;
|
|
1495
|
+
}
|
|
1496
|
+
async get(handle) {
|
|
1497
|
+
let error = null;
|
|
1498
|
+
if (!handle) {
|
|
1499
|
+
error = createValidationError("No handle provided.");
|
|
1500
|
+
return {
|
|
1501
|
+
data: null,
|
|
1502
|
+
error
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
|
|
1506
|
+
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1507
|
+
} }).catch((e) => {
|
|
1508
|
+
error ||= getResultError(e, "Failed to fetch sub-account limit.");
|
|
1509
|
+
return null;
|
|
1510
|
+
});
|
|
1511
|
+
if (!response) return {
|
|
1512
|
+
data: null,
|
|
1513
|
+
error
|
|
1514
|
+
};
|
|
1515
|
+
return {
|
|
1516
|
+
data: clean(response),
|
|
1517
|
+
error: null
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
async set(handle, options) {
|
|
1521
|
+
let error = null;
|
|
1522
|
+
if (!handle) {
|
|
1523
|
+
error = createValidationError("No handle provided.");
|
|
1524
|
+
return {
|
|
1525
|
+
success: false,
|
|
1526
|
+
error
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
if (options.sends < 0) {
|
|
1530
|
+
error = createValidationError("The sends value must be at least 0.");
|
|
1531
|
+
return {
|
|
1532
|
+
success: false,
|
|
1533
|
+
error
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
await this.mailchannels.put(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, {
|
|
1537
|
+
body: options,
|
|
1538
|
+
onResponseError: async ({ response }) => {
|
|
1539
|
+
error = getStatusError(response, {
|
|
1540
|
+
[400]: "Bad Request.",
|
|
1541
|
+
[404]: `Sub-account with handle '${handle}' not found.`
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}).catch((e) => {
|
|
1545
|
+
error ||= getResultError(e, "Failed to set sub-account limit.");
|
|
1546
|
+
});
|
|
1547
|
+
return {
|
|
1548
|
+
success: !error,
|
|
1549
|
+
error
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
async delete(handle) {
|
|
1553
|
+
let error = null;
|
|
1554
|
+
if (!handle) {
|
|
1555
|
+
error = createValidationError("No handle provided.");
|
|
1556
|
+
return {
|
|
1557
|
+
success: false,
|
|
1558
|
+
error
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
|
|
1562
|
+
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1563
|
+
} }).catch((e) => {
|
|
1564
|
+
error ||= getResultError(e, "Failed to delete sub-account limit.");
|
|
1565
|
+
});
|
|
1566
|
+
return {
|
|
1567
|
+
success: !error,
|
|
1568
|
+
error
|
|
1569
|
+
};
|
|
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
|
+
}
|
|
1723
|
+
async getUsage(handle) {
|
|
1724
|
+
let error = null;
|
|
1725
|
+
if (!handle) {
|
|
1726
|
+
error = createValidationError("No handle provided.");
|
|
1727
|
+
return {
|
|
1728
|
+
data: null,
|
|
1729
|
+
error
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/usage`, { onResponseError: async ({ response }) => {
|
|
1733
|
+
error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
|
|
1734
|
+
} }).catch((e) => {
|
|
1735
|
+
error ||= getResultError(e, "Failed to fetch sub-account usage.");
|
|
1736
|
+
return null;
|
|
1737
|
+
});
|
|
1738
|
+
if (!response) return {
|
|
1739
|
+
data: null,
|
|
1740
|
+
error
|
|
1741
|
+
};
|
|
1742
|
+
return {
|
|
1743
|
+
data: clean({
|
|
1744
|
+
endDate: response.period_end_date,
|
|
1745
|
+
startDate: response.period_start_date,
|
|
1746
|
+
total: response.total_usage,
|
|
1747
|
+
monthlyLimit: response.monthly_limit
|
|
1748
|
+
}),
|
|
1749
|
+
error: null
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
createApiKey(...args) {
|
|
1753
|
+
return this.apiKeys.create(...args);
|
|
1754
|
+
}
|
|
1755
|
+
listApiKeys(...args) {
|
|
1756
|
+
return this.apiKeys.list(...args);
|
|
1757
|
+
}
|
|
1758
|
+
deleteApiKey(...args) {
|
|
1759
|
+
return this.apiKeys.delete(...args);
|
|
1760
|
+
}
|
|
1761
|
+
createSmtpPassword(...args) {
|
|
1762
|
+
return this.smtpPasswords.create(...args);
|
|
1763
|
+
}
|
|
1764
|
+
listSmtpPasswords(...args) {
|
|
1765
|
+
return this.smtpPasswords.list(...args);
|
|
1766
|
+
}
|
|
1767
|
+
deleteSmtpPassword(...args) {
|
|
1768
|
+
return this.smtpPasswords.delete(...args);
|
|
1769
|
+
}
|
|
1770
|
+
setLimit(...args) {
|
|
1771
|
+
return this.limits.set(...args);
|
|
1772
|
+
}
|
|
1773
|
+
getLimit(...args) {
|
|
1774
|
+
return this.limits.get(...args);
|
|
1775
|
+
}
|
|
1776
|
+
deleteLimit(...args) {
|
|
1777
|
+
return this.limits.delete(...args);
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
const mapBucket = (bucket) => {
|
|
1781
|
+
return {
|
|
1782
|
+
count: bucket.count,
|
|
1783
|
+
periodStart: bucket.period_start
|
|
1784
|
+
};
|
|
1785
|
+
};
|
|
1786
|
+
var Metrics = class {
|
|
1787
|
+
mailchannels;
|
|
1788
|
+
constructor(mailchannels) {
|
|
1789
|
+
this.mailchannels = mailchannels;
|
|
1790
|
+
}
|
|
1791
|
+
async engagement(options) {
|
|
1792
|
+
let error = null;
|
|
1793
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1794
|
+
startTime: options?.startTime,
|
|
1795
|
+
endTime: options?.endTime
|
|
1796
|
+
});
|
|
1797
|
+
if (!dates || dateError) return {
|
|
1798
|
+
data: null,
|
|
1799
|
+
error: dateError
|
|
1800
|
+
};
|
|
1801
|
+
const response = await this.mailchannels.get("/tx/v1/metrics/engagement", {
|
|
1802
|
+
query: {
|
|
1803
|
+
start_time: dates.startTime,
|
|
1804
|
+
end_time: dates.endTime,
|
|
1805
|
+
campaign_id: options?.campaignId,
|
|
1806
|
+
interval: options?.interval
|
|
1807
|
+
},
|
|
1808
|
+
onResponseError: async ({ response }) => {
|
|
1809
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
1810
|
+
}
|
|
1811
|
+
}).catch((e) => {
|
|
1812
|
+
error ||= getResultError(e, "Failed to fetch engagement metrics.");
|
|
1813
|
+
return null;
|
|
1814
|
+
});
|
|
1815
|
+
if (!response) return {
|
|
1816
|
+
data: null,
|
|
1817
|
+
error
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
data: clean({
|
|
1821
|
+
buckets: {
|
|
1822
|
+
click: response.buckets.click.map(mapBucket),
|
|
1823
|
+
clickTrackingDelivered: response.buckets.click_tracking_delivered.map(mapBucket),
|
|
1824
|
+
open: response.buckets.open.map(mapBucket),
|
|
1825
|
+
openTrackingDelivered: response.buckets.open_tracking_delivered.map(mapBucket),
|
|
1826
|
+
uniqueClick: response.buckets.unique_click?.map(mapBucket),
|
|
1827
|
+
uniqueClickTrackingDelivered: response.buckets.unique_click_tracking_delivered?.map(mapBucket),
|
|
1828
|
+
uniqueOpen: response.buckets.unique_open?.map(mapBucket),
|
|
1829
|
+
uniqueOpenTrackingDelivered: response.buckets.unique_open_tracking_delivered?.map(mapBucket)
|
|
1830
|
+
},
|
|
1831
|
+
click: response.click,
|
|
1832
|
+
clickTrackingDelivered: response.click_tracking_delivered,
|
|
1833
|
+
endTime: response.end_time,
|
|
1834
|
+
open: response.open,
|
|
1835
|
+
openTrackingDelivered: response.open_tracking_delivered,
|
|
1836
|
+
startTime: response.start_time,
|
|
1837
|
+
uniqueClick: response.unique_click,
|
|
1838
|
+
uniqueClickTrackingDelivered: response.unique_click_tracking_delivered,
|
|
1839
|
+
uniqueOpen: response.unique_open,
|
|
1840
|
+
uniqueOpenTrackingDelivered: response.unique_open_tracking_delivered
|
|
1841
|
+
}),
|
|
1842
|
+
error: null
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
async performance(options) {
|
|
1846
|
+
let error = null;
|
|
1847
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1848
|
+
startTime: options?.startTime,
|
|
1849
|
+
endTime: options?.endTime
|
|
1850
|
+
});
|
|
1851
|
+
if (!dates || dateError) return {
|
|
1852
|
+
data: null,
|
|
1853
|
+
error: dateError
|
|
1854
|
+
};
|
|
1855
|
+
const response = await this.mailchannels.get("/tx/v1/metrics/performance", {
|
|
1856
|
+
query: {
|
|
1857
|
+
start_time: dates.startTime,
|
|
1858
|
+
end_time: dates.endTime,
|
|
1859
|
+
campaign_id: options?.campaignId,
|
|
1860
|
+
interval: options?.interval
|
|
1861
|
+
},
|
|
1862
|
+
onResponseError: async ({ response }) => {
|
|
1863
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
1864
|
+
}
|
|
1865
|
+
}).catch((e) => {
|
|
1866
|
+
error ||= getResultError(e, "Failed to fetch performance metrics.");
|
|
1867
|
+
return null;
|
|
1868
|
+
});
|
|
1869
|
+
if (!response) return {
|
|
1870
|
+
data: null,
|
|
1871
|
+
error
|
|
1872
|
+
};
|
|
1873
|
+
return {
|
|
1874
|
+
data: clean({
|
|
1875
|
+
bounced: response.bounced,
|
|
1876
|
+
complained: response.complained,
|
|
1877
|
+
buckets: {
|
|
1878
|
+
bounced: response.buckets.bounced.map(mapBucket),
|
|
1879
|
+
complained: response.buckets.complained.map(mapBucket),
|
|
1880
|
+
delivered: response.buckets.delivered.map(mapBucket),
|
|
1881
|
+
processed: response.buckets.processed.map(mapBucket)
|
|
1882
|
+
},
|
|
1883
|
+
delivered: response.delivered,
|
|
1884
|
+
endTime: response.end_time,
|
|
1885
|
+
processed: response.processed,
|
|
1886
|
+
startTime: response.start_time
|
|
1887
|
+
}),
|
|
1888
|
+
error: null
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
async recipientBehaviour(options) {
|
|
1892
|
+
let error = null;
|
|
1893
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1894
|
+
startTime: options?.startTime,
|
|
1895
|
+
endTime: options?.endTime
|
|
1896
|
+
});
|
|
1897
|
+
if (!dates || dateError) return {
|
|
1898
|
+
data: null,
|
|
1899
|
+
error: dateError
|
|
1900
|
+
};
|
|
1901
|
+
const response = await this.mailchannels.get("/tx/v1/metrics/recipient-behaviour", {
|
|
1902
|
+
query: {
|
|
1903
|
+
start_time: dates.startTime,
|
|
1904
|
+
end_time: dates.endTime,
|
|
1905
|
+
campaign_id: options?.campaignId,
|
|
1906
|
+
interval: options?.interval
|
|
1907
|
+
},
|
|
1908
|
+
onResponseError: async ({ response }) => {
|
|
1909
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
1910
|
+
}
|
|
1911
|
+
}).catch((e) => {
|
|
1912
|
+
error ||= getResultError(e, "Failed to fetch recipient behaviour metrics.");
|
|
1913
|
+
return null;
|
|
1914
|
+
});
|
|
1915
|
+
if (!response) return {
|
|
1916
|
+
data: null,
|
|
1917
|
+
error
|
|
1918
|
+
};
|
|
1919
|
+
return {
|
|
1920
|
+
data: clean({
|
|
1921
|
+
buckets: {
|
|
1922
|
+
unsubscribeDelivered: response.buckets.unsubscribe_delivered.map(mapBucket),
|
|
1923
|
+
unsubscribed: response.buckets.unsubscribed.map(mapBucket)
|
|
1924
|
+
},
|
|
1925
|
+
endTime: response.end_time,
|
|
1926
|
+
startTime: response.start_time,
|
|
1927
|
+
unsubscribeDelivered: response.unsubscribe_delivered,
|
|
1928
|
+
unsubscribed: response.unsubscribed
|
|
1929
|
+
}),
|
|
1930
|
+
error: null
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
async volume(options) {
|
|
1934
|
+
let error = null;
|
|
1935
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
1936
|
+
startTime: options?.startTime,
|
|
1937
|
+
endTime: options?.endTime
|
|
1938
|
+
});
|
|
1939
|
+
if (!dates || dateError) return {
|
|
1940
|
+
data: null,
|
|
1941
|
+
error: dateError
|
|
1942
|
+
};
|
|
1943
|
+
const response = await this.mailchannels.get("/tx/v1/metrics/volume", {
|
|
1944
|
+
query: {
|
|
1945
|
+
start_time: dates.startTime,
|
|
1946
|
+
end_time: dates.endTime,
|
|
1947
|
+
campaign_id: options?.campaignId,
|
|
1948
|
+
interval: options?.interval
|
|
1949
|
+
},
|
|
1950
|
+
onResponseError: async ({ response }) => {
|
|
1951
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
1952
|
+
}
|
|
1953
|
+
}).catch((e) => {
|
|
1954
|
+
error ||= getResultError(e, "Failed to fetch volume metrics.");
|
|
1955
|
+
return null;
|
|
1956
|
+
});
|
|
1957
|
+
if (!response) return {
|
|
1958
|
+
data: null,
|
|
1959
|
+
error
|
|
1960
|
+
};
|
|
1961
|
+
return {
|
|
1962
|
+
data: clean({
|
|
1963
|
+
buckets: {
|
|
1964
|
+
delivered: response.buckets.delivered.map(mapBucket),
|
|
1965
|
+
dropped: response.buckets.dropped.map(mapBucket),
|
|
1966
|
+
processed: response.buckets.processed.map(mapBucket)
|
|
1967
|
+
},
|
|
1968
|
+
delivered: response.delivered,
|
|
1969
|
+
dropped: response.dropped,
|
|
1970
|
+
endTime: response.end_time,
|
|
1971
|
+
processed: response.processed,
|
|
1972
|
+
startTime: response.start_time
|
|
1973
|
+
}),
|
|
1974
|
+
error: null
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
async usage() {
|
|
1978
|
+
let error = null;
|
|
1979
|
+
const response = await this.mailchannels.get("/tx/v1/usage", { onResponseError: async ({ response }) => {
|
|
1980
|
+
error = getStatusError(response);
|
|
1981
|
+
} }).catch((e) => {
|
|
1982
|
+
error ||= getResultError(e, "Failed to fetch usage metrics.");
|
|
1983
|
+
return null;
|
|
1984
|
+
});
|
|
1985
|
+
if (!response) return {
|
|
1986
|
+
data: null,
|
|
1987
|
+
error
|
|
1988
|
+
};
|
|
1989
|
+
return {
|
|
1990
|
+
data: clean({
|
|
1991
|
+
endDate: response.period_end_date,
|
|
1992
|
+
startDate: response.period_start_date,
|
|
1993
|
+
total: response.total_usage,
|
|
1994
|
+
monthlyLimit: response.monthly_limit
|
|
1995
|
+
}),
|
|
1996
|
+
error: null
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
async senders(type, options) {
|
|
2000
|
+
let error = null;
|
|
2001
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
2002
|
+
startTime: options?.startTime,
|
|
2003
|
+
endTime: options?.endTime
|
|
2004
|
+
});
|
|
2005
|
+
if (!dates || dateError) return {
|
|
2006
|
+
data: null,
|
|
2007
|
+
error: dateError
|
|
2008
|
+
};
|
|
2009
|
+
error = validatePagination({
|
|
2010
|
+
...options,
|
|
2011
|
+
max: 1e3
|
|
2012
|
+
});
|
|
2013
|
+
if (error) return {
|
|
2014
|
+
data: null,
|
|
2015
|
+
error
|
|
2016
|
+
};
|
|
2017
|
+
const response = await this.mailchannels.get(`/tx/v1/metrics/senders/${encodeURIComponent(type)}`, {
|
|
2018
|
+
query: {
|
|
2019
|
+
start_time: dates.startTime,
|
|
2020
|
+
end_time: dates.endTime,
|
|
2021
|
+
limit: options?.limit,
|
|
2022
|
+
offset: options?.offset,
|
|
2023
|
+
sort_order: options?.sortOrder
|
|
2024
|
+
},
|
|
2025
|
+
onResponseError: async ({ response }) => {
|
|
2026
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
2027
|
+
}
|
|
2028
|
+
}).catch((e) => {
|
|
2029
|
+
error ||= getResultError(e, "Failed to fetch senders metrics.");
|
|
2030
|
+
return null;
|
|
2031
|
+
});
|
|
2032
|
+
if (!response) return {
|
|
2033
|
+
data: null,
|
|
2034
|
+
error
|
|
2035
|
+
};
|
|
2036
|
+
return {
|
|
2037
|
+
data: clean({
|
|
2038
|
+
endTime: response.end_time,
|
|
2039
|
+
limit: response.limit,
|
|
2040
|
+
offset: response.offset,
|
|
2041
|
+
senders: response.senders,
|
|
2042
|
+
startTime: response.start_time,
|
|
2043
|
+
total: response.total
|
|
2044
|
+
}),
|
|
2045
|
+
error: null
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
var Suppressions = class {
|
|
2050
|
+
mailchannels;
|
|
2051
|
+
constructor(mailchannels) {
|
|
2052
|
+
this.mailchannels = mailchannels;
|
|
2053
|
+
}
|
|
2054
|
+
async create(entriesOrOptions, options) {
|
|
2055
|
+
let error = null;
|
|
2056
|
+
const entries = Array.isArray(entriesOrOptions) ? entriesOrOptions : entriesOrOptions.entries;
|
|
2057
|
+
const createOptions = Array.isArray(entriesOrOptions) ? options : entriesOrOptions;
|
|
2058
|
+
if (entries.length > 1e3) {
|
|
2059
|
+
error = createValidationError("The number of suppression entries must not exceed 1000.");
|
|
2060
|
+
return {
|
|
2061
|
+
success: false,
|
|
2062
|
+
error
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
const payload = {
|
|
2066
|
+
add_to_sub_accounts: createOptions?.addToSubAccounts,
|
|
2067
|
+
suppression_entries: entries.map((entry) => ({
|
|
2068
|
+
notes: entry.notes,
|
|
2069
|
+
recipient: entry.recipient,
|
|
2070
|
+
suppression_types: Array.from(new Set(entry.types || ["non-transactional"]))
|
|
2071
|
+
}))
|
|
2072
|
+
};
|
|
2073
|
+
await this.mailchannels.post("/tx/v1/suppression-list", {
|
|
2074
|
+
body: payload,
|
|
2075
|
+
onResponseError: async ({ response }) => {
|
|
2076
|
+
error = getStatusError(response, {
|
|
2077
|
+
[400]: "Bad Request.",
|
|
2078
|
+
[409]: "Conflict. One or more suppression entries in the request already exist and cannot be created again.",
|
|
2079
|
+
[413]: "Payload too large. The request exceeds the maximum allowed total of 1000 suppression entries for the parent account and/or its sub-accounts."
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}).catch((e) => {
|
|
2083
|
+
error ||= getResultError(e, "Failed to create suppression entries.");
|
|
2084
|
+
});
|
|
2085
|
+
return {
|
|
2086
|
+
success: !error,
|
|
2087
|
+
error
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
async delete(recipient, source) {
|
|
2091
|
+
let error = null;
|
|
2092
|
+
await this.mailchannels.delete(`/tx/v1/suppression-list/recipients/${encodeURIComponent(recipient)}`, {
|
|
2093
|
+
query: { source },
|
|
2094
|
+
onResponseError: async ({ response }) => {
|
|
2095
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
2096
|
+
}
|
|
2097
|
+
}).catch((e) => {
|
|
2098
|
+
error ||= getResultError(e, "Failed to delete suppression entry.");
|
|
2099
|
+
});
|
|
2100
|
+
return {
|
|
2101
|
+
success: !error,
|
|
2102
|
+
error
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
async list(options) {
|
|
2106
|
+
let error = null;
|
|
2107
|
+
if (options?.recipient && options.recipient.length > 255) {
|
|
2108
|
+
error = createValidationError("The recipient must not exceed 255 characters.");
|
|
2109
|
+
return {
|
|
2110
|
+
data: null,
|
|
2111
|
+
error
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const { dates, error: dateError } = parseDateInputs({
|
|
2115
|
+
createdBefore: options?.createdBefore,
|
|
2116
|
+
createdAfter: options?.createdAfter
|
|
2117
|
+
});
|
|
2118
|
+
if (!dates || dateError) return {
|
|
2119
|
+
data: null,
|
|
2120
|
+
error: dateError
|
|
2121
|
+
};
|
|
2122
|
+
error = validatePagination({
|
|
2123
|
+
...options,
|
|
2124
|
+
max: 1e3
|
|
2125
|
+
});
|
|
2126
|
+
if (error) return {
|
|
2127
|
+
data: null,
|
|
2128
|
+
error
|
|
2129
|
+
};
|
|
2130
|
+
const payload = {
|
|
2131
|
+
recipient: options?.recipient,
|
|
2132
|
+
source: options?.source,
|
|
2133
|
+
created_before: dates.createdBefore,
|
|
2134
|
+
created_after: dates.createdAfter,
|
|
2135
|
+
limit: options?.limit,
|
|
2136
|
+
offset: options?.offset
|
|
2137
|
+
};
|
|
2138
|
+
const response = await this.mailchannels.get("/tx/v1/suppression-list", {
|
|
2139
|
+
query: payload,
|
|
2140
|
+
onResponseError: async ({ response }) => {
|
|
2141
|
+
error = getStatusError(response, { [400]: "Bad Request." });
|
|
2142
|
+
}
|
|
2143
|
+
}).catch((e) => {
|
|
2144
|
+
error ||= getResultError(e, "Failed to fetch suppression entries.");
|
|
2145
|
+
return null;
|
|
2146
|
+
});
|
|
2147
|
+
if (!response) return {
|
|
2148
|
+
data: null,
|
|
2149
|
+
error
|
|
2150
|
+
};
|
|
2151
|
+
return {
|
|
2152
|
+
data: clean(response.suppression_list.map((entry) => ({
|
|
2153
|
+
createdAt: entry.created_at,
|
|
2154
|
+
notes: entry.notes,
|
|
2155
|
+
recipient: entry.recipient,
|
|
2156
|
+
sender: entry.sender,
|
|
2157
|
+
source: entry.source,
|
|
2158
|
+
types: entry.suppression_types
|
|
2159
|
+
}))),
|
|
2160
|
+
error: null
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
const base64Content = (data) => {
|
|
2165
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("base64");
|
|
2166
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
|
|
2167
|
+
};
|
|
2168
|
+
const guessContentType = (filename) => {
|
|
2169
|
+
return mime.getType(filename) || void 0;
|
|
2170
|
+
};
|
|
2171
|
+
var Attachment = class Attachment {
|
|
2172
|
+
static fromBytes(data, options) {
|
|
2173
|
+
const { filename, type, contentId } = options;
|
|
2174
|
+
return {
|
|
2175
|
+
content: base64Content(data),
|
|
2176
|
+
filename: decodeURIComponent(filename) || "attachment",
|
|
2177
|
+
type: type || guessContentType(filename),
|
|
2178
|
+
contentId
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
static async fromBlob(blob, options) {
|
|
2182
|
+
if (!(blob instanceof Blob)) throw new Error("Unable to create attachment: expected a Blob");
|
|
2183
|
+
const bytes = await blob.arrayBuffer();
|
|
2184
|
+
return Attachment.fromBytes(bytes, {
|
|
2185
|
+
type: blob.type,
|
|
2186
|
+
...options
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
var MailChannels = class extends MailChannelsClient {
|
|
2191
|
+
emails = new Emails(this);
|
|
2192
|
+
domains = new Domains(this);
|
|
2193
|
+
webhooks = new Webhooks(this);
|
|
2194
|
+
subAccounts = new SubAccounts(this);
|
|
2195
|
+
metrics = new Metrics(this);
|
|
2196
|
+
suppressions = new Suppressions(this);
|
|
2197
|
+
constructor(key, options) {
|
|
2198
|
+
super(key, options);
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
export { Attachment, Domains, Emails, MailChannels, MailChannelsClient, Metrics, SubAccounts, Suppressions, Webhooks, name, version };
|