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.
Files changed (31) hide show
  1. package/.agents/skills/mailchannels-js/SKILL.md +4 -0
  2. package/.agents/skills/mailchannels-js/resources/attachments.md +8 -10
  3. package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +5 -5
  4. package/.agents/skills/mailchannels-js/resources/custom-headers.md +4 -4
  5. package/.agents/skills/mailchannels-js/resources/dkim.md +10 -10
  6. package/.agents/skills/mailchannels-js/resources/domain-checks.md +6 -6
  7. package/.agents/skills/mailchannels-js/resources/error-handling.md +5 -5
  8. package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +9 -9
  9. package/.agents/skills/mailchannels-js/resources/overview.md +5 -5
  10. package/.agents/skills/mailchannels-js/resources/plugins/nodemailer.md +112 -0
  11. package/.agents/skills/mailchannels-js/resources/sending.md +10 -10
  12. package/.agents/skills/mailchannels-js/resources/sub-accounts.md +7 -9
  13. package/.agents/skills/mailchannels-js/resources/suppressions.md +4 -4
  14. package/.agents/skills/mailchannels-js/resources/templates.md +5 -5
  15. package/.agents/skills/mailchannels-js/resources/testing.md +52 -5
  16. package/.agents/skills/mailchannels-js/resources/unsubscribe.md +3 -3
  17. package/.agents/skills/mailchannels-js/resources/webhooks.md +8 -8
  18. package/README.md +73 -2
  19. package/dist/_chunks/mailchannels.d.mts +2094 -0
  20. package/dist/_chunks/mailchannels.mjs +2201 -0
  21. package/dist/_chunks/simulator.mjs +16 -8
  22. package/dist/cli/index.mjs +268 -0
  23. package/dist/mailchannels.d.mts +1 -2095
  24. package/dist/mailchannels.mjs +1 -2201
  25. package/dist/plugins/nodemailer/index.d.mts +33 -0
  26. package/dist/plugins/nodemailer/index.mjs +145 -0
  27. package/dist/simulator/index.d.mts +20 -0
  28. package/dist/simulator/index.mjs +2 -0
  29. package/package.json +21 -9
  30. package/dist/cli.d.mts +0 -1
  31. 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 };