mailchannels-sdk 0.7.6 โ†’ 0.7.7

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/README.md CHANGED
@@ -29,6 +29,7 @@ This library provides a simple way to interact with the [MailChannels API](https
29
29
  - ๐Ÿ“ [Naming Conventions](#naming-conventions)
30
30
  - โš–๏ธ [License](#license)
31
31
  - ๐Ÿ’ป [Development](#development)
32
+ - ๐Ÿงช [Local simulator](#local-simulator)
32
33
 
33
34
  ## <a name="features">๐Ÿš€ Features</a>
34
35
 
@@ -43,6 +44,7 @@ Some of the things you can do with the SDK:
43
44
  - Webhook notifications
44
45
  - Manage sub-accounts
45
46
  - Retrieve metrics
47
+ - Inspect webhook delivery batches
46
48
  - Handle suppressions
47
49
  - Configure inbound domains
48
50
  - Manage account and recipient lists
@@ -130,12 +132,72 @@ pnpm test:watch
130
132
  # Run typecheck
131
133
  pnpm test:types
132
134
 
135
+ # Refresh API parity fixtures
136
+ pnpm parity:fixtures
137
+
138
+ # Run the local Email API simulator
139
+ pnpm simulate:email-api
140
+
133
141
  # Release new version
134
142
  pnpm release
135
143
  ```
136
144
 
137
145
  </details>
138
146
 
147
+ ## <a name="local-simulator">๐Ÿงช Local simulator</a>
148
+
149
+ This repo includes a small local MailChannels Email API simulator at [scripts/email-api-simulator.mjs](./scripts/email-api-simulator.mjs). It keeps state in memory and emulates the SDK-supported Email API endpoints so you can test your application without calling the real MailChannels service.
150
+
151
+ ### Start the simulator
152
+
153
+ ```sh
154
+ # default: http://127.0.0.1:8787
155
+ pnpm simulate:email-api
156
+ ```
157
+
158
+ You can override the bind address with environment variables:
159
+
160
+ ```sh
161
+ MAILCHANNELS_SIMULATOR_HOST=127.0.0.1 MAILCHANNELS_SIMULATOR_PORT=8787 pnpm simulate:email-api
162
+ ```
163
+
164
+ ### Point the SDK at the simulator
165
+
166
+ Use the optional `baseUrl` constructor option when creating the client:
167
+
168
+ ```ts
169
+ import { MailChannels } from 'mailchannels-sdk'
170
+
171
+ const mailchannels = new MailChannels('local-test-key', {
172
+ baseUrl: 'http://127.0.0.1:8787'
173
+ })
174
+
175
+ const { data, error } = await mailchannels.emails.send({
176
+ from: 'sender@example.com',
177
+ to: 'recipient@example.com',
178
+ subject: 'Hello from the simulator',
179
+ html: '<p>Local test</p>'
180
+ })
181
+ ```
182
+
183
+ ### What the simulator supports today
184
+
185
+ - Email sends and async sends
186
+ - Domain checks
187
+ - DKIM key create, list, rotate, and update
188
+ - Webhook enrollment, listing, validation, signing key lookup, and batch inspection
189
+ - Sub-account lifecycle, API keys, SMTP passwords, limits, and usage
190
+ - Engagement, performance, recipient behaviour, sender, volume, and usage metrics
191
+ - Suppression create, list, and delete
192
+
193
+ ### Current limitations
194
+
195
+ - State is in-memory only and is reset when the process stops
196
+ - Any non-empty `X-API-Key` is accepted, with separate in-memory state per API key
197
+ - Webhook responses are simulated locally, but the simulator does not yet emit real webhook callbacks to your application
198
+
199
+ The next planned expansion is outbound webhook delivery so client applications can test webhook ingestion flows against the simulator as well.
200
+
139
201
  <!-- Badges -->
140
202
  [npm-version-src]: https://img.shields.io/npm/v/mailchannels-sdk.svg?style=flat&colorA=070a30&colorB=35a047
141
203
  [npm-version-href]: https://npmjs.com/package/mailchannels-sdk
@@ -1,8 +1,16 @@
1
1
  import { FetchOptions } from "ofetch";
2
+ interface MailChannelsClientOptions {
3
+ /**
4
+ * Override the MailChannels API base URL.
5
+ * Useful for local testing against a simulator.
6
+ * @default "https://api.mailchannels.net"
7
+ */
8
+ baseUrl?: string;
9
+ }
2
10
  declare class MailChannelsClient {
3
11
  #private;
4
- private static BASE_URL;
5
- constructor(key: string);
12
+ private static readonly DEFAULT_BASE_URL;
13
+ constructor(key: string, options?: MailChannelsClientOptions);
6
14
  protected _fetch<T>(path: string, options?: FetchOptions<"json">): Promise<T>;
7
15
  post<T>(path: string, options?: Omit<FetchOptions<"json">, "method">): Promise<T>;
8
16
  get<T>(path: string, options?: Omit<FetchOptions<"json">, "method">): Promise<T>;
@@ -79,6 +87,63 @@ interface EmailsSendTracking {
79
87
  */
80
88
  open?: boolean;
81
89
  }
90
+ type EmailsSendRecipientInput = EmailsSendRecipient[] | EmailsSendRecipient | string[] | string;
91
+ interface EmailsSendDkim {
92
+ /**
93
+ * Domain used for DKIM signing.
94
+ */
95
+ domain?: string;
96
+ /**
97
+ * DKIM private key encoded in Base64.
98
+ */
99
+ privateKey?: string;
100
+ /**
101
+ * DKIM selector in the domain DNS records.
102
+ */
103
+ selector?: string;
104
+ }
105
+ interface EmailsSendPersonalization {
106
+ /**
107
+ * The BCC recipients for this personalization.
108
+ */
109
+ bcc?: EmailsSendRecipientInput;
110
+ /**
111
+ * The CC recipients for this personalization.
112
+ */
113
+ cc?: EmailsSendRecipientInput;
114
+ /**
115
+ * DKIM settings for this personalization.
116
+ */
117
+ dkim?: EmailsSendDkim;
118
+ /**
119
+ * Optional envelope sender for this personalization.
120
+ */
121
+ envelopeFrom?: EmailsSendRecipient | string;
122
+ /**
123
+ * Optional sender override for this personalization.
124
+ */
125
+ from?: EmailsSendRecipient | string;
126
+ /**
127
+ * Custom headers for this personalization.
128
+ */
129
+ headers?: Record<string, string>;
130
+ /**
131
+ * Reply-to override for this personalization.
132
+ */
133
+ replyTo?: EmailsSendRecipient | string;
134
+ /**
135
+ * Subject override for this personalization.
136
+ */
137
+ subject?: string;
138
+ /**
139
+ * The recipients for this personalization.
140
+ */
141
+ to: EmailsSendRecipientInput;
142
+ /**
143
+ * Template variables for this personalization.
144
+ */
145
+ mustaches?: Record<string, unknown>;
146
+ }
82
147
  interface EmailsSendOptionsBase {
83
148
  /**
84
149
  * An array of attachments to be sent with the email.
@@ -104,7 +169,7 @@ interface EmailsSendOptionsBase {
104
169
  * @example
105
170
  * 'Name <email@example.com>'
106
171
  */
107
- bcc?: EmailsSendRecipient[] | EmailsSendRecipient | string[] | string;
172
+ bcc?: EmailsSendRecipientInput;
108
173
  /**
109
174
  * The CC recipients of the email. Can be an array of email addresses or an array of objects with email and name properties or a single email address string or an object with email and name properties.
110
175
  * @example
@@ -121,24 +186,11 @@ interface EmailsSendOptionsBase {
121
186
  * @example
122
187
  * 'Name <email@example.com>'
123
188
  */
124
- cc?: EmailsSendRecipient[] | EmailsSendRecipient | string[] | string;
189
+ cc?: EmailsSendRecipientInput;
125
190
  /**
126
191
  * The DKIM settings for the email.
127
192
  */
128
- dkim?: {
129
- /**
130
- * Domain used for DKIM signing.
131
- */
132
- domain: string;
133
- /**
134
- * DKIM private key encoded in Base64.
135
- */
136
- privateKey?: string;
137
- /**
138
- * DKIM selector in the domain DNS records.
139
- */
140
- selector: string;
141
- };
193
+ dkim?: EmailsSendDkim;
142
194
  /**
143
195
  * Optional envelope sender address. If not set, the envelope sender defaults to the `from.email` field. Can be overridden per-personalization. Only the email portion is used; the name field is ignored.
144
196
  * @example
@@ -168,6 +220,10 @@ interface EmailsSendOptionsBase {
168
220
  * - **Case sensitivity**: Headers are treated as case-insensitive. If multiple headers differ only by case, only one will be used, with no guarantee of which one.
169
221
  */
170
222
  headers?: Record<string, string>;
223
+ /**
224
+ * Explicit personalization objects. Use this for advanced MailChannels payloads with multiple recipient groups or per-personalization overrides.
225
+ */
226
+ personalizations?: EmailsSendPersonalization[];
171
227
  /**
172
228
  * The recipient of the email. Can be an array of email addresses or an array of objects with `email` and `name` properties or a single email address string or an object with `email` and `name` properties.
173
229
  * @example
@@ -184,7 +240,7 @@ interface EmailsSendOptionsBase {
184
240
  * @example
185
241
  * 'Name <email@example.com>'
186
242
  */
187
- to: EmailsSendRecipient[] | EmailsSendRecipient | string[] | string;
243
+ to?: EmailsSendRecipientInput;
188
244
  /**
189
245
  * Adjust open and click tracking for the message. Please note that enabling tracking for your messages requires a subscription that supports open and click tracking.
190
246
  *
@@ -227,7 +283,20 @@ interface EmailsSendOptionsBase {
227
283
  */
228
284
  transactional?: boolean;
229
285
  }
230
- type EmailsSendOptions = EmailsSendOptionsBase & ({
286
+ type EmailsSendTargetOptions = {
287
+ personalizations: EmailsSendPersonalization[];
288
+ to?: never;
289
+ cc?: never;
290
+ bcc?: never;
291
+ mustaches?: never;
292
+ } | {
293
+ personalizations?: never;
294
+ to: EmailsSendRecipientInput;
295
+ cc?: EmailsSendRecipientInput;
296
+ bcc?: EmailsSendRecipientInput;
297
+ mustaches?: Record<string, unknown>;
298
+ };
299
+ type EmailsSendOptions = EmailsSendOptionsBase & EmailsSendTargetOptions & ({
231
300
  /**
232
301
  * The HTML content of the email.
233
302
  * @example
@@ -2077,6 +2146,6 @@ declare class MailChannels extends MailChannelsClient {
2077
2146
  readonly lists: Lists;
2078
2147
  readonly users: Users;
2079
2148
  readonly service: Service;
2080
- constructor(key: string);
2149
+ constructor(key: string, options?: MailChannelsClientOptions);
2081
2150
  }
2082
- export { DataResponse, Domains, DomainsBulkCreateLoginLinks, DomainsBulkCreateLoginLinksResponse, DomainsBulkProvisionOptions, DomainsBulkProvisionResponse, DomainsCreateLoginLink, DomainsCreateLoginLinkResponse, DomainsData, DomainsDownstreamAddress, DomainsListDownstreamAddressesOptions, DomainsListDownstreamAddressesResponse, DomainsListOptions, DomainsListResponse, DomainsProvisionOptions, DomainsProvisionResponse, Emails, EmailsCheckDomainOptions, EmailsCheckDomainResponse, EmailsCheckDomainVerdict, EmailsCreateDkimKeyOptions, EmailsCreateDkimKeyResponse, EmailsDkimKey, EmailsDkimKeyStatus, EmailsGetDkimKeysOptions, EmailsGetDkimKeysResponse, EmailsRotateDkimKeyOptions, EmailsRotateDkimKeyResponse, EmailsSendAsyncResponse, EmailsSendAttachment, EmailsSendOptions, EmailsSendRecipient, EmailsSendResponse, EmailsSendTracking, EmailsUpdateDkimKeyOptions, ErrorResponse, ListEntriesResponse, ListEntry, ListEntryOptions, ListEntryResponse, ListNames, Lists, MailChannels, MailChannelsClient, Metrics, MetricsBucket, MetricsEngagement, MetricsEngagementResponse, MetricsOptions, MetricsPerformance, MetricsPerformanceResponse, MetricsRecipientBehaviour, MetricsRecipientBehaviourResponse, MetricsSenders, MetricsSendersOptions, MetricsSendersResponse, MetricsSendersType, MetricsUsageResponse, MetricsVolume, MetricsVolumeResponse, Service, ServiceReportOptions, ServiceSubscriptionsResponse, SubAccounts, SubAccountsAccount, SubAccountsApiKey, SubAccountsCreateApiKeyResponse, SubAccountsCreateResponse, SubAccountsCreateSmtpPasswordResponse, SubAccountsLimit, SubAccountsLimitResponse, SubAccountsListApiKeyOptions, SubAccountsListApiKeyResponse, SubAccountsListOptions, SubAccountsListResponse, SubAccountsListSmtpPasswordResponse, SubAccountsSmtpPassword, SubAccountsUsage, SubAccountsUsageResponse, SuccessResponse, Suppressions, SuppressionsCreateOptions, SuppressionsListEntry, SuppressionsListOptions, SuppressionsListResponse, SuppressionsSource, SuppressionsTypes, Users, UsersCreateOptions, UsersCreateResponse, WebhookEvent, WebhookEventClick, WebhookEventComplained, WebhookEventDelivered, WebhookEventDropped, WebhookEventHardBounced, WebhookEventOpen, WebhookEventProcessed, WebhookEventSoftBounced, WebhookEventTest, WebhookEventType, WebhookEventUnsubscribed, WebhookEvents, Webhooks, WebhooksBatch, WebhooksBatchResponseStatus, WebhooksBatchStatus, WebhooksBatchesOptions, WebhooksBatchesResponse, WebhooksListResponse, WebhooksSigningKeyResponse, WebhooksValidateResponse, WebhooksVerifyOptions };
2151
+ export { DataResponse, Domains, DomainsBulkCreateLoginLinks, DomainsBulkCreateLoginLinksResponse, DomainsBulkProvisionOptions, DomainsBulkProvisionResponse, DomainsCreateLoginLink, DomainsCreateLoginLinkResponse, DomainsData, DomainsDownstreamAddress, DomainsListDownstreamAddressesOptions, DomainsListDownstreamAddressesResponse, DomainsListOptions, DomainsListResponse, DomainsProvisionOptions, DomainsProvisionResponse, Emails, EmailsCheckDomainOptions, EmailsCheckDomainResponse, EmailsCheckDomainVerdict, EmailsCreateDkimKeyOptions, EmailsCreateDkimKeyResponse, EmailsDkimKey, EmailsDkimKeyStatus, EmailsGetDkimKeysOptions, EmailsGetDkimKeysResponse, EmailsRotateDkimKeyOptions, EmailsRotateDkimKeyResponse, EmailsSendAsyncResponse, EmailsSendAttachment, EmailsSendDkim, EmailsSendOptions, EmailsSendPersonalization, EmailsSendRecipient, EmailsSendRecipientInput, EmailsSendResponse, EmailsSendTracking, EmailsUpdateDkimKeyOptions, ErrorResponse, ListEntriesResponse, ListEntry, ListEntryOptions, ListEntryResponse, ListNames, Lists, MailChannels, MailChannelsClient, type MailChannelsClientOptions, Metrics, MetricsBucket, MetricsEngagement, MetricsEngagementResponse, MetricsOptions, MetricsPerformance, MetricsPerformanceResponse, MetricsRecipientBehaviour, MetricsRecipientBehaviourResponse, MetricsSenders, MetricsSendersOptions, MetricsSendersResponse, MetricsSendersType, MetricsUsageResponse, MetricsVolume, MetricsVolumeResponse, Service, ServiceReportOptions, ServiceSubscriptionsResponse, SubAccounts, SubAccountsAccount, SubAccountsApiKey, SubAccountsCreateApiKeyResponse, SubAccountsCreateResponse, SubAccountsCreateSmtpPasswordResponse, SubAccountsLimit, SubAccountsLimitResponse, SubAccountsListApiKeyOptions, SubAccountsListApiKeyResponse, SubAccountsListOptions, SubAccountsListResponse, SubAccountsListSmtpPasswordResponse, SubAccountsSmtpPassword, SubAccountsUsage, SubAccountsUsageResponse, SuccessResponse, Suppressions, SuppressionsCreateOptions, SuppressionsListEntry, SuppressionsListOptions, SuppressionsListResponse, SuppressionsSource, SuppressionsTypes, Users, UsersCreateOptions, UsersCreateResponse, WebhookEvent, WebhookEventClick, WebhookEventComplained, WebhookEventDelivered, WebhookEventDropped, WebhookEventHardBounced, WebhookEventOpen, WebhookEventProcessed, WebhookEventSoftBounced, WebhookEventTest, WebhookEventType, WebhookEventUnsubscribed, WebhookEvents, Webhooks, WebhooksBatch, WebhooksBatchResponseStatus, WebhooksBatchStatus, WebhooksBatchesOptions, WebhooksBatchesResponse, WebhooksListResponse, WebhooksSigningKeyResponse, WebhooksValidateResponse, WebhooksVerifyOptions };
@@ -2,10 +2,12 @@ import { $fetch } from "ofetch";
2
2
  import { subtle } from "node:crypto";
3
3
  import { Buffer } from "node:buffer";
4
4
  var MailChannelsClient = class MailChannelsClient {
5
- static BASE_URL = "https://api.mailchannels.net";
5
+ static DEFAULT_BASE_URL = "https://api.mailchannels.net";
6
+ #baseUrl;
6
7
  #headers;
7
- constructor(key) {
8
+ constructor(key, options = {}) {
8
9
  if (!key) throw new Error("Missing MailChannels API key.");
10
+ this.#baseUrl = options.baseUrl || MailChannelsClient.DEFAULT_BASE_URL;
9
11
  this.#headers = {
10
12
  "X-API-Key": key,
11
13
  "Accept": "application/json",
@@ -14,7 +16,7 @@ var MailChannelsClient = class MailChannelsClient {
14
16
  }
15
17
  async _fetch(path, options) {
16
18
  return $fetch(path, {
17
- baseURL: MailChannelsClient.BASE_URL,
19
+ baseURL: this.#baseUrl,
18
20
  ...options,
19
21
  headers: {
20
22
  ...this.#headers,
@@ -146,75 +148,175 @@ const mapBuckets = (arr) => {
146
148
  periodStart: period_start
147
149
  }));
148
150
  };
151
+ const RESERVED_HEADER_NAMES = new Set([
152
+ "authentication-results",
153
+ "bcc",
154
+ "cc",
155
+ "content-transfer-encoding",
156
+ "content-type",
157
+ "dkim-signature",
158
+ "from",
159
+ "message-id",
160
+ "received",
161
+ "reply-to",
162
+ "subject",
163
+ "to"
164
+ ]);
165
+ const getRecipientCount = (recipients) => recipients?.length || 0;
166
+ const validateHeaderMap = (headers, label) => {
167
+ if (!headers) return null;
168
+ const seenHeaders = /* @__PURE__ */ new Set();
169
+ for (const [headerName, headerValue] of Object.entries(headers)) {
170
+ if (typeof headerValue !== "string") return `${label} header '${headerName}' must have a string value.`;
171
+ const normalizedHeaderName = headerName.toLowerCase();
172
+ if (seenHeaders.has(normalizedHeaderName)) return `${label} headers must be unique when compared case-insensitively.`;
173
+ if (RESERVED_HEADER_NAMES.has(normalizedHeaderName)) return `${label} headers cannot include the reserved header '${headerName}'.`;
174
+ seenHeaders.add(normalizedHeaderName);
175
+ }
176
+ return null;
177
+ };
178
+ const validateSendDkim = (dkim, label) => {
179
+ if (!dkim) return null;
180
+ if (dkim.domain && !dkim.selector) return `${label} DKIM domain requires a selector.`;
181
+ if (dkim.privateKey && (!dkim.domain || !dkim.selector)) return `${label} DKIM privateKey requires both a domain and selector.`;
182
+ return null;
183
+ };
184
+ const mapDkimKey = (key) => ({
185
+ algorithm: key.algorithm,
186
+ createdAt: key.created_at,
187
+ dnsRecords: key.dkim_dns_records,
188
+ domain: key.domain,
189
+ gracePeriodExpiresAt: key.gracePeriodExpiresAt,
190
+ length: key.key_length,
191
+ publicKey: key.public_key,
192
+ retiresAt: key.retiresAt,
193
+ selector: key.selector,
194
+ status: key.status,
195
+ statusModifiedAt: key.status_modified_at
196
+ });
197
+ const mapDkim = (dkim) => ({
198
+ dkim_domain: dkim?.domain,
199
+ dkim_private_key: dkim?.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
200
+ dkim_selector: dkim?.selector
201
+ });
202
+ const mapPersonalization = (personalization, index) => {
203
+ const to = parseArrayRecipients(personalization.to);
204
+ if (!to || !to.length) return `Personalization at index ${index} must include at least one recipient in the \`to\` field.`;
205
+ if (to.length > 1e3) return `Personalization at index ${index} cannot include more than 1000 \`to\` recipients.`;
206
+ const cc = parseArrayRecipients(personalization.cc);
207
+ if (cc && cc.length > 1e3) return `Personalization at index ${index} cannot include more than 1000 \`cc\` recipients.`;
208
+ const bcc = parseArrayRecipients(personalization.bcc);
209
+ if (bcc && bcc.length > 1e3) return `Personalization at index ${index} cannot include more than 1000 \`bcc\` recipients.`;
210
+ const headerError = validateHeaderMap(personalization.headers, `Personalization at index ${index}`);
211
+ if (headerError) return headerError;
212
+ const dkimError = validateSendDkim(personalization.dkim, `Personalization at index ${index}`);
213
+ if (dkimError) return dkimError;
214
+ return {
215
+ bcc,
216
+ cc,
217
+ ...mapDkim(personalization.dkim),
218
+ dynamic_template_data: personalization.mustaches,
219
+ envelope_from: parseRecipient(personalization.envelopeFrom),
220
+ from: parseRecipient(personalization.from),
221
+ headers: personalization.headers,
222
+ reply_to: parseRecipient(personalization.replyTo),
223
+ subject: personalization.subject,
224
+ to
225
+ };
226
+ };
227
+ const buildSendPayload = (options) => {
228
+ const { from, html, text } = options;
229
+ const parsedFrom = parseRecipient(from);
230
+ if (!parsedFrom || !parsedFrom.email) return "No sender provided. Use the `from` option to specify a sender";
231
+ if (!text && !html) return "No email content provided";
232
+ if (options.attachments && options.attachments.length > 1e3) return "The maximum number of attachments is 1000.";
233
+ if (options.campaignId && (options.campaignId.length > 48 || /\s/.test(options.campaignId))) return "campaignId must be 48 characters or fewer and must not contain spaces.";
234
+ const rootHeaderError = validateHeaderMap(options.headers, "Root");
235
+ if (rootHeaderError) return rootHeaderError;
236
+ const rootDkimError = validateSendDkim(options.dkim, "Root");
237
+ if (rootDkimError) return rootDkimError;
238
+ let personalizations;
239
+ if (options.personalizations) {
240
+ if (!options.personalizations.length) return "At least one personalization must be provided.";
241
+ if (options.personalizations.length > 1e3) return "The maximum number of personalizations is 1000.";
242
+ const mapped = options.personalizations.map(mapPersonalization);
243
+ const error = mapped.find((item) => typeof item === "string");
244
+ if (error) return error;
245
+ personalizations = mapped;
246
+ } else {
247
+ const parsedTo = parseArrayRecipients(options.to);
248
+ if (!parsedTo || !parsedTo.length) return "No recipients provided. Use the `to` option to specify at least one recipient";
249
+ if (parsedTo.length > 1e3) return "The maximum number of `to` recipients is 1000.";
250
+ const parsedCc = parseArrayRecipients(options.cc);
251
+ if (parsedCc && parsedCc.length > 1e3) return "The maximum number of `cc` recipients is 1000.";
252
+ const parsedBcc = parseArrayRecipients(options.bcc);
253
+ if (parsedBcc && parsedBcc.length > 1e3) return "The maximum number of `bcc` recipients is 1000.";
254
+ personalizations = [{
255
+ bcc: parsedBcc,
256
+ cc: parsedCc,
257
+ dynamic_template_data: options.mustaches,
258
+ to: parsedTo
259
+ }];
260
+ }
261
+ if (options.transactional === false) {
262
+ if (!personalizations.every((personalization) => {
263
+ const personalizationDkim = personalization.dkim_selector;
264
+ const rootDkim = options.dkim?.selector;
265
+ return Boolean(personalizationDkim || rootDkim);
266
+ })) return "Non-transactional messages must be DKIM signed.";
267
+ if (personalizations.some((personalization) => {
268
+ return getRecipientCount(personalization.to) + getRecipientCount(personalization.cc) + getRecipientCount(personalization.bcc) !== 1;
269
+ })) return "Non-transactional messages must have exactly one recipient per personalization.";
270
+ }
271
+ const content = [];
272
+ const template_type = Boolean(options.mustaches) || Boolean(options.personalizations?.some((personalization) => personalization.mustaches)) ? "mustache" : void 0;
273
+ if (text) content.push({
274
+ type: "text/plain",
275
+ value: text,
276
+ template_type
277
+ });
278
+ if (html) content.push({
279
+ type: "text/html",
280
+ value: html,
281
+ template_type
282
+ });
283
+ return {
284
+ attachments: options.attachments,
285
+ campaign_id: options.campaignId,
286
+ ...mapDkim(options.dkim),
287
+ envelope_from: parseRecipient(options.envelopeFrom),
288
+ personalizations,
289
+ headers: options.headers,
290
+ reply_to: parseRecipient(options.replyTo),
291
+ from: parsedFrom,
292
+ subject: options.subject,
293
+ content,
294
+ tracking_settings: options.tracking ? {
295
+ click_tracking: typeof options.tracking.click === "boolean" ? { enable: options.tracking.click } : void 0,
296
+ open_tracking: typeof options.tracking.open === "boolean" ? { enable: options.tracking.open } : void 0
297
+ } : void 0,
298
+ transactional: options.transactional
299
+ };
300
+ };
149
301
  var Emails = class {
150
302
  constructor(mailchannels) {
151
303
  this.mailchannels = mailchannels;
152
304
  }
153
305
  async _sendEmail(options, flags) {
154
306
  let error = null;
155
- const { cc, bcc, from, to, html, text, mustaches, dkim } = options;
156
- const parsedFrom = parseRecipient(from);
157
- if (!parsedFrom || !parsedFrom.email) {
158
- error = createError("No sender provided. Use the `from` option to specify a sender");
159
- return {
160
- success: false,
307
+ const payload = buildSendPayload(options);
308
+ if (typeof payload === "string") {
309
+ error = createError(payload);
310
+ if (flags.async) return {
161
311
  data: null,
162
312
  error
163
313
  };
164
- }
165
- const parsedTo = parseArrayRecipients(to);
166
- if (!parsedTo || !parsedTo.length) {
167
- error = createError("No recipients provided. Use the `to` option to specify at least one recipient");
168
- return {
169
- success: false,
170
- data: null,
171
- error
172
- };
173
- }
174
- if (!text && !html) {
175
- error = createError("No email content provided");
176
314
  return {
177
315
  success: false,
178
316
  data: null,
179
317
  error
180
318
  };
181
319
  }
182
- const content = [];
183
- const template_type = mustaches ? "mustache" : void 0;
184
- if (text) content.push({
185
- type: "text/plain",
186
- value: text,
187
- template_type
188
- });
189
- if (html) content.push({
190
- type: "text/html",
191
- value: html,
192
- template_type
193
- });
194
- const payload = {
195
- attachments: options.attachments,
196
- campaign_id: options.campaignId,
197
- personalizations: [{
198
- bcc: parseArrayRecipients(bcc),
199
- cc: parseArrayRecipients(cc),
200
- to: parsedTo,
201
- dkim_domain: dkim?.domain || void 0,
202
- dkim_private_key: dkim?.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
203
- dkim_selector: dkim?.selector || void 0,
204
- dynamic_template_data: options.mustaches
205
- }],
206
- headers: options.headers,
207
- reply_to: parseRecipient(options.replyTo),
208
- envelope_from: parseRecipient(options.envelopeFrom),
209
- from: parsedFrom,
210
- subject: options.subject,
211
- content,
212
- tracking_settings: options.tracking ? {
213
- click_tracking: options.tracking.click ? { enable: options.tracking.click } : void 0,
214
- open_tracking: options.tracking.open ? { enable: options.tracking.open } : void 0
215
- } : void 0,
216
- transactional: options.transactional
217
- };
218
320
  const endpoint = flags.async ? "/tx/v1/send-async" : "/tx/v1/send";
219
321
  const response = await this.mailchannels.post(endpoint, {
220
322
  query: { "dry-run": flags.dryRun },
@@ -271,8 +373,23 @@ var Emails = class {
271
373
  async checkDomain(options) {
272
374
  let error = null;
273
375
  const { dkim, domain, senderId } = options;
376
+ const dkimOptions = dkim ? Array.isArray(dkim) ? dkim : [dkim] : void 0;
377
+ if (dkimOptions && dkimOptions.length > 10) {
378
+ error = createError("A maximum of 10 DKIM settings can be provided.");
379
+ return {
380
+ data: null,
381
+ error
382
+ };
383
+ }
384
+ if (dkimOptions?.find(({ privateKey, selector }) => privateKey && !selector)) {
385
+ error = createError("DKIM settings with a privateKey must also include a selector.");
386
+ return {
387
+ data: null,
388
+ error
389
+ };
390
+ }
274
391
  const payload = {
275
- dkim_settings: (dkim ? Array.isArray(dkim) ? dkim : [dkim] : void 0)?.map(({ domain, privateKey, selector }) => ({
392
+ dkim_settings: dkimOptions?.map(({ domain, privateKey, selector }) => ({
276
393
  dkim_domain: domain,
277
394
  dkim_private_key: privateKey ? stripPemHeaders(privateKey) : void 0,
278
395
  dkim_selector: selector
@@ -344,19 +461,7 @@ var Emails = class {
344
461
  error
345
462
  };
346
463
  return {
347
- data: clean({
348
- algorithm: response.algorithm,
349
- createdAt: response.created_at,
350
- dnsRecords: response.dkim_dns_records,
351
- domain: response.domain,
352
- gracePeriodExpiresAt: response.gracePeriodExpiresAt,
353
- length: response.key_length,
354
- publicKey: response.public_key,
355
- retiresAt: response.retiresAt,
356
- selector: response.selector,
357
- status: response.status,
358
- statusModifiedAt: response.status_modified_at
359
- }),
464
+ data: clean(mapDkimKey(response)),
360
465
  error: null
361
466
  };
362
467
  }
@@ -398,19 +503,7 @@ var Emails = class {
398
503
  error
399
504
  };
400
505
  return {
401
- data: clean(response.keys.map((key) => ({
402
- algorithm: key.algorithm,
403
- createdAt: key.created_at,
404
- dnsRecords: key.dkim_dns_records,
405
- domain: key.domain,
406
- gracePeriodExpiresAt: key.gracePeriodExpiresAt,
407
- length: key.key_length,
408
- publicKey: key.public_key,
409
- retiresAt: key.retiresAt,
410
- selector: key.selector,
411
- status: key.status,
412
- statusModifiedAt: key.status_modified_at
413
- }))),
506
+ data: clean(response.keys.map(mapDkimKey)),
414
507
  error: null
415
508
  };
416
509
  }
@@ -476,32 +569,8 @@ var Emails = class {
476
569
  };
477
570
  return {
478
571
  data: clean({
479
- new: {
480
- algorithm: response.new_key.algorithm,
481
- createdAt: response.new_key.created_at,
482
- dnsRecords: response.new_key.dkim_dns_records,
483
- domain: response.new_key.domain,
484
- gracePeriodExpiresAt: response.new_key.gracePeriodExpiresAt,
485
- length: response.new_key.key_length,
486
- publicKey: response.new_key.public_key,
487
- retiresAt: response.new_key.retiresAt,
488
- selector: response.new_key.selector,
489
- status: response.new_key.status,
490
- statusModifiedAt: response.new_key.status_modified_at
491
- },
492
- rotated: {
493
- algorithm: response.rotated_key.algorithm,
494
- createdAt: response.rotated_key.created_at,
495
- dnsRecords: response.rotated_key.dkim_dns_records,
496
- domain: response.rotated_key.domain,
497
- gracePeriodExpiresAt: response.rotated_key.gracePeriodExpiresAt,
498
- length: response.rotated_key.key_length,
499
- publicKey: response.rotated_key.public_key,
500
- retiresAt: response.rotated_key.retiresAt,
501
- selector: response.rotated_key.selector,
502
- status: response.rotated_key.status,
503
- statusModifiedAt: response.rotated_key.status_modified_at
504
- }
572
+ new: mapDkimKey(response.new_key),
573
+ rotated: mapDkimKey(response.rotated_key)
505
574
  }),
506
575
  error: null
507
576
  };
@@ -710,6 +779,35 @@ var Webhooks = class Webhooks {
710
779
  data: null,
711
780
  error
712
781
  };
782
+ if (options?.statuses && options.statuses.length > 6) return {
783
+ data: null,
784
+ error: createError("A maximum of 6 status filters can be provided.")
785
+ };
786
+ if (options?.statuses && new Set(options.statuses).size !== options.statuses.length) return {
787
+ data: null,
788
+ error: createError("Status filters must be unique.")
789
+ };
790
+ if (options?.createdAfter && Number.isNaN(Date.parse(options.createdAfter))) return {
791
+ data: null,
792
+ error: createError("createdAfter must be a valid date string.")
793
+ };
794
+ if (options?.createdBefore && Number.isNaN(Date.parse(options.createdBefore))) return {
795
+ data: null,
796
+ error: createError("createdBefore must be a valid date string.")
797
+ };
798
+ if (options?.createdAfter && options?.createdBefore) {
799
+ const createdAfter = Date.parse(options.createdAfter);
800
+ const createdBefore = Date.parse(options.createdBefore);
801
+ const maxRangeMs = 744 * 60 * 60 * 1e3;
802
+ if (createdBefore <= createdAfter) return {
803
+ data: null,
804
+ error: createError("createdBefore must be later than createdAfter.")
805
+ };
806
+ if (createdBefore - createdAfter > maxRangeMs) return {
807
+ data: null,
808
+ error: createError("The time range between createdAfter and createdBefore must not exceed 31 days.")
809
+ };
810
+ }
713
811
  const response = await this.mailchannels.get("/tx/v1/webhook-batch", {
714
812
  query: {
715
813
  created_after: options?.createdAfter,
@@ -2172,8 +2270,8 @@ var MailChannels = class extends MailChannelsClient {
2172
2270
  lists = new Lists(this);
2173
2271
  users = new Users(this);
2174
2272
  service = new Service(this);
2175
- constructor(key) {
2176
- super(key);
2273
+ constructor(key, options) {
2274
+ super(key, options);
2177
2275
  }
2178
2276
  };
2179
2277
  export { Domains, Emails, Lists, MailChannels, MailChannelsClient, Metrics, Service, SubAccounts, Suppressions, Users, Webhooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailchannels-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Node.js SDK to integrate MailChannels API into your JavaScript or TypeScript server-side applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,21 +40,23 @@
40
40
  "@stylistic/eslint-plugin": "^5.10.0",
41
41
  "@types/markdown-it": "^14.1.2",
42
42
  "@types/node": "^25.5.2",
43
- "@vitest/coverage-v8": "^4.1.2",
43
+ "@vitest/coverage-v8": "^4.1.3",
44
44
  "changelogen": "^0.6.2",
45
45
  "jiti": "^2.6.1",
46
46
  "obuild": "^0.4.33",
47
- "oxlint": "^1.58.0",
47
+ "oxlint": "^1.59.0",
48
48
  "scule": "^1.3.0",
49
49
  "typescript": "^6.0.2",
50
50
  "vitepress": "^2.0.0-alpha.17",
51
51
  "vitepress-plugin-group-icons": "^1.7.3",
52
52
  "vitepress-plugin-llms": "^1.12.0",
53
- "vitest": "^4.1.2"
53
+ "vitest": "^4.1.3"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "obuild",
57
+ "parity:fixtures": "node scripts/generate-parity-fixtures.mjs",
57
58
  "release": "pnpm lint && pnpm test && pnpm build && changelogen --release && git push --follow-tags",
59
+ "simulate:email-api": "node scripts/email-api-simulator.mjs",
58
60
  "lint": "oxlint",
59
61
  "lint:fix": "oxlint --fix",
60
62
  "test": "vitest run --reporter=verbose --coverage",