mailumi 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -7
- package/dist/cjs/index.d.ts +304 -5
- package/dist/cjs/index.js +60 -7
- package/dist/cli.js +190 -1
- package/dist/index.d.ts +304 -5
- package/dist/index.js +60 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ The official Node.js SDK and CLI for [Mailumi](https://mailumi.com), the email A
|
|
|
7
7
|
- Every method resolves to `{ data, error }` and never throws for API errors
|
|
8
8
|
- Automatic retries for rate limits and server errors; sends reuse one `Idempotency-Key`, so a retry never sends twice
|
|
9
9
|
- Webhook signature verification built in
|
|
10
|
-
-
|
|
10
|
+
- Set up domains, webhooks, templates, campaigns and suppressions from code, not only in the dashboard
|
|
11
|
+
- `npx mailumi send` and every other API call from the terminal
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -117,6 +118,46 @@ const { data: page } = await mailumi.emails.list({ direction: 'inbound' });
|
|
|
117
118
|
const { data: bytes } = await mailumi.emails.attachment({ id, index: 0 });
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
## Set up domains and webhooks
|
|
122
|
+
|
|
123
|
+
With an API key that has `domains:write` and `webhooks:write`, or Full access, a script, deploy pipeline or AI agent can do the whole setup:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const { data: domain } = await mailumi.domains.create({ name: 'mail.yourdomain.com' });
|
|
127
|
+
for (const r of domain.records) console.log(r.type, r.name, r.value, r.required ? '' : '(recommended)');
|
|
128
|
+
|
|
129
|
+
// After publishing the records at your DNS provider:
|
|
130
|
+
const { data: check } = await mailumi.domains.verify(domain.id);
|
|
131
|
+
check.ready; // true once the domain can send
|
|
132
|
+
check.message; // what is still missing
|
|
133
|
+
|
|
134
|
+
const { data: hook } = await mailumi.webhooks.create({
|
|
135
|
+
name: 'Production',
|
|
136
|
+
url: 'https://yourapp.com/webhooks/mailumi',
|
|
137
|
+
events: ['email.delivered', 'email.bounced', 'email.complained'],
|
|
138
|
+
});
|
|
139
|
+
hook.secret; // only returned now: store it as MAILUMI_WEBHOOK_SECRET
|
|
140
|
+
|
|
141
|
+
await mailumi.webhooks.test(hook.id);
|
|
142
|
+
const { data: deliveries } = await mailumi.webhooks.deliveries(hook.id);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Mailumi also checks new domains every 5 minutes and emails you as soon as one is verified, so there is no need to poll. If you do, wait at least 15 seconds between checks. `domains.update({ id, openTracking, clickTracking })` changes tracking; turning receiving on with `receivingEnabled: true` also needs `mxConsent: true`, because the MX record replaces any mailbox provider the domain uses now.
|
|
146
|
+
|
|
147
|
+
| Permission | Allows |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `email:send` | Send, batch, schedule, reschedule and cancel emails |
|
|
150
|
+
| `email:read` | Read emails, delivery events and attachments |
|
|
151
|
+
| `domains:read` / `domains:write` | Read domains / add, verify, configure and remove them |
|
|
152
|
+
| `webhooks:write` | Create, test, retry and disable webhooks |
|
|
153
|
+
| `templates:write` | Create, change and delete templates |
|
|
154
|
+
| `contacts:write` | Manage audiences and contacts |
|
|
155
|
+
| `campaigns:write` | Create, send and delete campaigns |
|
|
156
|
+
| `suppressions:write` | List and remove suppressed addresses |
|
|
157
|
+
| `full_access` | Everything, including permissions added later |
|
|
158
|
+
|
|
159
|
+
API keys cannot create or revoke API keys; that stays in the dashboard. A key limited to one domain only sees and manages that domain.
|
|
160
|
+
|
|
120
161
|
## Webhooks
|
|
121
162
|
|
|
122
163
|
Mailumi signs each webhook with the [Standard Webhooks](https://www.standardwebhooks.com/) format. `verifyWebhook` checks the signature and timestamp and returns the parsed event. It needs no API key; the secret defaults to `MAILUMI_WEBHOOK_SECRET`.
|
|
@@ -132,17 +173,23 @@ export async function POST(request: Request) {
|
|
|
132
173
|
if (error) return new Response(error.message, { status: 401 });
|
|
133
174
|
|
|
134
175
|
if (event.type === 'email.received') {
|
|
135
|
-
|
|
176
|
+
const { from, reply_to, subject, text, html, message_id, spam, attachments } = event.data;
|
|
177
|
+
// spam is a boolean; attachments have filename, content_type, size and download_url
|
|
178
|
+
}
|
|
179
|
+
if (event.type === 'email.bounced') {
|
|
180
|
+
const { recipient, bounce } = event.data; // bounce.type is "Permanent" or "Transient"
|
|
136
181
|
}
|
|
137
182
|
return new Response('OK');
|
|
138
183
|
}
|
|
139
184
|
```
|
|
140
185
|
|
|
186
|
+
`event.data` is typed by `event.type`: `EmailReceivedData` for `email.received`, and `EmailEventData` for events about email you sent, which is the email plus `recipient` and a detail such as `delivery`, `bounce` or `click`. Every field is described under [Receiving email](https://mailumi.com/docs#receiving) and [Webhooks](https://mailumi.com/docs#webhooks).
|
|
187
|
+
|
|
141
188
|
`headers` can be a `Headers` object, Node's `req.headers`, or `{ id, timestamp, signature }`. Deliveries can repeat, so store `event.id` and skip ids you have seen. Events: `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`, `email.opened`, `email.clicked`, `email.failed`, `email.unsubscribed` and `email.received`.
|
|
142
189
|
|
|
143
|
-
## Audiences and
|
|
190
|
+
## Audiences, contacts and campaigns
|
|
144
191
|
|
|
145
|
-
Add contacts from your app for campaigns.
|
|
192
|
+
Add contacts from your app for campaigns. Every campaign email gets an unsubscribe link and one-click unsubscribe. Contacts need an API key with the `contacts:write` permission, campaigns `campaigns:write`.
|
|
146
193
|
|
|
147
194
|
```ts
|
|
148
195
|
const { data: audience } = await mailumi.audiences.create({ name: 'Newsletter' });
|
|
@@ -156,6 +203,25 @@ const { data } = await mailumi.contacts.list({ audienceId: audience.id, search:
|
|
|
156
203
|
|
|
157
204
|
Unsubscribing applies to the address in every audience of the workspace, and adding or importing a contact never resubscribes it.
|
|
158
205
|
|
|
206
|
+
```ts
|
|
207
|
+
const { data: campaign } = await mailumi.campaigns.create({
|
|
208
|
+
name: 'October update',
|
|
209
|
+
subject: 'What is new, {{first_name}}',
|
|
210
|
+
text: 'Hi {{first_name}},\n\nHere is what we shipped this month: https://yourdomain.com/changelog',
|
|
211
|
+
from: 'Acme <news@yourdomain.com>',
|
|
212
|
+
audienceId: audience.id,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const { data: draft } = await mailumi.campaigns.get(campaign.id);
|
|
216
|
+
draft.readiness; // { recipients, missing }: fix everything in missing before sending
|
|
217
|
+
|
|
218
|
+
await mailumi.campaigns.send(campaign.id);
|
|
219
|
+
const { data: sent } = await mailumi.campaigns.get(campaign.id);
|
|
220
|
+
sent.stats; // delivered, bounced, opened, clicked, unsubscribed, ...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Templates for transactional email work the same way: `templates.create({ name, subject, html, variables })` returns an ID to send with `templateId`. `suppressions.list()` shows addresses Mailumi no longer sends to after a bounce or complaint, and `suppressions.remove(address)` allows one again.
|
|
224
|
+
|
|
159
225
|
## Errors
|
|
160
226
|
|
|
161
227
|
```ts
|
|
@@ -173,7 +239,7 @@ if (error) {
|
|
|
173
239
|
| `unauthorized` | The API key is missing, wrong, revoked or expired. |
|
|
174
240
|
| `forbidden` | The key lacks the permission, or is limited to another domain. |
|
|
175
241
|
| `plan_limit_reached` | The monthly or daily email allowance is used up. |
|
|
176
|
-
| `not_found` | The email, domain, audience or
|
|
242
|
+
| `not_found` | The email, domain, webhook, template, audience, contact or campaign does not exist. |
|
|
177
243
|
| `conflict` | The `Idempotency-Key` was used for a different request. |
|
|
178
244
|
| `rate_limited` | Too many requests; retried automatically. |
|
|
179
245
|
| `internal_error`, `service_unavailable` | A temporary problem; retried automatically. |
|
|
@@ -202,20 +268,28 @@ npx mailumi emails list
|
|
|
202
268
|
npx mailumi emails get <id>
|
|
203
269
|
npx mailumi emails cancel <id>
|
|
204
270
|
npx mailumi domains list
|
|
271
|
+
npx mailumi domains add mail.yourdomain.com
|
|
272
|
+
npx mailumi domains verify <domain_id> --wait # re-checks every 15 seconds until verified
|
|
273
|
+
npx mailumi webhooks create https://yourapp.com/webhooks/mailumi --name Production --events email.delivered,email.bounced
|
|
274
|
+
npx mailumi webhooks test <webhook_id>
|
|
275
|
+
npx mailumi templates create --name Welcome --subject "Welcome, {{first_name}}" --html-file ./welcome.html
|
|
205
276
|
npx mailumi audiences list
|
|
206
277
|
npx mailumi contacts add <audience_id> jane@example.com --first-name Jane
|
|
278
|
+
npx mailumi campaigns create --name "October update" --subject "News" --text-file ./news.txt --from news@yourdomain.com --audience <audience_id>
|
|
279
|
+
npx mailumi campaigns send <campaign_id>
|
|
280
|
+
npx mailumi suppressions list
|
|
207
281
|
```
|
|
208
282
|
|
|
209
283
|
Add `--json` for machine-readable output and `--help` for every command and option. The CLI exits with code 1 on errors.
|
|
210
284
|
|
|
211
285
|
## AI coding assistants
|
|
212
286
|
|
|
213
|
-
Mailumi publishes [llms.txt](https://mailumi.com/llms.txt), [llms-full.txt](https://mailumi.com/llms-full.txt), an [OpenAPI 3.1 description](https://mailumi.com/openapi.json) and an [MCP server](https://mailumi.com/docs#mcp), so Cursor, Claude Code, Codex and VS Code can send test emails
|
|
287
|
+
Mailumi publishes [llms.txt](https://mailumi.com/llms.txt), [llms-full.txt](https://mailumi.com/llms-full.txt), an [OpenAPI 3.1 description](https://mailumi.com/openapi.json) and an [MCP server](https://mailumi.com/docs#mcp), so Cursor, Claude Code, Codex and VS Code can send test emails, check delivery and set up domains, webhooks, templates and campaigns while they build your integration. See [mailumi.com/ai](https://mailumi.com/ai).
|
|
214
288
|
|
|
215
289
|
## Links
|
|
216
290
|
|
|
217
291
|
- [Documentation](https://mailumi.com/docs)
|
|
218
|
-
- [Pricing](https://mailumi.com/pricing): the free plan includes
|
|
292
|
+
- [Pricing](https://mailumi.com/pricing): the free plan includes 1,500 emails a month
|
|
219
293
|
- [Support](https://mailumi.com/contact)
|
|
220
294
|
|
|
221
295
|
## Versioning
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The official Mailumi SDK. Works anywhere `fetch` exists: Node.js 18+, Bun, Deno and Cloudflare Workers.
|
|
3
3
|
* Every method resolves to `{ data, error }` and never throws for API errors.
|
|
4
4
|
*/
|
|
5
|
-
export declare const VERSION = "1.
|
|
5
|
+
export declare const VERSION = "1.2.0";
|
|
6
6
|
export type ErrorCode = 'invalid_request' | 'unauthorized' | 'plan_limit_reached' | 'forbidden' | 'not_found' | 'method_not_allowed' | 'conflict' | 'payload_too_large' | 'rate_limited' | 'internal_error' | 'service_unavailable' | 'network_error' | 'missing_api_key' | 'invalid_signature' | 'missing_webhook_secret';
|
|
7
7
|
export interface MailumiError {
|
|
8
8
|
name: 'MailumiError';
|
|
@@ -116,10 +116,13 @@ export interface DnsRecord {
|
|
|
116
116
|
value: string;
|
|
117
117
|
priority?: number;
|
|
118
118
|
verified: boolean;
|
|
119
|
+
/** What DNS returned at this name when it does not match `value`. Only on records that are not verified. */
|
|
120
|
+
found?: string[];
|
|
119
121
|
}
|
|
120
122
|
export interface Domain {
|
|
121
123
|
id: string;
|
|
122
124
|
name: string;
|
|
125
|
+
region?: string;
|
|
123
126
|
ownershipVerified?: boolean;
|
|
124
127
|
sendingEnabled?: boolean;
|
|
125
128
|
receivingEnabled?: boolean;
|
|
@@ -131,6 +134,125 @@ export interface Domain {
|
|
|
131
134
|
createdAt?: number;
|
|
132
135
|
records: DnsRecord[];
|
|
133
136
|
}
|
|
137
|
+
export interface DomainVerification {
|
|
138
|
+
domain: Domain;
|
|
139
|
+
/** `true` once the domain can send. */
|
|
140
|
+
ready: boolean;
|
|
141
|
+
/** What is still missing, or that the domain is ready. */
|
|
142
|
+
message: string;
|
|
143
|
+
/** How many required records have a different value at the DNS provider. */
|
|
144
|
+
wrong?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface DomainSettings {
|
|
147
|
+
id: string;
|
|
148
|
+
/** Receive email for the domain and send it to your email.received webhooks. */
|
|
149
|
+
receivingEnabled?: boolean;
|
|
150
|
+
/** Must be `true` to turn receiving on: the MX record replaces any mailbox provider the domain uses now. */
|
|
151
|
+
mxConsent?: boolean;
|
|
152
|
+
/** Pro and Scale plans. */
|
|
153
|
+
openTracking?: boolean;
|
|
154
|
+
/** Pro and Scale plans. */
|
|
155
|
+
clickTracking?: boolean;
|
|
156
|
+
}
|
|
157
|
+
export type WebhookSubscription = Exclude<WebhookEventType, 'webhook.test'>;
|
|
158
|
+
export interface NewWebhook {
|
|
159
|
+
name: string;
|
|
160
|
+
/** A public HTTPS URL on port 443. */
|
|
161
|
+
url: string;
|
|
162
|
+
/** Defaults to `['email.received']`. */
|
|
163
|
+
events?: WebhookSubscription[];
|
|
164
|
+
/** Only send events for this domain. Leave it out for every domain. */
|
|
165
|
+
domainId?: string;
|
|
166
|
+
/** For email.received: only mail to this address before the @, for example "support". */
|
|
167
|
+
recipient?: string;
|
|
168
|
+
}
|
|
169
|
+
export interface Webhook {
|
|
170
|
+
object: 'webhook';
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
url: string;
|
|
174
|
+
events: WebhookSubscription[];
|
|
175
|
+
domain_id: string | null;
|
|
176
|
+
recipient: string | null;
|
|
177
|
+
enabled: boolean;
|
|
178
|
+
created_at: string;
|
|
179
|
+
}
|
|
180
|
+
export interface WebhookDelivery {
|
|
181
|
+
/** Also the webhook-id header of the request. */
|
|
182
|
+
id: string;
|
|
183
|
+
object: 'webhook_delivery';
|
|
184
|
+
webhook_id: string;
|
|
185
|
+
event: WebhookEventType;
|
|
186
|
+
email_id: string | null;
|
|
187
|
+
status: 'pending' | 'delivered' | 'failed' | 'cancelled';
|
|
188
|
+
attempts: number;
|
|
189
|
+
response_code: number | null;
|
|
190
|
+
last_error: string | null;
|
|
191
|
+
next_attempt_at: string | null;
|
|
192
|
+
created_at: string;
|
|
193
|
+
delivered_at: string | null;
|
|
194
|
+
}
|
|
195
|
+
export interface NewTemplate {
|
|
196
|
+
name: string;
|
|
197
|
+
/** Placeholders such as `{{first_name}}` work in the subject and HTML. */
|
|
198
|
+
subject: string;
|
|
199
|
+
html: string;
|
|
200
|
+
/** Default values for placeholders an email does not pass. */
|
|
201
|
+
variables?: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
export interface Template {
|
|
204
|
+
object: 'template';
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
subject: string;
|
|
208
|
+
html: string;
|
|
209
|
+
variables: Record<string, string>;
|
|
210
|
+
created_at: string;
|
|
211
|
+
updated_at: string;
|
|
212
|
+
}
|
|
213
|
+
export interface NewCampaign {
|
|
214
|
+
name: string;
|
|
215
|
+
/** `{{first_name}}`, `{{last_name}}` and `{{email}}` are filled in per contact. */
|
|
216
|
+
subject: string;
|
|
217
|
+
/** Plain text. Blank lines start paragraphs and links become clickable. Mailumi adds your postal address and an unsubscribe link. */
|
|
218
|
+
text: string;
|
|
219
|
+
/** A sender on a verified domain. Needed before sending. */
|
|
220
|
+
from?: string;
|
|
221
|
+
/** Needed before sending. */
|
|
222
|
+
audienceId?: string | null;
|
|
223
|
+
}
|
|
224
|
+
export type CampaignStats = Record<'total' | 'pending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'opened' | 'clicked' | 'unsubscribed', number>;
|
|
225
|
+
export interface Campaign {
|
|
226
|
+
object: 'campaign';
|
|
227
|
+
id: string;
|
|
228
|
+
name: string;
|
|
229
|
+
subject: string;
|
|
230
|
+
text: string;
|
|
231
|
+
from: string | null;
|
|
232
|
+
audience_id: string | null;
|
|
233
|
+
status: 'draft' | 'sending' | 'sent' | 'failed';
|
|
234
|
+
/** How many contacts it was sent to. */
|
|
235
|
+
recipients: number;
|
|
236
|
+
error: string | null;
|
|
237
|
+
sent_at: string | null;
|
|
238
|
+
created_at: string;
|
|
239
|
+
updated_at: string;
|
|
240
|
+
/** Drafts, from `campaigns.get`: how many contacts it would go to now, and what to fix before sending. */
|
|
241
|
+
readiness?: {
|
|
242
|
+
recipients: number;
|
|
243
|
+
missing: string[];
|
|
244
|
+
};
|
|
245
|
+
/** Campaigns that were sent. */
|
|
246
|
+
stats?: CampaignStats;
|
|
247
|
+
}
|
|
248
|
+
export interface Suppression {
|
|
249
|
+
object: 'suppression';
|
|
250
|
+
address: string;
|
|
251
|
+
reason: 'bounce' | 'complaint' | 'manual';
|
|
252
|
+
detail: string | null;
|
|
253
|
+
email_id: string | null;
|
|
254
|
+
created_at: string;
|
|
255
|
+
}
|
|
134
256
|
export interface Audience {
|
|
135
257
|
object: 'audience';
|
|
136
258
|
id: string;
|
|
@@ -189,6 +311,105 @@ export interface WebhookEvent<T = Record<string, any>> {
|
|
|
189
311
|
created_at: string;
|
|
190
312
|
data: T;
|
|
191
313
|
}
|
|
314
|
+
interface EmailFields {
|
|
315
|
+
email_id: string;
|
|
316
|
+
domain_id: string;
|
|
317
|
+
from: string;
|
|
318
|
+
to: string[];
|
|
319
|
+
subject: string;
|
|
320
|
+
last_event: string;
|
|
321
|
+
tags: Record<string, string>;
|
|
322
|
+
created_at: string;
|
|
323
|
+
}
|
|
324
|
+
export interface ReceivedAttachment {
|
|
325
|
+
id: string;
|
|
326
|
+
filename: string;
|
|
327
|
+
content_type: string;
|
|
328
|
+
/** Set for inline images that the HTML shows with `cid:`. */
|
|
329
|
+
content_id: string | null;
|
|
330
|
+
/** Size in bytes. */
|
|
331
|
+
size: number;
|
|
332
|
+
/** Download it server-side with `mailumi.emails.attachment({ id: email_id, index })` or a key with email:read. */
|
|
333
|
+
download_url: string;
|
|
334
|
+
}
|
|
335
|
+
/** `data` of an `email.received` event. */
|
|
336
|
+
export interface EmailReceivedData extends EmailFields {
|
|
337
|
+
direction: 'inbound';
|
|
338
|
+
status: 'received';
|
|
339
|
+
scheduled_at: null;
|
|
340
|
+
error: null;
|
|
341
|
+
/** `Name <address>`, or only the address when the email has no name. */
|
|
342
|
+
from: string;
|
|
343
|
+
/** The recipients at this domain, in lowercase. */
|
|
344
|
+
to: string[];
|
|
345
|
+
cc: string[];
|
|
346
|
+
reply_to: string[];
|
|
347
|
+
/** The Message-ID header with its angle brackets, for In-Reply-To and References when you reply. */
|
|
348
|
+
message_id: string | null;
|
|
349
|
+
/** Empty when the email has no plain-text part. */
|
|
350
|
+
text: string;
|
|
351
|
+
/** Empty when the email has no HTML part. It is the sender's HTML: sanitize it before showing it. */
|
|
352
|
+
html: string;
|
|
353
|
+
/** `true` when the Amazon SES spam check flagged the email. */
|
|
354
|
+
spam: boolean;
|
|
355
|
+
attachments: ReceivedAttachment[];
|
|
356
|
+
}
|
|
357
|
+
/** `data` of an event about an email you sent: the email as it is now, plus the detail for that event. */
|
|
358
|
+
export interface EmailEventData extends EmailFields {
|
|
359
|
+
direction: 'outbound';
|
|
360
|
+
status: string;
|
|
361
|
+
scheduled_at: string | null;
|
|
362
|
+
error: string | null;
|
|
363
|
+
/** The recipient the event is about. Not set on email.sent and email.failed, nor on opens and clicks of an email with more than one recipient. */
|
|
364
|
+
recipient?: string;
|
|
365
|
+
delivery?: {
|
|
366
|
+
smtp_response: string | null;
|
|
367
|
+
};
|
|
368
|
+
delay?: {
|
|
369
|
+
type: string | null;
|
|
370
|
+
expires_at: string | null;
|
|
371
|
+
message: string | null;
|
|
372
|
+
};
|
|
373
|
+
bounce?: {
|
|
374
|
+
type: 'Permanent' | 'Transient' | (string & {}) | null;
|
|
375
|
+
sub_type: string | null;
|
|
376
|
+
message: string | null;
|
|
377
|
+
};
|
|
378
|
+
complaint?: {
|
|
379
|
+
type: string | null;
|
|
380
|
+
};
|
|
381
|
+
open?: {
|
|
382
|
+
ip_address: string | null;
|
|
383
|
+
user_agent: string | null;
|
|
384
|
+
};
|
|
385
|
+
click?: {
|
|
386
|
+
link: string | null;
|
|
387
|
+
ip_address: string | null;
|
|
388
|
+
user_agent: string | null;
|
|
389
|
+
};
|
|
390
|
+
failed?: {
|
|
391
|
+
reason: string;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** The event `verifyWebhook` returns. Check `event.type` and `event.data` has the matching fields. */
|
|
395
|
+
export type MailumiWebhookEvent = {
|
|
396
|
+
id: string;
|
|
397
|
+
type: 'email.received';
|
|
398
|
+
created_at: string;
|
|
399
|
+
data: EmailReceivedData;
|
|
400
|
+
} | {
|
|
401
|
+
id: string;
|
|
402
|
+
type: Exclude<WebhookEventType, 'email.received' | 'webhook.test'>;
|
|
403
|
+
created_at: string;
|
|
404
|
+
data: EmailEventData;
|
|
405
|
+
} | {
|
|
406
|
+
id: string;
|
|
407
|
+
type: 'webhook.test';
|
|
408
|
+
created_at: string;
|
|
409
|
+
data: {
|
|
410
|
+
message: string;
|
|
411
|
+
};
|
|
412
|
+
};
|
|
192
413
|
export interface VerifyWebhookOptions {
|
|
193
414
|
/** The raw request body exactly as received, before any JSON parsing. */
|
|
194
415
|
payload: string | Uint8Array;
|
|
@@ -203,7 +424,8 @@ export interface VerifyWebhookOptions {
|
|
|
203
424
|
* Checks the Standard Webhooks signature of a Mailumi webhook and returns the parsed event.
|
|
204
425
|
* Store `event.id` and skip ids you have seen: a delivery can arrive more than once.
|
|
205
426
|
*/
|
|
206
|
-
export declare function verifyWebhook
|
|
427
|
+
export declare function verifyWebhook(options: VerifyWebhookOptions): Promise<Result<MailumiWebhookEvent>>;
|
|
428
|
+
export declare function verifyWebhook<T>(options: VerifyWebhookOptions): Promise<Result<WebhookEvent<T>>>;
|
|
207
429
|
/** Converts the SDK's camelCase options to the API's request body. snake_case keys are accepted too. */
|
|
208
430
|
export declare function toApiEmail(o: SendEmailOptions & Record<string, any>): Record<string, unknown>;
|
|
209
431
|
export declare class Mailumi {
|
|
@@ -247,6 +469,54 @@ export declare class Mailumi {
|
|
|
247
469
|
list: () => Promise<Result<{
|
|
248
470
|
data: Domain[];
|
|
249
471
|
}>>;
|
|
472
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
473
|
+
create: ({ name }: {
|
|
474
|
+
name: string;
|
|
475
|
+
}) => Promise<Result<Domain>>;
|
|
476
|
+
get: (id: string) => Promise<Result<Domain>>;
|
|
477
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
478
|
+
verify: (id: string) => Promise<Result<DomainVerification>>;
|
|
479
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }: DomainSettings & Record<string, any>) => Promise<Result<Domain>>;
|
|
480
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
481
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
482
|
+
};
|
|
483
|
+
readonly webhooks: {
|
|
484
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
485
|
+
verify: typeof verifyWebhook;
|
|
486
|
+
list: () => Promise<Result<{
|
|
487
|
+
data: Webhook[];
|
|
488
|
+
}>>;
|
|
489
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
490
|
+
create: (webhook: NewWebhook) => Promise<Result<Webhook & {
|
|
491
|
+
secret: string;
|
|
492
|
+
}>>;
|
|
493
|
+
get: (id: string) => Promise<Result<Webhook>>;
|
|
494
|
+
/** Stops sending events to the webhook. */
|
|
495
|
+
disable: (id: string) => Promise<Result<Webhook>>;
|
|
496
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
497
|
+
test: (id: string) => Promise<Result<WebhookDelivery>>;
|
|
498
|
+
/** The 100 most recent deliveries, newest first. */
|
|
499
|
+
deliveries: (id: string) => Promise<Result<{
|
|
500
|
+
data: WebhookDelivery[];
|
|
501
|
+
}>>;
|
|
502
|
+
/** Delivers an event again now. */
|
|
503
|
+
retry: ({ id, deliveryId }: {
|
|
504
|
+
id: string;
|
|
505
|
+
deliveryId: string;
|
|
506
|
+
}) => Promise<Result<WebhookDelivery>>;
|
|
507
|
+
};
|
|
508
|
+
readonly templates: {
|
|
509
|
+
list: () => Promise<Result<{
|
|
510
|
+
data: Template[];
|
|
511
|
+
}>>;
|
|
512
|
+
/** Saves a template to send with `templateId`. */
|
|
513
|
+
create: (template: NewTemplate) => Promise<Result<Template>>;
|
|
514
|
+
get: (id: string) => Promise<Result<Template>>;
|
|
515
|
+
/** Changes only the fields you pass. */
|
|
516
|
+
update: ({ id, ...template }: Partial<NewTemplate> & {
|
|
517
|
+
id: string;
|
|
518
|
+
}) => Promise<Result<Template>>;
|
|
519
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
250
520
|
};
|
|
251
521
|
readonly audiences: {
|
|
252
522
|
list: () => Promise<Result<{
|
|
@@ -291,9 +561,38 @@ export declare class Mailumi {
|
|
|
291
561
|
}) => Promise<Result<Contact>>;
|
|
292
562
|
remove: (ref: ContactRef) => Promise<Result<Deleted>>;
|
|
293
563
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
564
|
+
readonly campaigns: {
|
|
565
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
566
|
+
list: () => Promise<Result<{
|
|
567
|
+
data: Campaign[];
|
|
568
|
+
}>>;
|
|
569
|
+
/** Creates a draft. */
|
|
570
|
+
create: (campaign: NewCampaign) => Promise<Result<Campaign>>;
|
|
571
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
572
|
+
get: (id: string) => Promise<Result<Campaign>>;
|
|
573
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
574
|
+
update: ({ id, ...campaign }: Partial<NewCampaign> & {
|
|
575
|
+
id: string;
|
|
576
|
+
}) => Promise<Result<Campaign>>;
|
|
577
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
578
|
+
send: (id: string) => Promise<Result<Campaign>>;
|
|
579
|
+
/** Copies a campaign into a new draft. */
|
|
580
|
+
duplicate: (id: string) => Promise<Result<Campaign>>;
|
|
581
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
582
|
+
};
|
|
583
|
+
readonly suppressions: {
|
|
584
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
585
|
+
list: (q?: {
|
|
586
|
+
search?: string;
|
|
587
|
+
}) => Promise<Result<{
|
|
588
|
+
data: Suppression[];
|
|
589
|
+
}>>;
|
|
590
|
+
/** Allows sending to the address again. */
|
|
591
|
+
remove: (address: string) => Promise<Result<{
|
|
592
|
+
object: "suppression";
|
|
593
|
+
address: string;
|
|
594
|
+
deleted: true;
|
|
595
|
+
}>>;
|
|
297
596
|
};
|
|
298
597
|
private readonly key;
|
|
299
598
|
private readonly baseUrl;
|
package/dist/cjs/index.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.toApiEmail = toApiEmail;
|
|
|
7
7
|
* The official Mailumi SDK. Works anywhere `fetch` exists: Node.js 18+, Bun, Deno and Cloudflare Workers.
|
|
8
8
|
* Every method resolves to `{ data, error }` and never throws for API errors.
|
|
9
9
|
*/
|
|
10
|
-
exports.VERSION = '1.
|
|
10
|
+
exports.VERSION = '1.2.0';
|
|
11
11
|
const DEFAULT_BASE_URL = 'https://mailumi.com/api/v1';
|
|
12
12
|
function header(h, name, short) {
|
|
13
13
|
if (typeof h.get === 'function')
|
|
@@ -29,10 +29,6 @@ function sameText(a, b) {
|
|
|
29
29
|
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
30
30
|
return diff === 0;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Checks the Standard Webhooks signature of a Mailumi webhook and returns the parsed event.
|
|
34
|
-
* Store `event.id` and skip ids you have seen: a delivery can arrive more than once.
|
|
35
|
-
*/
|
|
36
32
|
async function verifyWebhook({ payload, headers, secret, tolerance = 300 }) {
|
|
37
33
|
const key = secret ?? globalThis.process?.env?.MAILUMI_WEBHOOK_SECRET;
|
|
38
34
|
if (!key)
|
|
@@ -85,10 +81,11 @@ function toApiEmail(o) {
|
|
|
85
81
|
}
|
|
86
82
|
const toApiContact = (c) => ({ email: c.email, first_name: c.firstName ?? c.first_name, last_name: c.lastName ?? c.last_name, unsubscribed: c.unsubscribed });
|
|
87
83
|
const contactPath = (r) => `/audiences/${encodeURIComponent(r.audienceId)}/contacts/${encodeURIComponent(r.id ?? r.email ?? '')}`;
|
|
84
|
+
const toApiWebhook = (w) => ({ name: w.name, url: w.url, events: w.events, domain_id: w.domainId ?? w.domain_id, recipient: w.recipient });
|
|
85
|
+
const toApiCampaign = (c) => ({ name: c.name, subject: c.subject, text: c.text, from: c.from, audience_id: c.audienceId !== undefined ? c.audienceId : c.audience_id });
|
|
86
|
+
const pathTo = (base, id, rest = '') => `${base}/${encodeURIComponent(id)}${rest}`;
|
|
88
87
|
class Mailumi {
|
|
89
88
|
constructor(apiKey, options = {}) {
|
|
90
|
-
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
91
|
-
this.webhooks = { verify: verifyWebhook };
|
|
92
89
|
const env = globalThis.process?.env ?? {};
|
|
93
90
|
this.key = apiKey ?? env.MAILUMI_API_KEY ?? '';
|
|
94
91
|
this.baseUrl = (options.baseUrl ?? env.MAILUMI_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
@@ -116,6 +113,62 @@ class Mailumi {
|
|
|
116
113
|
this.domains = {
|
|
117
114
|
/** Lists your domains, their DNS records and whether each is verified. */
|
|
118
115
|
list: () => this.request('GET', '/domains'),
|
|
116
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
117
|
+
create: ({ name }) => this.request('POST', '/domains', { name }),
|
|
118
|
+
get: (id) => this.request('GET', pathTo('/domains', id)),
|
|
119
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
120
|
+
verify: (id) => this.request('POST', pathTo('/domains', id, '/verify')),
|
|
121
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }) => this.request('PATCH', pathTo('/domains', id), {
|
|
122
|
+
receiving_enabled: receivingEnabled ?? rest.receiving_enabled, mx_consent: mxConsent ?? rest.mx_consent, open_tracking: openTracking ?? rest.open_tracking, click_tracking: clickTracking ?? rest.click_tracking,
|
|
123
|
+
}),
|
|
124
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
125
|
+
remove: (id) => this.request('DELETE', pathTo('/domains', id)),
|
|
126
|
+
};
|
|
127
|
+
this.webhooks = {
|
|
128
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
129
|
+
verify: verifyWebhook,
|
|
130
|
+
list: () => this.request('GET', '/webhooks'),
|
|
131
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
132
|
+
create: (webhook) => this.request('POST', '/webhooks', toApiWebhook(webhook)),
|
|
133
|
+
get: (id) => this.request('GET', pathTo('/webhooks', id)),
|
|
134
|
+
/** Stops sending events to the webhook. */
|
|
135
|
+
disable: (id) => this.request('DELETE', pathTo('/webhooks', id)),
|
|
136
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
137
|
+
test: (id) => this.request('POST', pathTo('/webhooks', id, '/test')),
|
|
138
|
+
/** The 100 most recent deliveries, newest first. */
|
|
139
|
+
deliveries: (id) => this.request('GET', pathTo('/webhooks', id, '/deliveries')),
|
|
140
|
+
/** Delivers an event again now. */
|
|
141
|
+
retry: ({ id, deliveryId }) => this.request('POST', pathTo('/webhooks', id, `/deliveries/${encodeURIComponent(deliveryId)}/retry`)),
|
|
142
|
+
};
|
|
143
|
+
this.templates = {
|
|
144
|
+
list: () => this.request('GET', '/templates'),
|
|
145
|
+
/** Saves a template to send with `templateId`. */
|
|
146
|
+
create: (template) => this.request('POST', '/templates', template),
|
|
147
|
+
get: (id) => this.request('GET', pathTo('/templates', id)),
|
|
148
|
+
/** Changes only the fields you pass. */
|
|
149
|
+
update: ({ id, ...template }) => this.request('PATCH', pathTo('/templates', id), template),
|
|
150
|
+
remove: (id) => this.request('DELETE', pathTo('/templates', id)),
|
|
151
|
+
};
|
|
152
|
+
this.campaigns = {
|
|
153
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
154
|
+
list: () => this.request('GET', '/campaigns'),
|
|
155
|
+
/** Creates a draft. */
|
|
156
|
+
create: (campaign) => this.request('POST', '/campaigns', toApiCampaign(campaign)),
|
|
157
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
158
|
+
get: (id) => this.request('GET', pathTo('/campaigns', id)),
|
|
159
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
160
|
+
update: ({ id, ...campaign }) => this.request('PATCH', pathTo('/campaigns', id), toApiCampaign(campaign)),
|
|
161
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
162
|
+
send: (id) => this.request('POST', pathTo('/campaigns', id, '/send')),
|
|
163
|
+
/** Copies a campaign into a new draft. */
|
|
164
|
+
duplicate: (id) => this.request('POST', pathTo('/campaigns', id, '/duplicate')),
|
|
165
|
+
remove: (id) => this.request('DELETE', pathTo('/campaigns', id)),
|
|
166
|
+
};
|
|
167
|
+
this.suppressions = {
|
|
168
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
169
|
+
list: (q = {}) => this.request('GET', '/suppressions' + query(q)),
|
|
170
|
+
/** Allows sending to the address again. */
|
|
171
|
+
remove: (address) => this.request('DELETE', pathTo('/suppressions', address)),
|
|
119
172
|
};
|
|
120
173
|
this.audiences = {
|
|
121
174
|
list: () => this.request('GET', '/audiences'),
|
package/dist/cli.js
CHANGED
|
@@ -13,11 +13,36 @@ Commands
|
|
|
13
13
|
emails cancel <id> Cancel a scheduled email
|
|
14
14
|
emails reschedule <id> <at> Change the send time: an ISO 8601 date or "in 2 hours"
|
|
15
15
|
domains list List domains and their verification status
|
|
16
|
+
domains add <name> Add a domain and show the DNS records to publish
|
|
17
|
+
domains get <id> Show a domain's DNS records and which were found
|
|
18
|
+
domains verify <id> Check the DNS records now (--wait: re-check every 15 seconds until verified)
|
|
19
|
+
domains update <id> --receiving on|off [--mx-consent], --open-tracking on|off, --click-tracking on|off
|
|
20
|
+
domains remove <id> Remove a domain
|
|
21
|
+
webhooks list List webhooks
|
|
22
|
+
webhooks create <url> --name <name> [--events email.delivered,email.bounced] [--domain <id>] [--recipient <name>]
|
|
23
|
+
webhooks test <id> Send a signed test event
|
|
24
|
+
webhooks deliveries <id> Show the latest deliveries
|
|
25
|
+
webhooks retry <id> <delivery_id>
|
|
26
|
+
webhooks disable <id> Stop sending events to a webhook
|
|
27
|
+
templates list List templates
|
|
28
|
+
templates get <id> Show a template
|
|
29
|
+
templates create --name <name> --subject <text> --html <html> | --html-file <path> [--var name=default]
|
|
30
|
+
templates update <id> Change any of --name, --subject, --html, --html-file, --var
|
|
31
|
+
templates remove <id> Delete a template
|
|
16
32
|
audiences list List audiences
|
|
17
33
|
audiences create <name> Create an audience
|
|
18
34
|
contacts list <audience> List contacts (--search <text>, --after <cursor>)
|
|
19
35
|
contacts add <audience> <email> [--first-name <name>] [--last-name <name>]
|
|
20
36
|
contacts unsubscribe <audience> <email>
|
|
37
|
+
campaigns list List campaigns with their results
|
|
38
|
+
campaigns get <id> Show a campaign, and what is missing before a draft can be sent
|
|
39
|
+
campaigns create --name <name> --subject <text> --text <text> | --text-file <path> [--from <address>] [--audience <id>]
|
|
40
|
+
campaigns update <id> Change any of --name, --subject, --text, --text-file, --from, --audience
|
|
41
|
+
campaigns send <id> Send a draft to its audience now
|
|
42
|
+
campaigns duplicate <id> Copy a campaign into a new draft
|
|
43
|
+
campaigns remove <id> Delete a campaign
|
|
44
|
+
suppressions list List suppressed addresses (--search <text>)
|
|
45
|
+
suppressions remove <address> Allow sending to an address again
|
|
21
46
|
|
|
22
47
|
Send options
|
|
23
48
|
--from <address> Sender on a verified domain, e.g. "Acme <hello@yourdomain.com>"
|
|
@@ -37,7 +62,7 @@ Global options
|
|
|
37
62
|
--help, --version
|
|
38
63
|
|
|
39
64
|
Docs: https://mailumi.com/docs`;
|
|
40
|
-
const BOOLEAN = new Set(['json', 'help', 'version', 'inbound', 'outbound']);
|
|
65
|
+
const BOOLEAN = new Set(['json', 'help', 'version', 'inbound', 'outbound', 'wait', 'mx-consent']);
|
|
41
66
|
function parse(argv) {
|
|
42
67
|
const args = [], flags = {};
|
|
43
68
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -87,6 +112,28 @@ function sendOptions(f) {
|
|
|
87
112
|
variables: pairs(f, 'var') && Object.fromEntries(pairs(f, 'var')), tags: pairs(f, 'tag')?.map(([name, value]) => ({ name, value })),
|
|
88
113
|
};
|
|
89
114
|
}
|
|
115
|
+
function toggle(f, name) {
|
|
116
|
+
const v = one(f, name);
|
|
117
|
+
if (v === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (/^(on|true|yes)$/i.test(v))
|
|
120
|
+
return true;
|
|
121
|
+
if (/^(off|false|no)$/i.test(v))
|
|
122
|
+
return false;
|
|
123
|
+
throw new Error(`--${name} expects on or off, got "${v}".`);
|
|
124
|
+
}
|
|
125
|
+
const fileOr = (f, name) => one(f, name) ?? (one(f, `${name}-file`) ? readFileSync(one(f, `${name}-file`), 'utf8') : undefined);
|
|
126
|
+
function templateOptions(f) {
|
|
127
|
+
const variables = pairs(f, 'var');
|
|
128
|
+
return { name: one(f, 'name'), subject: one(f, 'subject'), html: fileOr(f, 'html'), variables: variables && Object.fromEntries(variables) };
|
|
129
|
+
}
|
|
130
|
+
const campaignOptions = (f) => ({ name: one(f, 'name'), subject: one(f, 'subject'), text: fileOr(f, 'text'), from: one(f, 'from'), audienceId: one(f, 'audience') });
|
|
131
|
+
const recordList = (d) => d.records.map(r => {
|
|
132
|
+
const state = r.verified ? 'found' : r.found?.length ? `wrong value, found ${r.found.join(', ')}` : 'missing';
|
|
133
|
+
return `[${state}] ${r.type} ${r.name}${r.required ? '' : ' (recommended)'}\n ${r.priority !== undefined ? `${r.priority} ` : ''}${r.value}`;
|
|
134
|
+
}).join('\n');
|
|
135
|
+
const domainView = (d) => `${d.name} (${d.id}): ${d.sendingStatus === 'verified' ? 'verified, ready to send' : d.sendingStatus}\n\n${recordList(d)}`;
|
|
136
|
+
const KEEPS_CHECKING = 'Mailumi keeps checking every 5 minutes and emails you once the domain is verified.';
|
|
90
137
|
const table = (rows, columns) => {
|
|
91
138
|
if (!rows.length)
|
|
92
139
|
return '(none)';
|
|
@@ -134,6 +181,100 @@ async function run(argv) {
|
|
|
134
181
|
result = await client.domains.list();
|
|
135
182
|
print = l => table(l.data, ['name', 'sendingStatus', 'receivingStatus', 'id']);
|
|
136
183
|
break;
|
|
184
|
+
case 'domains add':
|
|
185
|
+
need(1, 'domains add <name>');
|
|
186
|
+
result = await client.domains.create({ name: rest[0] });
|
|
187
|
+
print = d => `Added ${d.name} (${d.id}). Publish these DNS records at your DNS provider:\n\n${recordList(d)}\n\nThen run: mailumi domains verify ${d.id} --wait`;
|
|
188
|
+
break;
|
|
189
|
+
case 'domains get':
|
|
190
|
+
need(1, 'domains get <id>');
|
|
191
|
+
result = await client.domains.get(rest[0]);
|
|
192
|
+
print = domainView;
|
|
193
|
+
break;
|
|
194
|
+
case 'domains verify': {
|
|
195
|
+
need(1, 'domains verify <id> [--wait]');
|
|
196
|
+
const until = Date.now() + 30 * 60000;
|
|
197
|
+
for (;;) {
|
|
198
|
+
result = await client.domains.verify(rest[0]);
|
|
199
|
+
if (!flags.wait || result.error || result.data.ready || result.data.domain.sendingStatus === 'failed' || Date.now() > until)
|
|
200
|
+
break;
|
|
201
|
+
if (!json)
|
|
202
|
+
console.error(`${result.data.message} Checking again in 15 seconds…`);
|
|
203
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
204
|
+
}
|
|
205
|
+
print = v => `${v.message}\n\n${domainView(v.domain)}${v.ready ? '' : `\n\n${KEEPS_CHECKING}`}`;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 'domains update':
|
|
209
|
+
need(1, 'domains update <id> [--receiving on|off] [--mx-consent] [--open-tracking on|off] [--click-tracking on|off]');
|
|
210
|
+
result = await client.domains.update({ id: rest[0], receivingEnabled: toggle(flags, 'receiving'), mxConsent: flags['mx-consent'] ? true : undefined, openTracking: toggle(flags, 'open-tracking'), clickTracking: toggle(flags, 'click-tracking') });
|
|
211
|
+
print = d => `Updated ${d.name}: receiving ${d.receivingEnabled ? 'on' : 'off'}, open tracking ${d.openTracking ? 'on' : 'off'}, click tracking ${d.clickTracking ? 'on' : 'off'}.`;
|
|
212
|
+
break;
|
|
213
|
+
case 'domains remove':
|
|
214
|
+
need(1, 'domains remove <id>');
|
|
215
|
+
result = await client.domains.remove(rest[0]);
|
|
216
|
+
print = d => `Removed domain ${d.id}.`;
|
|
217
|
+
break;
|
|
218
|
+
case 'webhooks list':
|
|
219
|
+
result = await client.webhooks.list();
|
|
220
|
+
print = l => table(l.data, ['id', 'name', 'url', 'events', 'enabled']);
|
|
221
|
+
break;
|
|
222
|
+
case 'webhooks create': {
|
|
223
|
+
need(1, 'webhooks create <url> --name <name>');
|
|
224
|
+
const name = one(flags, 'name');
|
|
225
|
+
if (!name)
|
|
226
|
+
throw new Error('webhooks create needs --name.');
|
|
227
|
+
result = await client.webhooks.create({ url: rest[0], name, events: many(flags, 'events'), domainId: one(flags, 'domain'), recipient: one(flags, 'recipient') });
|
|
228
|
+
print = w => `Created webhook ${w.name} (${w.id}) for ${w.events.join(', ')}.\n\nSigning secret, shown only now. Store it as MAILUMI_WEBHOOK_SECRET:\n${w.secret}\n\nSend a test event with: mailumi webhooks test ${w.id}`;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'webhooks test':
|
|
232
|
+
need(1, 'webhooks test <id>');
|
|
233
|
+
result = await client.webhooks.test(rest[0]);
|
|
234
|
+
print = d => d.status === 'delivered' ? `Delivered test event ${d.id} (HTTP ${d.response_code}).` : `The test event ${d.id} is ${d.status}${d.response_code ? ` (HTTP ${d.response_code})` : ''}${d.last_error ? `: ${d.last_error}` : ''}.${d.next_attempt_at ? ` Next attempt at ${d.next_attempt_at}.` : ''}`;
|
|
235
|
+
break;
|
|
236
|
+
case 'webhooks deliveries':
|
|
237
|
+
need(1, 'webhooks deliveries <id>');
|
|
238
|
+
result = await client.webhooks.deliveries(rest[0]);
|
|
239
|
+
print = l => table(l.data, ['id', 'event', 'status', 'attempts', 'response_code', 'created_at']);
|
|
240
|
+
break;
|
|
241
|
+
case 'webhooks retry':
|
|
242
|
+
need(2, 'webhooks retry <id> <delivery_id>');
|
|
243
|
+
result = await client.webhooks.retry({ id: rest[0], deliveryId: rest[1] });
|
|
244
|
+
print = d => `Delivery ${d.id} is ${d.status}${d.response_code ? ` (HTTP ${d.response_code})` : ''}.`;
|
|
245
|
+
break;
|
|
246
|
+
case 'webhooks disable':
|
|
247
|
+
need(1, 'webhooks disable <id>');
|
|
248
|
+
result = await client.webhooks.disable(rest[0]);
|
|
249
|
+
print = w => `Disabled webhook ${w.name} (${w.id}).`;
|
|
250
|
+
break;
|
|
251
|
+
case 'templates list':
|
|
252
|
+
result = await client.templates.list();
|
|
253
|
+
print = l => table(l.data, ['id', 'name', 'subject', 'updated_at']);
|
|
254
|
+
break;
|
|
255
|
+
case 'templates get':
|
|
256
|
+
need(1, 'templates get <id>');
|
|
257
|
+
result = await client.templates.get(rest[0]);
|
|
258
|
+
print = t => [`${t.name} (${t.id})`, `Subject: ${t.subject}`, `Variables: ${Object.entries(t.variables).map(([k, v]) => v ? `${k}=${v}` : k).join(', ') || 'none'}`, '', t.html].join('\n');
|
|
259
|
+
break;
|
|
260
|
+
case 'templates create': {
|
|
261
|
+
const t = templateOptions(flags);
|
|
262
|
+
if (!t.name || !t.subject || !t.html)
|
|
263
|
+
throw new Error('templates create needs --name, --subject and --html or --html-file.');
|
|
264
|
+
result = await client.templates.create({ ...t, name: t.name, subject: t.subject, html: t.html });
|
|
265
|
+
print = t => `Created template ${t.name} (${t.id}). Send it with: mailumi send --template ${t.id} --from <address> --to <address>`;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case 'templates update':
|
|
269
|
+
need(1, 'templates update <id>');
|
|
270
|
+
result = await client.templates.update({ id: rest[0], ...templateOptions(flags) });
|
|
271
|
+
print = t => `Updated template ${t.name} (${t.id}).`;
|
|
272
|
+
break;
|
|
273
|
+
case 'templates remove':
|
|
274
|
+
need(1, 'templates remove <id>');
|
|
275
|
+
result = await client.templates.remove(rest[0]);
|
|
276
|
+
print = t => `Deleted template ${t.id}.`;
|
|
277
|
+
break;
|
|
137
278
|
case 'audiences list':
|
|
138
279
|
result = await client.audiences.list();
|
|
139
280
|
print = l => table(l.data, ['id', 'name', 'contacts', 'subscribed']);
|
|
@@ -158,6 +299,54 @@ async function run(argv) {
|
|
|
158
299
|
result = await client.contacts.update({ audienceId: rest[0], email: rest[1], unsubscribed: true });
|
|
159
300
|
print = c => `Unsubscribed ${c.email} from every audience.`;
|
|
160
301
|
break;
|
|
302
|
+
case 'campaigns list':
|
|
303
|
+
result = await client.campaigns.list();
|
|
304
|
+
print = l => table(l.data.map((c) => ({ ...c, delivered: c.stats?.delivered ?? '' })), ['id', 'name', 'status', 'recipients', 'delivered', 'updated_at']);
|
|
305
|
+
break;
|
|
306
|
+
case 'campaigns get':
|
|
307
|
+
need(1, 'campaigns get <id>');
|
|
308
|
+
result = await client.campaigns.get(rest[0]);
|
|
309
|
+
print = c => [`${c.name} (${c.id}): ${c.status}`, `From: ${c.from ?? 'not set'}`, `Audience: ${c.audience_id ?? 'not set'}`, `Subject: ${c.subject}`,
|
|
310
|
+
...(c.readiness ? ['', c.readiness.missing.length ? `Before sending: ${c.readiness.missing.join(' ')}` : `Ready to send to ${c.readiness.recipients} contacts: mailumi campaigns send ${c.id}`] : []),
|
|
311
|
+
...(c.stats ? ['', Object.entries(c.stats).map(([k, v]) => `${k} ${v}`).join(', ')] : []), '', c.text].join('\n');
|
|
312
|
+
break;
|
|
313
|
+
case 'campaigns create': {
|
|
314
|
+
const c = campaignOptions(flags);
|
|
315
|
+
if (!c.name || !c.subject || !c.text)
|
|
316
|
+
throw new Error('campaigns create needs --name, --subject and --text or --text-file.');
|
|
317
|
+
result = await client.campaigns.create({ ...c, name: c.name, subject: c.subject, text: c.text });
|
|
318
|
+
print = c => `Created draft ${c.name} (${c.id}). Check it with: mailumi campaigns get ${c.id}`;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'campaigns update':
|
|
322
|
+
need(1, 'campaigns update <id>');
|
|
323
|
+
result = await client.campaigns.update({ id: rest[0], ...campaignOptions(flags) });
|
|
324
|
+
print = c => `Updated draft ${c.name} (${c.id}).`;
|
|
325
|
+
break;
|
|
326
|
+
case 'campaigns send':
|
|
327
|
+
need(1, 'campaigns send <id>');
|
|
328
|
+
result = await client.campaigns.send(rest[0]);
|
|
329
|
+
print = c => `Sending ${c.name} to ${c.recipients} contacts. Follow it with: mailumi campaigns get ${c.id}`;
|
|
330
|
+
break;
|
|
331
|
+
case 'campaigns duplicate':
|
|
332
|
+
need(1, 'campaigns duplicate <id>');
|
|
333
|
+
result = await client.campaigns.duplicate(rest[0]);
|
|
334
|
+
print = c => `Created draft ${c.name} (${c.id}).`;
|
|
335
|
+
break;
|
|
336
|
+
case 'campaigns remove':
|
|
337
|
+
need(1, 'campaigns remove <id>');
|
|
338
|
+
result = await client.campaigns.remove(rest[0]);
|
|
339
|
+
print = c => `Deleted campaign ${c.id}.`;
|
|
340
|
+
break;
|
|
341
|
+
case 'suppressions list':
|
|
342
|
+
result = await client.suppressions.list({ search: one(flags, 'search') });
|
|
343
|
+
print = l => table(l.data, ['address', 'reason', 'detail', 'created_at']);
|
|
344
|
+
break;
|
|
345
|
+
case 'suppressions remove':
|
|
346
|
+
need(1, 'suppressions remove <address>');
|
|
347
|
+
result = await client.suppressions.remove(rest[0]);
|
|
348
|
+
print = s => `Mailumi will send to ${s.address} again.`;
|
|
349
|
+
break;
|
|
161
350
|
default:
|
|
162
351
|
throw new Error(`Unknown command "${args.join(' ')}". Run "mailumi --help".`);
|
|
163
352
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The official Mailumi SDK. Works anywhere `fetch` exists: Node.js 18+, Bun, Deno and Cloudflare Workers.
|
|
3
3
|
* Every method resolves to `{ data, error }` and never throws for API errors.
|
|
4
4
|
*/
|
|
5
|
-
export declare const VERSION = "1.
|
|
5
|
+
export declare const VERSION = "1.2.0";
|
|
6
6
|
export type ErrorCode = 'invalid_request' | 'unauthorized' | 'plan_limit_reached' | 'forbidden' | 'not_found' | 'method_not_allowed' | 'conflict' | 'payload_too_large' | 'rate_limited' | 'internal_error' | 'service_unavailable' | 'network_error' | 'missing_api_key' | 'invalid_signature' | 'missing_webhook_secret';
|
|
7
7
|
export interface MailumiError {
|
|
8
8
|
name: 'MailumiError';
|
|
@@ -116,10 +116,13 @@ export interface DnsRecord {
|
|
|
116
116
|
value: string;
|
|
117
117
|
priority?: number;
|
|
118
118
|
verified: boolean;
|
|
119
|
+
/** What DNS returned at this name when it does not match `value`. Only on records that are not verified. */
|
|
120
|
+
found?: string[];
|
|
119
121
|
}
|
|
120
122
|
export interface Domain {
|
|
121
123
|
id: string;
|
|
122
124
|
name: string;
|
|
125
|
+
region?: string;
|
|
123
126
|
ownershipVerified?: boolean;
|
|
124
127
|
sendingEnabled?: boolean;
|
|
125
128
|
receivingEnabled?: boolean;
|
|
@@ -131,6 +134,125 @@ export interface Domain {
|
|
|
131
134
|
createdAt?: number;
|
|
132
135
|
records: DnsRecord[];
|
|
133
136
|
}
|
|
137
|
+
export interface DomainVerification {
|
|
138
|
+
domain: Domain;
|
|
139
|
+
/** `true` once the domain can send. */
|
|
140
|
+
ready: boolean;
|
|
141
|
+
/** What is still missing, or that the domain is ready. */
|
|
142
|
+
message: string;
|
|
143
|
+
/** How many required records have a different value at the DNS provider. */
|
|
144
|
+
wrong?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface DomainSettings {
|
|
147
|
+
id: string;
|
|
148
|
+
/** Receive email for the domain and send it to your email.received webhooks. */
|
|
149
|
+
receivingEnabled?: boolean;
|
|
150
|
+
/** Must be `true` to turn receiving on: the MX record replaces any mailbox provider the domain uses now. */
|
|
151
|
+
mxConsent?: boolean;
|
|
152
|
+
/** Pro and Scale plans. */
|
|
153
|
+
openTracking?: boolean;
|
|
154
|
+
/** Pro and Scale plans. */
|
|
155
|
+
clickTracking?: boolean;
|
|
156
|
+
}
|
|
157
|
+
export type WebhookSubscription = Exclude<WebhookEventType, 'webhook.test'>;
|
|
158
|
+
export interface NewWebhook {
|
|
159
|
+
name: string;
|
|
160
|
+
/** A public HTTPS URL on port 443. */
|
|
161
|
+
url: string;
|
|
162
|
+
/** Defaults to `['email.received']`. */
|
|
163
|
+
events?: WebhookSubscription[];
|
|
164
|
+
/** Only send events for this domain. Leave it out for every domain. */
|
|
165
|
+
domainId?: string;
|
|
166
|
+
/** For email.received: only mail to this address before the @, for example "support". */
|
|
167
|
+
recipient?: string;
|
|
168
|
+
}
|
|
169
|
+
export interface Webhook {
|
|
170
|
+
object: 'webhook';
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
url: string;
|
|
174
|
+
events: WebhookSubscription[];
|
|
175
|
+
domain_id: string | null;
|
|
176
|
+
recipient: string | null;
|
|
177
|
+
enabled: boolean;
|
|
178
|
+
created_at: string;
|
|
179
|
+
}
|
|
180
|
+
export interface WebhookDelivery {
|
|
181
|
+
/** Also the webhook-id header of the request. */
|
|
182
|
+
id: string;
|
|
183
|
+
object: 'webhook_delivery';
|
|
184
|
+
webhook_id: string;
|
|
185
|
+
event: WebhookEventType;
|
|
186
|
+
email_id: string | null;
|
|
187
|
+
status: 'pending' | 'delivered' | 'failed' | 'cancelled';
|
|
188
|
+
attempts: number;
|
|
189
|
+
response_code: number | null;
|
|
190
|
+
last_error: string | null;
|
|
191
|
+
next_attempt_at: string | null;
|
|
192
|
+
created_at: string;
|
|
193
|
+
delivered_at: string | null;
|
|
194
|
+
}
|
|
195
|
+
export interface NewTemplate {
|
|
196
|
+
name: string;
|
|
197
|
+
/** Placeholders such as `{{first_name}}` work in the subject and HTML. */
|
|
198
|
+
subject: string;
|
|
199
|
+
html: string;
|
|
200
|
+
/** Default values for placeholders an email does not pass. */
|
|
201
|
+
variables?: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
export interface Template {
|
|
204
|
+
object: 'template';
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
subject: string;
|
|
208
|
+
html: string;
|
|
209
|
+
variables: Record<string, string>;
|
|
210
|
+
created_at: string;
|
|
211
|
+
updated_at: string;
|
|
212
|
+
}
|
|
213
|
+
export interface NewCampaign {
|
|
214
|
+
name: string;
|
|
215
|
+
/** `{{first_name}}`, `{{last_name}}` and `{{email}}` are filled in per contact. */
|
|
216
|
+
subject: string;
|
|
217
|
+
/** Plain text. Blank lines start paragraphs and links become clickable. Mailumi adds your postal address and an unsubscribe link. */
|
|
218
|
+
text: string;
|
|
219
|
+
/** A sender on a verified domain. Needed before sending. */
|
|
220
|
+
from?: string;
|
|
221
|
+
/** Needed before sending. */
|
|
222
|
+
audienceId?: string | null;
|
|
223
|
+
}
|
|
224
|
+
export type CampaignStats = Record<'total' | 'pending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'opened' | 'clicked' | 'unsubscribed', number>;
|
|
225
|
+
export interface Campaign {
|
|
226
|
+
object: 'campaign';
|
|
227
|
+
id: string;
|
|
228
|
+
name: string;
|
|
229
|
+
subject: string;
|
|
230
|
+
text: string;
|
|
231
|
+
from: string | null;
|
|
232
|
+
audience_id: string | null;
|
|
233
|
+
status: 'draft' | 'sending' | 'sent' | 'failed';
|
|
234
|
+
/** How many contacts it was sent to. */
|
|
235
|
+
recipients: number;
|
|
236
|
+
error: string | null;
|
|
237
|
+
sent_at: string | null;
|
|
238
|
+
created_at: string;
|
|
239
|
+
updated_at: string;
|
|
240
|
+
/** Drafts, from `campaigns.get`: how many contacts it would go to now, and what to fix before sending. */
|
|
241
|
+
readiness?: {
|
|
242
|
+
recipients: number;
|
|
243
|
+
missing: string[];
|
|
244
|
+
};
|
|
245
|
+
/** Campaigns that were sent. */
|
|
246
|
+
stats?: CampaignStats;
|
|
247
|
+
}
|
|
248
|
+
export interface Suppression {
|
|
249
|
+
object: 'suppression';
|
|
250
|
+
address: string;
|
|
251
|
+
reason: 'bounce' | 'complaint' | 'manual';
|
|
252
|
+
detail: string | null;
|
|
253
|
+
email_id: string | null;
|
|
254
|
+
created_at: string;
|
|
255
|
+
}
|
|
134
256
|
export interface Audience {
|
|
135
257
|
object: 'audience';
|
|
136
258
|
id: string;
|
|
@@ -189,6 +311,105 @@ export interface WebhookEvent<T = Record<string, any>> {
|
|
|
189
311
|
created_at: string;
|
|
190
312
|
data: T;
|
|
191
313
|
}
|
|
314
|
+
interface EmailFields {
|
|
315
|
+
email_id: string;
|
|
316
|
+
domain_id: string;
|
|
317
|
+
from: string;
|
|
318
|
+
to: string[];
|
|
319
|
+
subject: string;
|
|
320
|
+
last_event: string;
|
|
321
|
+
tags: Record<string, string>;
|
|
322
|
+
created_at: string;
|
|
323
|
+
}
|
|
324
|
+
export interface ReceivedAttachment {
|
|
325
|
+
id: string;
|
|
326
|
+
filename: string;
|
|
327
|
+
content_type: string;
|
|
328
|
+
/** Set for inline images that the HTML shows with `cid:`. */
|
|
329
|
+
content_id: string | null;
|
|
330
|
+
/** Size in bytes. */
|
|
331
|
+
size: number;
|
|
332
|
+
/** Download it server-side with `mailumi.emails.attachment({ id: email_id, index })` or a key with email:read. */
|
|
333
|
+
download_url: string;
|
|
334
|
+
}
|
|
335
|
+
/** `data` of an `email.received` event. */
|
|
336
|
+
export interface EmailReceivedData extends EmailFields {
|
|
337
|
+
direction: 'inbound';
|
|
338
|
+
status: 'received';
|
|
339
|
+
scheduled_at: null;
|
|
340
|
+
error: null;
|
|
341
|
+
/** `Name <address>`, or only the address when the email has no name. */
|
|
342
|
+
from: string;
|
|
343
|
+
/** The recipients at this domain, in lowercase. */
|
|
344
|
+
to: string[];
|
|
345
|
+
cc: string[];
|
|
346
|
+
reply_to: string[];
|
|
347
|
+
/** The Message-ID header with its angle brackets, for In-Reply-To and References when you reply. */
|
|
348
|
+
message_id: string | null;
|
|
349
|
+
/** Empty when the email has no plain-text part. */
|
|
350
|
+
text: string;
|
|
351
|
+
/** Empty when the email has no HTML part. It is the sender's HTML: sanitize it before showing it. */
|
|
352
|
+
html: string;
|
|
353
|
+
/** `true` when the Amazon SES spam check flagged the email. */
|
|
354
|
+
spam: boolean;
|
|
355
|
+
attachments: ReceivedAttachment[];
|
|
356
|
+
}
|
|
357
|
+
/** `data` of an event about an email you sent: the email as it is now, plus the detail for that event. */
|
|
358
|
+
export interface EmailEventData extends EmailFields {
|
|
359
|
+
direction: 'outbound';
|
|
360
|
+
status: string;
|
|
361
|
+
scheduled_at: string | null;
|
|
362
|
+
error: string | null;
|
|
363
|
+
/** The recipient the event is about. Not set on email.sent and email.failed, nor on opens and clicks of an email with more than one recipient. */
|
|
364
|
+
recipient?: string;
|
|
365
|
+
delivery?: {
|
|
366
|
+
smtp_response: string | null;
|
|
367
|
+
};
|
|
368
|
+
delay?: {
|
|
369
|
+
type: string | null;
|
|
370
|
+
expires_at: string | null;
|
|
371
|
+
message: string | null;
|
|
372
|
+
};
|
|
373
|
+
bounce?: {
|
|
374
|
+
type: 'Permanent' | 'Transient' | (string & {}) | null;
|
|
375
|
+
sub_type: string | null;
|
|
376
|
+
message: string | null;
|
|
377
|
+
};
|
|
378
|
+
complaint?: {
|
|
379
|
+
type: string | null;
|
|
380
|
+
};
|
|
381
|
+
open?: {
|
|
382
|
+
ip_address: string | null;
|
|
383
|
+
user_agent: string | null;
|
|
384
|
+
};
|
|
385
|
+
click?: {
|
|
386
|
+
link: string | null;
|
|
387
|
+
ip_address: string | null;
|
|
388
|
+
user_agent: string | null;
|
|
389
|
+
};
|
|
390
|
+
failed?: {
|
|
391
|
+
reason: string;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** The event `verifyWebhook` returns. Check `event.type` and `event.data` has the matching fields. */
|
|
395
|
+
export type MailumiWebhookEvent = {
|
|
396
|
+
id: string;
|
|
397
|
+
type: 'email.received';
|
|
398
|
+
created_at: string;
|
|
399
|
+
data: EmailReceivedData;
|
|
400
|
+
} | {
|
|
401
|
+
id: string;
|
|
402
|
+
type: Exclude<WebhookEventType, 'email.received' | 'webhook.test'>;
|
|
403
|
+
created_at: string;
|
|
404
|
+
data: EmailEventData;
|
|
405
|
+
} | {
|
|
406
|
+
id: string;
|
|
407
|
+
type: 'webhook.test';
|
|
408
|
+
created_at: string;
|
|
409
|
+
data: {
|
|
410
|
+
message: string;
|
|
411
|
+
};
|
|
412
|
+
};
|
|
192
413
|
export interface VerifyWebhookOptions {
|
|
193
414
|
/** The raw request body exactly as received, before any JSON parsing. */
|
|
194
415
|
payload: string | Uint8Array;
|
|
@@ -203,7 +424,8 @@ export interface VerifyWebhookOptions {
|
|
|
203
424
|
* Checks the Standard Webhooks signature of a Mailumi webhook and returns the parsed event.
|
|
204
425
|
* Store `event.id` and skip ids you have seen: a delivery can arrive more than once.
|
|
205
426
|
*/
|
|
206
|
-
export declare function verifyWebhook
|
|
427
|
+
export declare function verifyWebhook(options: VerifyWebhookOptions): Promise<Result<MailumiWebhookEvent>>;
|
|
428
|
+
export declare function verifyWebhook<T>(options: VerifyWebhookOptions): Promise<Result<WebhookEvent<T>>>;
|
|
207
429
|
/** Converts the SDK's camelCase options to the API's request body. snake_case keys are accepted too. */
|
|
208
430
|
export declare function toApiEmail(o: SendEmailOptions & Record<string, any>): Record<string, unknown>;
|
|
209
431
|
export declare class Mailumi {
|
|
@@ -247,6 +469,54 @@ export declare class Mailumi {
|
|
|
247
469
|
list: () => Promise<Result<{
|
|
248
470
|
data: Domain[];
|
|
249
471
|
}>>;
|
|
472
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
473
|
+
create: ({ name }: {
|
|
474
|
+
name: string;
|
|
475
|
+
}) => Promise<Result<Domain>>;
|
|
476
|
+
get: (id: string) => Promise<Result<Domain>>;
|
|
477
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
478
|
+
verify: (id: string) => Promise<Result<DomainVerification>>;
|
|
479
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }: DomainSettings & Record<string, any>) => Promise<Result<Domain>>;
|
|
480
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
481
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
482
|
+
};
|
|
483
|
+
readonly webhooks: {
|
|
484
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
485
|
+
verify: typeof verifyWebhook;
|
|
486
|
+
list: () => Promise<Result<{
|
|
487
|
+
data: Webhook[];
|
|
488
|
+
}>>;
|
|
489
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
490
|
+
create: (webhook: NewWebhook) => Promise<Result<Webhook & {
|
|
491
|
+
secret: string;
|
|
492
|
+
}>>;
|
|
493
|
+
get: (id: string) => Promise<Result<Webhook>>;
|
|
494
|
+
/** Stops sending events to the webhook. */
|
|
495
|
+
disable: (id: string) => Promise<Result<Webhook>>;
|
|
496
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
497
|
+
test: (id: string) => Promise<Result<WebhookDelivery>>;
|
|
498
|
+
/** The 100 most recent deliveries, newest first. */
|
|
499
|
+
deliveries: (id: string) => Promise<Result<{
|
|
500
|
+
data: WebhookDelivery[];
|
|
501
|
+
}>>;
|
|
502
|
+
/** Delivers an event again now. */
|
|
503
|
+
retry: ({ id, deliveryId }: {
|
|
504
|
+
id: string;
|
|
505
|
+
deliveryId: string;
|
|
506
|
+
}) => Promise<Result<WebhookDelivery>>;
|
|
507
|
+
};
|
|
508
|
+
readonly templates: {
|
|
509
|
+
list: () => Promise<Result<{
|
|
510
|
+
data: Template[];
|
|
511
|
+
}>>;
|
|
512
|
+
/** Saves a template to send with `templateId`. */
|
|
513
|
+
create: (template: NewTemplate) => Promise<Result<Template>>;
|
|
514
|
+
get: (id: string) => Promise<Result<Template>>;
|
|
515
|
+
/** Changes only the fields you pass. */
|
|
516
|
+
update: ({ id, ...template }: Partial<NewTemplate> & {
|
|
517
|
+
id: string;
|
|
518
|
+
}) => Promise<Result<Template>>;
|
|
519
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
250
520
|
};
|
|
251
521
|
readonly audiences: {
|
|
252
522
|
list: () => Promise<Result<{
|
|
@@ -291,9 +561,38 @@ export declare class Mailumi {
|
|
|
291
561
|
}) => Promise<Result<Contact>>;
|
|
292
562
|
remove: (ref: ContactRef) => Promise<Result<Deleted>>;
|
|
293
563
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
564
|
+
readonly campaigns: {
|
|
565
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
566
|
+
list: () => Promise<Result<{
|
|
567
|
+
data: Campaign[];
|
|
568
|
+
}>>;
|
|
569
|
+
/** Creates a draft. */
|
|
570
|
+
create: (campaign: NewCampaign) => Promise<Result<Campaign>>;
|
|
571
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
572
|
+
get: (id: string) => Promise<Result<Campaign>>;
|
|
573
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
574
|
+
update: ({ id, ...campaign }: Partial<NewCampaign> & {
|
|
575
|
+
id: string;
|
|
576
|
+
}) => Promise<Result<Campaign>>;
|
|
577
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
578
|
+
send: (id: string) => Promise<Result<Campaign>>;
|
|
579
|
+
/** Copies a campaign into a new draft. */
|
|
580
|
+
duplicate: (id: string) => Promise<Result<Campaign>>;
|
|
581
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
582
|
+
};
|
|
583
|
+
readonly suppressions: {
|
|
584
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
585
|
+
list: (q?: {
|
|
586
|
+
search?: string;
|
|
587
|
+
}) => Promise<Result<{
|
|
588
|
+
data: Suppression[];
|
|
589
|
+
}>>;
|
|
590
|
+
/** Allows sending to the address again. */
|
|
591
|
+
remove: (address: string) => Promise<Result<{
|
|
592
|
+
object: "suppression";
|
|
593
|
+
address: string;
|
|
594
|
+
deleted: true;
|
|
595
|
+
}>>;
|
|
297
596
|
};
|
|
298
597
|
private readonly key;
|
|
299
598
|
private readonly baseUrl;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The official Mailumi SDK. Works anywhere `fetch` exists: Node.js 18+, Bun, Deno and Cloudflare Workers.
|
|
3
3
|
* Every method resolves to `{ data, error }` and never throws for API errors.
|
|
4
4
|
*/
|
|
5
|
-
export const VERSION = '1.
|
|
5
|
+
export const VERSION = '1.2.0';
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://mailumi.com/api/v1';
|
|
7
7
|
function header(h, name, short) {
|
|
8
8
|
if (typeof h.get === 'function')
|
|
@@ -24,10 +24,6 @@ function sameText(a, b) {
|
|
|
24
24
|
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
25
25
|
return diff === 0;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Checks the Standard Webhooks signature of a Mailumi webhook and returns the parsed event.
|
|
29
|
-
* Store `event.id` and skip ids you have seen: a delivery can arrive more than once.
|
|
30
|
-
*/
|
|
31
27
|
export async function verifyWebhook({ payload, headers, secret, tolerance = 300 }) {
|
|
32
28
|
const key = secret ?? globalThis.process?.env?.MAILUMI_WEBHOOK_SECRET;
|
|
33
29
|
if (!key)
|
|
@@ -80,10 +76,11 @@ export function toApiEmail(o) {
|
|
|
80
76
|
}
|
|
81
77
|
const toApiContact = (c) => ({ email: c.email, first_name: c.firstName ?? c.first_name, last_name: c.lastName ?? c.last_name, unsubscribed: c.unsubscribed });
|
|
82
78
|
const contactPath = (r) => `/audiences/${encodeURIComponent(r.audienceId)}/contacts/${encodeURIComponent(r.id ?? r.email ?? '')}`;
|
|
79
|
+
const toApiWebhook = (w) => ({ name: w.name, url: w.url, events: w.events, domain_id: w.domainId ?? w.domain_id, recipient: w.recipient });
|
|
80
|
+
const toApiCampaign = (c) => ({ name: c.name, subject: c.subject, text: c.text, from: c.from, audience_id: c.audienceId !== undefined ? c.audienceId : c.audience_id });
|
|
81
|
+
const pathTo = (base, id, rest = '') => `${base}/${encodeURIComponent(id)}${rest}`;
|
|
83
82
|
export class Mailumi {
|
|
84
83
|
constructor(apiKey, options = {}) {
|
|
85
|
-
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
86
|
-
this.webhooks = { verify: verifyWebhook };
|
|
87
84
|
const env = globalThis.process?.env ?? {};
|
|
88
85
|
this.key = apiKey ?? env.MAILUMI_API_KEY ?? '';
|
|
89
86
|
this.baseUrl = (options.baseUrl ?? env.MAILUMI_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
@@ -111,6 +108,62 @@ export class Mailumi {
|
|
|
111
108
|
this.domains = {
|
|
112
109
|
/** Lists your domains, their DNS records and whether each is verified. */
|
|
113
110
|
list: () => this.request('GET', '/domains'),
|
|
111
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
112
|
+
create: ({ name }) => this.request('POST', '/domains', { name }),
|
|
113
|
+
get: (id) => this.request('GET', pathTo('/domains', id)),
|
|
114
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
115
|
+
verify: (id) => this.request('POST', pathTo('/domains', id, '/verify')),
|
|
116
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }) => this.request('PATCH', pathTo('/domains', id), {
|
|
117
|
+
receiving_enabled: receivingEnabled ?? rest.receiving_enabled, mx_consent: mxConsent ?? rest.mx_consent, open_tracking: openTracking ?? rest.open_tracking, click_tracking: clickTracking ?? rest.click_tracking,
|
|
118
|
+
}),
|
|
119
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
120
|
+
remove: (id) => this.request('DELETE', pathTo('/domains', id)),
|
|
121
|
+
};
|
|
122
|
+
this.webhooks = {
|
|
123
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
124
|
+
verify: verifyWebhook,
|
|
125
|
+
list: () => this.request('GET', '/webhooks'),
|
|
126
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
127
|
+
create: (webhook) => this.request('POST', '/webhooks', toApiWebhook(webhook)),
|
|
128
|
+
get: (id) => this.request('GET', pathTo('/webhooks', id)),
|
|
129
|
+
/** Stops sending events to the webhook. */
|
|
130
|
+
disable: (id) => this.request('DELETE', pathTo('/webhooks', id)),
|
|
131
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
132
|
+
test: (id) => this.request('POST', pathTo('/webhooks', id, '/test')),
|
|
133
|
+
/** The 100 most recent deliveries, newest first. */
|
|
134
|
+
deliveries: (id) => this.request('GET', pathTo('/webhooks', id, '/deliveries')),
|
|
135
|
+
/** Delivers an event again now. */
|
|
136
|
+
retry: ({ id, deliveryId }) => this.request('POST', pathTo('/webhooks', id, `/deliveries/${encodeURIComponent(deliveryId)}/retry`)),
|
|
137
|
+
};
|
|
138
|
+
this.templates = {
|
|
139
|
+
list: () => this.request('GET', '/templates'),
|
|
140
|
+
/** Saves a template to send with `templateId`. */
|
|
141
|
+
create: (template) => this.request('POST', '/templates', template),
|
|
142
|
+
get: (id) => this.request('GET', pathTo('/templates', id)),
|
|
143
|
+
/** Changes only the fields you pass. */
|
|
144
|
+
update: ({ id, ...template }) => this.request('PATCH', pathTo('/templates', id), template),
|
|
145
|
+
remove: (id) => this.request('DELETE', pathTo('/templates', id)),
|
|
146
|
+
};
|
|
147
|
+
this.campaigns = {
|
|
148
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
149
|
+
list: () => this.request('GET', '/campaigns'),
|
|
150
|
+
/** Creates a draft. */
|
|
151
|
+
create: (campaign) => this.request('POST', '/campaigns', toApiCampaign(campaign)),
|
|
152
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
153
|
+
get: (id) => this.request('GET', pathTo('/campaigns', id)),
|
|
154
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
155
|
+
update: ({ id, ...campaign }) => this.request('PATCH', pathTo('/campaigns', id), toApiCampaign(campaign)),
|
|
156
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
157
|
+
send: (id) => this.request('POST', pathTo('/campaigns', id, '/send')),
|
|
158
|
+
/** Copies a campaign into a new draft. */
|
|
159
|
+
duplicate: (id) => this.request('POST', pathTo('/campaigns', id, '/duplicate')),
|
|
160
|
+
remove: (id) => this.request('DELETE', pathTo('/campaigns', id)),
|
|
161
|
+
};
|
|
162
|
+
this.suppressions = {
|
|
163
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
164
|
+
list: (q = {}) => this.request('GET', '/suppressions' + query(q)),
|
|
165
|
+
/** Allows sending to the address again. */
|
|
166
|
+
remove: (address) => this.request('DELETE', pathTo('/suppressions', address)),
|
|
114
167
|
};
|
|
115
168
|
this.audiences = {
|
|
116
169
|
list: () => this.request('GET', '/audiences'),
|
package/package.json
CHANGED