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 +62 -0
- package/dist/mailchannels.d.mts +91 -22
- package/dist/mailchannels.mjs +210 -112
- package/package.json +6 -4
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
|
package/dist/mailchannels.d.mts
CHANGED
|
@@ -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
|
|
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?:
|
|
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?:
|
|
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
|
|
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
|
|
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 };
|
package/dist/mailchannels.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|