mailumi 1.1.0 → 1.2.1
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 +73 -5
- package/dist/cjs/index.d.ts +207 -4
- package/dist/cjs/index.js +60 -3
- package/dist/cli.js +191 -1
- package/dist/index.d.ts +207 -4
- package/dist/index.js +60 -3
- 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`.
|
|
@@ -146,9 +187,9 @@ export async function POST(request: Request) {
|
|
|
146
187
|
|
|
147
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`.
|
|
148
189
|
|
|
149
|
-
## Audiences and
|
|
190
|
+
## Audiences, contacts and campaigns
|
|
150
191
|
|
|
151
|
-
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`.
|
|
152
193
|
|
|
153
194
|
```ts
|
|
154
195
|
const { data: audience } = await mailumi.audiences.create({ name: 'Newsletter' });
|
|
@@ -162,6 +203,25 @@ const { data } = await mailumi.contacts.list({ audienceId: audience.id, search:
|
|
|
162
203
|
|
|
163
204
|
Unsubscribing applies to the address in every audience of the workspace, and adding or importing a contact never resubscribes it.
|
|
164
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
|
+
|
|
165
225
|
## Errors
|
|
166
226
|
|
|
167
227
|
```ts
|
|
@@ -179,7 +239,7 @@ if (error) {
|
|
|
179
239
|
| `unauthorized` | The API key is missing, wrong, revoked or expired. |
|
|
180
240
|
| `forbidden` | The key lacks the permission, or is limited to another domain. |
|
|
181
241
|
| `plan_limit_reached` | The monthly or daily email allowance is used up. |
|
|
182
|
-
| `not_found` | The email, domain, audience or
|
|
242
|
+
| `not_found` | The email, domain, webhook, template, audience, contact or campaign does not exist. |
|
|
183
243
|
| `conflict` | The `Idempotency-Key` was used for a different request. |
|
|
184
244
|
| `rate_limited` | Too many requests; retried automatically. |
|
|
185
245
|
| `internal_error`, `service_unavailable` | A temporary problem; retried automatically. |
|
|
@@ -208,15 +268,23 @@ npx mailumi emails list
|
|
|
208
268
|
npx mailumi emails get <id>
|
|
209
269
|
npx mailumi emails cancel <id>
|
|
210
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
|
|
211
276
|
npx mailumi audiences list
|
|
212
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
|
|
213
281
|
```
|
|
214
282
|
|
|
215
283
|
Add `--json` for machine-readable output and `--help` for every command and option. The CLI exits with code 1 on errors.
|
|
216
284
|
|
|
217
285
|
## AI coding assistants
|
|
218
286
|
|
|
219
|
-
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).
|
|
220
288
|
|
|
221
289
|
## Links
|
|
222
290
|
|
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.1
|
|
5
|
+
export declare const VERSION = "1.2.1";
|
|
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,15 @@ 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[];
|
|
121
|
+
/** Where a missing record was found instead: its name with the domain added twice, because the DNS provider adds the domain itself. Enter only the part before the domain. */
|
|
122
|
+
foundAt?: string;
|
|
119
123
|
}
|
|
120
124
|
export interface Domain {
|
|
121
125
|
id: string;
|
|
122
126
|
name: string;
|
|
127
|
+
region?: string;
|
|
123
128
|
ownershipVerified?: boolean;
|
|
124
129
|
sendingEnabled?: boolean;
|
|
125
130
|
receivingEnabled?: boolean;
|
|
@@ -131,6 +136,127 @@ export interface Domain {
|
|
|
131
136
|
createdAt?: number;
|
|
132
137
|
records: DnsRecord[];
|
|
133
138
|
}
|
|
139
|
+
export interface DomainVerification {
|
|
140
|
+
domain: Domain;
|
|
141
|
+
/** `true` once the domain can send. */
|
|
142
|
+
ready: boolean;
|
|
143
|
+
/** What is still missing, or that the domain is ready. */
|
|
144
|
+
message: string;
|
|
145
|
+
/** How many required records have a different value at the DNS provider. */
|
|
146
|
+
wrong?: number;
|
|
147
|
+
/** How many records were found at a doubled name; see `foundAt` on each record. */
|
|
148
|
+
misplaced?: number;
|
|
149
|
+
}
|
|
150
|
+
export interface DomainSettings {
|
|
151
|
+
id: string;
|
|
152
|
+
/** Receive email for the domain and send it to your email.received webhooks. */
|
|
153
|
+
receivingEnabled?: boolean;
|
|
154
|
+
/** Must be `true` to turn receiving on: the MX record replaces any mailbox provider the domain uses now. */
|
|
155
|
+
mxConsent?: boolean;
|
|
156
|
+
/** Pro and Scale plans. */
|
|
157
|
+
openTracking?: boolean;
|
|
158
|
+
/** Pro and Scale plans. */
|
|
159
|
+
clickTracking?: boolean;
|
|
160
|
+
}
|
|
161
|
+
export type WebhookSubscription = Exclude<WebhookEventType, 'webhook.test'>;
|
|
162
|
+
export interface NewWebhook {
|
|
163
|
+
name: string;
|
|
164
|
+
/** A public HTTPS URL on port 443. */
|
|
165
|
+
url: string;
|
|
166
|
+
/** Defaults to `['email.received']`. */
|
|
167
|
+
events?: WebhookSubscription[];
|
|
168
|
+
/** Only send events for this domain. Leave it out for every domain. */
|
|
169
|
+
domainId?: string;
|
|
170
|
+
/** For email.received: only mail to this address before the @, for example "support". */
|
|
171
|
+
recipient?: string;
|
|
172
|
+
}
|
|
173
|
+
export interface Webhook {
|
|
174
|
+
object: 'webhook';
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
url: string;
|
|
178
|
+
events: WebhookSubscription[];
|
|
179
|
+
domain_id: string | null;
|
|
180
|
+
recipient: string | null;
|
|
181
|
+
enabled: boolean;
|
|
182
|
+
created_at: string;
|
|
183
|
+
}
|
|
184
|
+
export interface WebhookDelivery {
|
|
185
|
+
/** Also the webhook-id header of the request. */
|
|
186
|
+
id: string;
|
|
187
|
+
object: 'webhook_delivery';
|
|
188
|
+
webhook_id: string;
|
|
189
|
+
event: WebhookEventType;
|
|
190
|
+
email_id: string | null;
|
|
191
|
+
status: 'pending' | 'delivered' | 'failed' | 'cancelled';
|
|
192
|
+
attempts: number;
|
|
193
|
+
response_code: number | null;
|
|
194
|
+
last_error: string | null;
|
|
195
|
+
next_attempt_at: string | null;
|
|
196
|
+
created_at: string;
|
|
197
|
+
delivered_at: string | null;
|
|
198
|
+
}
|
|
199
|
+
export interface NewTemplate {
|
|
200
|
+
name: string;
|
|
201
|
+
/** Placeholders such as `{{first_name}}` work in the subject and HTML. */
|
|
202
|
+
subject: string;
|
|
203
|
+
html: string;
|
|
204
|
+
/** Default values for placeholders an email does not pass. */
|
|
205
|
+
variables?: Record<string, string>;
|
|
206
|
+
}
|
|
207
|
+
export interface Template {
|
|
208
|
+
object: 'template';
|
|
209
|
+
id: string;
|
|
210
|
+
name: string;
|
|
211
|
+
subject: string;
|
|
212
|
+
html: string;
|
|
213
|
+
variables: Record<string, string>;
|
|
214
|
+
created_at: string;
|
|
215
|
+
updated_at: string;
|
|
216
|
+
}
|
|
217
|
+
export interface NewCampaign {
|
|
218
|
+
name: string;
|
|
219
|
+
/** `{{first_name}}`, `{{last_name}}` and `{{email}}` are filled in per contact. */
|
|
220
|
+
subject: string;
|
|
221
|
+
/** Plain text. Blank lines start paragraphs and links become clickable. Mailumi adds your postal address and an unsubscribe link. */
|
|
222
|
+
text: string;
|
|
223
|
+
/** A sender on a verified domain. Needed before sending. */
|
|
224
|
+
from?: string;
|
|
225
|
+
/** Needed before sending. */
|
|
226
|
+
audienceId?: string | null;
|
|
227
|
+
}
|
|
228
|
+
export type CampaignStats = Record<'total' | 'pending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'opened' | 'clicked' | 'unsubscribed', number>;
|
|
229
|
+
export interface Campaign {
|
|
230
|
+
object: 'campaign';
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
subject: string;
|
|
234
|
+
text: string;
|
|
235
|
+
from: string | null;
|
|
236
|
+
audience_id: string | null;
|
|
237
|
+
status: 'draft' | 'sending' | 'sent' | 'failed';
|
|
238
|
+
/** How many contacts it was sent to. */
|
|
239
|
+
recipients: number;
|
|
240
|
+
error: string | null;
|
|
241
|
+
sent_at: string | null;
|
|
242
|
+
created_at: string;
|
|
243
|
+
updated_at: string;
|
|
244
|
+
/** Drafts, from `campaigns.get`: how many contacts it would go to now, and what to fix before sending. */
|
|
245
|
+
readiness?: {
|
|
246
|
+
recipients: number;
|
|
247
|
+
missing: string[];
|
|
248
|
+
};
|
|
249
|
+
/** Campaigns that were sent. */
|
|
250
|
+
stats?: CampaignStats;
|
|
251
|
+
}
|
|
252
|
+
export interface Suppression {
|
|
253
|
+
object: 'suppression';
|
|
254
|
+
address: string;
|
|
255
|
+
reason: 'bounce' | 'complaint' | 'manual';
|
|
256
|
+
detail: string | null;
|
|
257
|
+
email_id: string | null;
|
|
258
|
+
created_at: string;
|
|
259
|
+
}
|
|
134
260
|
export interface Audience {
|
|
135
261
|
object: 'audience';
|
|
136
262
|
id: string;
|
|
@@ -347,6 +473,54 @@ export declare class Mailumi {
|
|
|
347
473
|
list: () => Promise<Result<{
|
|
348
474
|
data: Domain[];
|
|
349
475
|
}>>;
|
|
476
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
477
|
+
create: ({ name }: {
|
|
478
|
+
name: string;
|
|
479
|
+
}) => Promise<Result<Domain>>;
|
|
480
|
+
get: (id: string) => Promise<Result<Domain>>;
|
|
481
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
482
|
+
verify: (id: string) => Promise<Result<DomainVerification>>;
|
|
483
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }: DomainSettings & Record<string, any>) => Promise<Result<Domain>>;
|
|
484
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
485
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
486
|
+
};
|
|
487
|
+
readonly webhooks: {
|
|
488
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
489
|
+
verify: typeof verifyWebhook;
|
|
490
|
+
list: () => Promise<Result<{
|
|
491
|
+
data: Webhook[];
|
|
492
|
+
}>>;
|
|
493
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
494
|
+
create: (webhook: NewWebhook) => Promise<Result<Webhook & {
|
|
495
|
+
secret: string;
|
|
496
|
+
}>>;
|
|
497
|
+
get: (id: string) => Promise<Result<Webhook>>;
|
|
498
|
+
/** Stops sending events to the webhook. */
|
|
499
|
+
disable: (id: string) => Promise<Result<Webhook>>;
|
|
500
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
501
|
+
test: (id: string) => Promise<Result<WebhookDelivery>>;
|
|
502
|
+
/** The 100 most recent deliveries, newest first. */
|
|
503
|
+
deliveries: (id: string) => Promise<Result<{
|
|
504
|
+
data: WebhookDelivery[];
|
|
505
|
+
}>>;
|
|
506
|
+
/** Delivers an event again now. */
|
|
507
|
+
retry: ({ id, deliveryId }: {
|
|
508
|
+
id: string;
|
|
509
|
+
deliveryId: string;
|
|
510
|
+
}) => Promise<Result<WebhookDelivery>>;
|
|
511
|
+
};
|
|
512
|
+
readonly templates: {
|
|
513
|
+
list: () => Promise<Result<{
|
|
514
|
+
data: Template[];
|
|
515
|
+
}>>;
|
|
516
|
+
/** Saves a template to send with `templateId`. */
|
|
517
|
+
create: (template: NewTemplate) => Promise<Result<Template>>;
|
|
518
|
+
get: (id: string) => Promise<Result<Template>>;
|
|
519
|
+
/** Changes only the fields you pass. */
|
|
520
|
+
update: ({ id, ...template }: Partial<NewTemplate> & {
|
|
521
|
+
id: string;
|
|
522
|
+
}) => Promise<Result<Template>>;
|
|
523
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
350
524
|
};
|
|
351
525
|
readonly audiences: {
|
|
352
526
|
list: () => Promise<Result<{
|
|
@@ -391,9 +565,38 @@ export declare class Mailumi {
|
|
|
391
565
|
}) => Promise<Result<Contact>>;
|
|
392
566
|
remove: (ref: ContactRef) => Promise<Result<Deleted>>;
|
|
393
567
|
};
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
568
|
+
readonly campaigns: {
|
|
569
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
570
|
+
list: () => Promise<Result<{
|
|
571
|
+
data: Campaign[];
|
|
572
|
+
}>>;
|
|
573
|
+
/** Creates a draft. */
|
|
574
|
+
create: (campaign: NewCampaign) => Promise<Result<Campaign>>;
|
|
575
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
576
|
+
get: (id: string) => Promise<Result<Campaign>>;
|
|
577
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
578
|
+
update: ({ id, ...campaign }: Partial<NewCampaign> & {
|
|
579
|
+
id: string;
|
|
580
|
+
}) => Promise<Result<Campaign>>;
|
|
581
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
582
|
+
send: (id: string) => Promise<Result<Campaign>>;
|
|
583
|
+
/** Copies a campaign into a new draft. */
|
|
584
|
+
duplicate: (id: string) => Promise<Result<Campaign>>;
|
|
585
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
586
|
+
};
|
|
587
|
+
readonly suppressions: {
|
|
588
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
589
|
+
list: (q?: {
|
|
590
|
+
search?: string;
|
|
591
|
+
}) => Promise<Result<{
|
|
592
|
+
data: Suppression[];
|
|
593
|
+
}>>;
|
|
594
|
+
/** Allows sending to the address again. */
|
|
595
|
+
remove: (address: string) => Promise<Result<{
|
|
596
|
+
object: "suppression";
|
|
597
|
+
address: string;
|
|
598
|
+
deleted: true;
|
|
599
|
+
}>>;
|
|
397
600
|
};
|
|
398
601
|
private readonly key;
|
|
399
602
|
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.1
|
|
10
|
+
exports.VERSION = '1.2.1';
|
|
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')
|
|
@@ -81,10 +81,11 @@ function toApiEmail(o) {
|
|
|
81
81
|
}
|
|
82
82
|
const toApiContact = (c) => ({ email: c.email, first_name: c.firstName ?? c.first_name, last_name: c.lastName ?? c.last_name, unsubscribed: c.unsubscribed });
|
|
83
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}`;
|
|
84
87
|
class Mailumi {
|
|
85
88
|
constructor(apiKey, options = {}) {
|
|
86
|
-
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
87
|
-
this.webhooks = { verify: verifyWebhook };
|
|
88
89
|
const env = globalThis.process?.env ?? {};
|
|
89
90
|
this.key = apiKey ?? env.MAILUMI_API_KEY ?? '';
|
|
90
91
|
this.baseUrl = (options.baseUrl ?? env.MAILUMI_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
@@ -112,6 +113,62 @@ class Mailumi {
|
|
|
112
113
|
this.domains = {
|
|
113
114
|
/** Lists your domains, their DNS records and whether each is verified. */
|
|
114
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)),
|
|
115
172
|
};
|
|
116
173
|
this.audiences = {
|
|
117
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,29 @@ 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 zone = r.foundAt?.slice(r.name.length + 1);
|
|
133
|
+
const state = r.verified ? 'found' : zone ? `wrong name, found at ${r.foundAt}: enter only ${r.name === zone ? '@' : r.name.slice(0, -zone.length - 1)}` : r.found?.length ? `wrong value, found ${r.found.join(', ')}` : 'missing';
|
|
134
|
+
return `[${state}] ${r.type} ${r.name}${r.required ? '' : ' (recommended)'}\n ${r.priority !== undefined ? `${r.priority} ` : ''}${r.value}`;
|
|
135
|
+
}).join('\n');
|
|
136
|
+
const domainView = (d) => `${d.name} (${d.id}): ${d.sendingStatus === 'verified' ? 'verified, ready to send' : d.sendingStatus}\n\n${recordList(d)}`;
|
|
137
|
+
const KEEPS_CHECKING = 'Mailumi keeps checking every 5 minutes and emails you once the domain is verified.';
|
|
90
138
|
const table = (rows, columns) => {
|
|
91
139
|
if (!rows.length)
|
|
92
140
|
return '(none)';
|
|
@@ -134,6 +182,100 @@ async function run(argv) {
|
|
|
134
182
|
result = await client.domains.list();
|
|
135
183
|
print = l => table(l.data, ['name', 'sendingStatus', 'receivingStatus', 'id']);
|
|
136
184
|
break;
|
|
185
|
+
case 'domains add':
|
|
186
|
+
need(1, 'domains add <name>');
|
|
187
|
+
result = await client.domains.create({ name: rest[0] });
|
|
188
|
+
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`;
|
|
189
|
+
break;
|
|
190
|
+
case 'domains get':
|
|
191
|
+
need(1, 'domains get <id>');
|
|
192
|
+
result = await client.domains.get(rest[0]);
|
|
193
|
+
print = domainView;
|
|
194
|
+
break;
|
|
195
|
+
case 'domains verify': {
|
|
196
|
+
need(1, 'domains verify <id> [--wait]');
|
|
197
|
+
const until = Date.now() + 30 * 60000;
|
|
198
|
+
for (;;) {
|
|
199
|
+
result = await client.domains.verify(rest[0]);
|
|
200
|
+
if (!flags.wait || result.error || result.data.ready || result.data.domain.sendingStatus === 'failed' || Date.now() > until)
|
|
201
|
+
break;
|
|
202
|
+
if (!json)
|
|
203
|
+
console.error(`${result.data.message} Checking again in 15 seconds…`);
|
|
204
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
205
|
+
}
|
|
206
|
+
print = v => `${v.message}\n\n${domainView(v.domain)}${v.ready ? '' : `\n\n${KEEPS_CHECKING}`}`;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'domains update':
|
|
210
|
+
need(1, 'domains update <id> [--receiving on|off] [--mx-consent] [--open-tracking on|off] [--click-tracking on|off]');
|
|
211
|
+
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') });
|
|
212
|
+
print = d => `Updated ${d.name}: receiving ${d.receivingEnabled ? 'on' : 'off'}, open tracking ${d.openTracking ? 'on' : 'off'}, click tracking ${d.clickTracking ? 'on' : 'off'}.`;
|
|
213
|
+
break;
|
|
214
|
+
case 'domains remove':
|
|
215
|
+
need(1, 'domains remove <id>');
|
|
216
|
+
result = await client.domains.remove(rest[0]);
|
|
217
|
+
print = d => `Removed domain ${d.id}.`;
|
|
218
|
+
break;
|
|
219
|
+
case 'webhooks list':
|
|
220
|
+
result = await client.webhooks.list();
|
|
221
|
+
print = l => table(l.data, ['id', 'name', 'url', 'events', 'enabled']);
|
|
222
|
+
break;
|
|
223
|
+
case 'webhooks create': {
|
|
224
|
+
need(1, 'webhooks create <url> --name <name>');
|
|
225
|
+
const name = one(flags, 'name');
|
|
226
|
+
if (!name)
|
|
227
|
+
throw new Error('webhooks create needs --name.');
|
|
228
|
+
result = await client.webhooks.create({ url: rest[0], name, events: many(flags, 'events'), domainId: one(flags, 'domain'), recipient: one(flags, 'recipient') });
|
|
229
|
+
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}`;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case 'webhooks test':
|
|
233
|
+
need(1, 'webhooks test <id>');
|
|
234
|
+
result = await client.webhooks.test(rest[0]);
|
|
235
|
+
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}.` : ''}`;
|
|
236
|
+
break;
|
|
237
|
+
case 'webhooks deliveries':
|
|
238
|
+
need(1, 'webhooks deliveries <id>');
|
|
239
|
+
result = await client.webhooks.deliveries(rest[0]);
|
|
240
|
+
print = l => table(l.data, ['id', 'event', 'status', 'attempts', 'response_code', 'created_at']);
|
|
241
|
+
break;
|
|
242
|
+
case 'webhooks retry':
|
|
243
|
+
need(2, 'webhooks retry <id> <delivery_id>');
|
|
244
|
+
result = await client.webhooks.retry({ id: rest[0], deliveryId: rest[1] });
|
|
245
|
+
print = d => `Delivery ${d.id} is ${d.status}${d.response_code ? ` (HTTP ${d.response_code})` : ''}.`;
|
|
246
|
+
break;
|
|
247
|
+
case 'webhooks disable':
|
|
248
|
+
need(1, 'webhooks disable <id>');
|
|
249
|
+
result = await client.webhooks.disable(rest[0]);
|
|
250
|
+
print = w => `Disabled webhook ${w.name} (${w.id}).`;
|
|
251
|
+
break;
|
|
252
|
+
case 'templates list':
|
|
253
|
+
result = await client.templates.list();
|
|
254
|
+
print = l => table(l.data, ['id', 'name', 'subject', 'updated_at']);
|
|
255
|
+
break;
|
|
256
|
+
case 'templates get':
|
|
257
|
+
need(1, 'templates get <id>');
|
|
258
|
+
result = await client.templates.get(rest[0]);
|
|
259
|
+
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');
|
|
260
|
+
break;
|
|
261
|
+
case 'templates create': {
|
|
262
|
+
const t = templateOptions(flags);
|
|
263
|
+
if (!t.name || !t.subject || !t.html)
|
|
264
|
+
throw new Error('templates create needs --name, --subject and --html or --html-file.');
|
|
265
|
+
result = await client.templates.create({ ...t, name: t.name, subject: t.subject, html: t.html });
|
|
266
|
+
print = t => `Created template ${t.name} (${t.id}). Send it with: mailumi send --template ${t.id} --from <address> --to <address>`;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'templates update':
|
|
270
|
+
need(1, 'templates update <id>');
|
|
271
|
+
result = await client.templates.update({ id: rest[0], ...templateOptions(flags) });
|
|
272
|
+
print = t => `Updated template ${t.name} (${t.id}).`;
|
|
273
|
+
break;
|
|
274
|
+
case 'templates remove':
|
|
275
|
+
need(1, 'templates remove <id>');
|
|
276
|
+
result = await client.templates.remove(rest[0]);
|
|
277
|
+
print = t => `Deleted template ${t.id}.`;
|
|
278
|
+
break;
|
|
137
279
|
case 'audiences list':
|
|
138
280
|
result = await client.audiences.list();
|
|
139
281
|
print = l => table(l.data, ['id', 'name', 'contacts', 'subscribed']);
|
|
@@ -158,6 +300,54 @@ async function run(argv) {
|
|
|
158
300
|
result = await client.contacts.update({ audienceId: rest[0], email: rest[1], unsubscribed: true });
|
|
159
301
|
print = c => `Unsubscribed ${c.email} from every audience.`;
|
|
160
302
|
break;
|
|
303
|
+
case 'campaigns list':
|
|
304
|
+
result = await client.campaigns.list();
|
|
305
|
+
print = l => table(l.data.map((c) => ({ ...c, delivered: c.stats?.delivered ?? '' })), ['id', 'name', 'status', 'recipients', 'delivered', 'updated_at']);
|
|
306
|
+
break;
|
|
307
|
+
case 'campaigns get':
|
|
308
|
+
need(1, 'campaigns get <id>');
|
|
309
|
+
result = await client.campaigns.get(rest[0]);
|
|
310
|
+
print = c => [`${c.name} (${c.id}): ${c.status}`, `From: ${c.from ?? 'not set'}`, `Audience: ${c.audience_id ?? 'not set'}`, `Subject: ${c.subject}`,
|
|
311
|
+
...(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}`] : []),
|
|
312
|
+
...(c.stats ? ['', Object.entries(c.stats).map(([k, v]) => `${k} ${v}`).join(', ')] : []), '', c.text].join('\n');
|
|
313
|
+
break;
|
|
314
|
+
case 'campaigns create': {
|
|
315
|
+
const c = campaignOptions(flags);
|
|
316
|
+
if (!c.name || !c.subject || !c.text)
|
|
317
|
+
throw new Error('campaigns create needs --name, --subject and --text or --text-file.');
|
|
318
|
+
result = await client.campaigns.create({ ...c, name: c.name, subject: c.subject, text: c.text });
|
|
319
|
+
print = c => `Created draft ${c.name} (${c.id}). Check it with: mailumi campaigns get ${c.id}`;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case 'campaigns update':
|
|
323
|
+
need(1, 'campaigns update <id>');
|
|
324
|
+
result = await client.campaigns.update({ id: rest[0], ...campaignOptions(flags) });
|
|
325
|
+
print = c => `Updated draft ${c.name} (${c.id}).`;
|
|
326
|
+
break;
|
|
327
|
+
case 'campaigns send':
|
|
328
|
+
need(1, 'campaigns send <id>');
|
|
329
|
+
result = await client.campaigns.send(rest[0]);
|
|
330
|
+
print = c => `Sending ${c.name} to ${c.recipients} contacts. Follow it with: mailumi campaigns get ${c.id}`;
|
|
331
|
+
break;
|
|
332
|
+
case 'campaigns duplicate':
|
|
333
|
+
need(1, 'campaigns duplicate <id>');
|
|
334
|
+
result = await client.campaigns.duplicate(rest[0]);
|
|
335
|
+
print = c => `Created draft ${c.name} (${c.id}).`;
|
|
336
|
+
break;
|
|
337
|
+
case 'campaigns remove':
|
|
338
|
+
need(1, 'campaigns remove <id>');
|
|
339
|
+
result = await client.campaigns.remove(rest[0]);
|
|
340
|
+
print = c => `Deleted campaign ${c.id}.`;
|
|
341
|
+
break;
|
|
342
|
+
case 'suppressions list':
|
|
343
|
+
result = await client.suppressions.list({ search: one(flags, 'search') });
|
|
344
|
+
print = l => table(l.data, ['address', 'reason', 'detail', 'created_at']);
|
|
345
|
+
break;
|
|
346
|
+
case 'suppressions remove':
|
|
347
|
+
need(1, 'suppressions remove <address>');
|
|
348
|
+
result = await client.suppressions.remove(rest[0]);
|
|
349
|
+
print = s => `Mailumi will send to ${s.address} again.`;
|
|
350
|
+
break;
|
|
161
351
|
default:
|
|
162
352
|
throw new Error(`Unknown command "${args.join(' ')}". Run "mailumi --help".`);
|
|
163
353
|
}
|
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.1
|
|
5
|
+
export declare const VERSION = "1.2.1";
|
|
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,15 @@ 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[];
|
|
121
|
+
/** Where a missing record was found instead: its name with the domain added twice, because the DNS provider adds the domain itself. Enter only the part before the domain. */
|
|
122
|
+
foundAt?: string;
|
|
119
123
|
}
|
|
120
124
|
export interface Domain {
|
|
121
125
|
id: string;
|
|
122
126
|
name: string;
|
|
127
|
+
region?: string;
|
|
123
128
|
ownershipVerified?: boolean;
|
|
124
129
|
sendingEnabled?: boolean;
|
|
125
130
|
receivingEnabled?: boolean;
|
|
@@ -131,6 +136,127 @@ export interface Domain {
|
|
|
131
136
|
createdAt?: number;
|
|
132
137
|
records: DnsRecord[];
|
|
133
138
|
}
|
|
139
|
+
export interface DomainVerification {
|
|
140
|
+
domain: Domain;
|
|
141
|
+
/** `true` once the domain can send. */
|
|
142
|
+
ready: boolean;
|
|
143
|
+
/** What is still missing, or that the domain is ready. */
|
|
144
|
+
message: string;
|
|
145
|
+
/** How many required records have a different value at the DNS provider. */
|
|
146
|
+
wrong?: number;
|
|
147
|
+
/** How many records were found at a doubled name; see `foundAt` on each record. */
|
|
148
|
+
misplaced?: number;
|
|
149
|
+
}
|
|
150
|
+
export interface DomainSettings {
|
|
151
|
+
id: string;
|
|
152
|
+
/** Receive email for the domain and send it to your email.received webhooks. */
|
|
153
|
+
receivingEnabled?: boolean;
|
|
154
|
+
/** Must be `true` to turn receiving on: the MX record replaces any mailbox provider the domain uses now. */
|
|
155
|
+
mxConsent?: boolean;
|
|
156
|
+
/** Pro and Scale plans. */
|
|
157
|
+
openTracking?: boolean;
|
|
158
|
+
/** Pro and Scale plans. */
|
|
159
|
+
clickTracking?: boolean;
|
|
160
|
+
}
|
|
161
|
+
export type WebhookSubscription = Exclude<WebhookEventType, 'webhook.test'>;
|
|
162
|
+
export interface NewWebhook {
|
|
163
|
+
name: string;
|
|
164
|
+
/** A public HTTPS URL on port 443. */
|
|
165
|
+
url: string;
|
|
166
|
+
/** Defaults to `['email.received']`. */
|
|
167
|
+
events?: WebhookSubscription[];
|
|
168
|
+
/** Only send events for this domain. Leave it out for every domain. */
|
|
169
|
+
domainId?: string;
|
|
170
|
+
/** For email.received: only mail to this address before the @, for example "support". */
|
|
171
|
+
recipient?: string;
|
|
172
|
+
}
|
|
173
|
+
export interface Webhook {
|
|
174
|
+
object: 'webhook';
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
url: string;
|
|
178
|
+
events: WebhookSubscription[];
|
|
179
|
+
domain_id: string | null;
|
|
180
|
+
recipient: string | null;
|
|
181
|
+
enabled: boolean;
|
|
182
|
+
created_at: string;
|
|
183
|
+
}
|
|
184
|
+
export interface WebhookDelivery {
|
|
185
|
+
/** Also the webhook-id header of the request. */
|
|
186
|
+
id: string;
|
|
187
|
+
object: 'webhook_delivery';
|
|
188
|
+
webhook_id: string;
|
|
189
|
+
event: WebhookEventType;
|
|
190
|
+
email_id: string | null;
|
|
191
|
+
status: 'pending' | 'delivered' | 'failed' | 'cancelled';
|
|
192
|
+
attempts: number;
|
|
193
|
+
response_code: number | null;
|
|
194
|
+
last_error: string | null;
|
|
195
|
+
next_attempt_at: string | null;
|
|
196
|
+
created_at: string;
|
|
197
|
+
delivered_at: string | null;
|
|
198
|
+
}
|
|
199
|
+
export interface NewTemplate {
|
|
200
|
+
name: string;
|
|
201
|
+
/** Placeholders such as `{{first_name}}` work in the subject and HTML. */
|
|
202
|
+
subject: string;
|
|
203
|
+
html: string;
|
|
204
|
+
/** Default values for placeholders an email does not pass. */
|
|
205
|
+
variables?: Record<string, string>;
|
|
206
|
+
}
|
|
207
|
+
export interface Template {
|
|
208
|
+
object: 'template';
|
|
209
|
+
id: string;
|
|
210
|
+
name: string;
|
|
211
|
+
subject: string;
|
|
212
|
+
html: string;
|
|
213
|
+
variables: Record<string, string>;
|
|
214
|
+
created_at: string;
|
|
215
|
+
updated_at: string;
|
|
216
|
+
}
|
|
217
|
+
export interface NewCampaign {
|
|
218
|
+
name: string;
|
|
219
|
+
/** `{{first_name}}`, `{{last_name}}` and `{{email}}` are filled in per contact. */
|
|
220
|
+
subject: string;
|
|
221
|
+
/** Plain text. Blank lines start paragraphs and links become clickable. Mailumi adds your postal address and an unsubscribe link. */
|
|
222
|
+
text: string;
|
|
223
|
+
/** A sender on a verified domain. Needed before sending. */
|
|
224
|
+
from?: string;
|
|
225
|
+
/** Needed before sending. */
|
|
226
|
+
audienceId?: string | null;
|
|
227
|
+
}
|
|
228
|
+
export type CampaignStats = Record<'total' | 'pending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'opened' | 'clicked' | 'unsubscribed', number>;
|
|
229
|
+
export interface Campaign {
|
|
230
|
+
object: 'campaign';
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
subject: string;
|
|
234
|
+
text: string;
|
|
235
|
+
from: string | null;
|
|
236
|
+
audience_id: string | null;
|
|
237
|
+
status: 'draft' | 'sending' | 'sent' | 'failed';
|
|
238
|
+
/** How many contacts it was sent to. */
|
|
239
|
+
recipients: number;
|
|
240
|
+
error: string | null;
|
|
241
|
+
sent_at: string | null;
|
|
242
|
+
created_at: string;
|
|
243
|
+
updated_at: string;
|
|
244
|
+
/** Drafts, from `campaigns.get`: how many contacts it would go to now, and what to fix before sending. */
|
|
245
|
+
readiness?: {
|
|
246
|
+
recipients: number;
|
|
247
|
+
missing: string[];
|
|
248
|
+
};
|
|
249
|
+
/** Campaigns that were sent. */
|
|
250
|
+
stats?: CampaignStats;
|
|
251
|
+
}
|
|
252
|
+
export interface Suppression {
|
|
253
|
+
object: 'suppression';
|
|
254
|
+
address: string;
|
|
255
|
+
reason: 'bounce' | 'complaint' | 'manual';
|
|
256
|
+
detail: string | null;
|
|
257
|
+
email_id: string | null;
|
|
258
|
+
created_at: string;
|
|
259
|
+
}
|
|
134
260
|
export interface Audience {
|
|
135
261
|
object: 'audience';
|
|
136
262
|
id: string;
|
|
@@ -347,6 +473,54 @@ export declare class Mailumi {
|
|
|
347
473
|
list: () => Promise<Result<{
|
|
348
474
|
data: Domain[];
|
|
349
475
|
}>>;
|
|
476
|
+
/** Adds a domain and returns the DNS records to publish. Needs a key with access to all domains. */
|
|
477
|
+
create: ({ name }: {
|
|
478
|
+
name: string;
|
|
479
|
+
}) => Promise<Result<Domain>>;
|
|
480
|
+
get: (id: string) => Promise<Result<Domain>>;
|
|
481
|
+
/** Checks the DNS records now. Mailumi also checks new domains every 5 minutes and emails you once one is verified. */
|
|
482
|
+
verify: (id: string) => Promise<Result<DomainVerification>>;
|
|
483
|
+
update: ({ id, receivingEnabled, mxConsent, openTracking, clickTracking, ...rest }: DomainSettings & Record<string, any>) => Promise<Result<Domain>>;
|
|
484
|
+
/** Removes the domain. Emails can no longer be sent from it. */
|
|
485
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
486
|
+
};
|
|
487
|
+
readonly webhooks: {
|
|
488
|
+
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
489
|
+
verify: typeof verifyWebhook;
|
|
490
|
+
list: () => Promise<Result<{
|
|
491
|
+
data: Webhook[];
|
|
492
|
+
}>>;
|
|
493
|
+
/** Creates a webhook. `secret` is only returned here: store it as MAILUMI_WEBHOOK_SECRET. */
|
|
494
|
+
create: (webhook: NewWebhook) => Promise<Result<Webhook & {
|
|
495
|
+
secret: string;
|
|
496
|
+
}>>;
|
|
497
|
+
get: (id: string) => Promise<Result<Webhook>>;
|
|
498
|
+
/** Stops sending events to the webhook. */
|
|
499
|
+
disable: (id: string) => Promise<Result<Webhook>>;
|
|
500
|
+
/** Sends a signed `webhook.test` event and returns the delivery. */
|
|
501
|
+
test: (id: string) => Promise<Result<WebhookDelivery>>;
|
|
502
|
+
/** The 100 most recent deliveries, newest first. */
|
|
503
|
+
deliveries: (id: string) => Promise<Result<{
|
|
504
|
+
data: WebhookDelivery[];
|
|
505
|
+
}>>;
|
|
506
|
+
/** Delivers an event again now. */
|
|
507
|
+
retry: ({ id, deliveryId }: {
|
|
508
|
+
id: string;
|
|
509
|
+
deliveryId: string;
|
|
510
|
+
}) => Promise<Result<WebhookDelivery>>;
|
|
511
|
+
};
|
|
512
|
+
readonly templates: {
|
|
513
|
+
list: () => Promise<Result<{
|
|
514
|
+
data: Template[];
|
|
515
|
+
}>>;
|
|
516
|
+
/** Saves a template to send with `templateId`. */
|
|
517
|
+
create: (template: NewTemplate) => Promise<Result<Template>>;
|
|
518
|
+
get: (id: string) => Promise<Result<Template>>;
|
|
519
|
+
/** Changes only the fields you pass. */
|
|
520
|
+
update: ({ id, ...template }: Partial<NewTemplate> & {
|
|
521
|
+
id: string;
|
|
522
|
+
}) => Promise<Result<Template>>;
|
|
523
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
350
524
|
};
|
|
351
525
|
readonly audiences: {
|
|
352
526
|
list: () => Promise<Result<{
|
|
@@ -391,9 +565,38 @@ export declare class Mailumi {
|
|
|
391
565
|
}) => Promise<Result<Contact>>;
|
|
392
566
|
remove: (ref: ContactRef) => Promise<Result<Deleted>>;
|
|
393
567
|
};
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
568
|
+
readonly campaigns: {
|
|
569
|
+
/** Lists campaigns, newest first, with delivery statistics for those that were sent. */
|
|
570
|
+
list: () => Promise<Result<{
|
|
571
|
+
data: Campaign[];
|
|
572
|
+
}>>;
|
|
573
|
+
/** Creates a draft. */
|
|
574
|
+
create: (campaign: NewCampaign) => Promise<Result<Campaign>>;
|
|
575
|
+
/** Drafts include `readiness`; sent campaigns include `stats`. */
|
|
576
|
+
get: (id: string) => Promise<Result<Campaign>>;
|
|
577
|
+
/** Changes only the fields you pass. Only drafts can be changed. */
|
|
578
|
+
update: ({ id, ...campaign }: Partial<NewCampaign> & {
|
|
579
|
+
id: string;
|
|
580
|
+
}) => Promise<Result<Campaign>>;
|
|
581
|
+
/** Sends a draft to every subscribed contact in its audience now. */
|
|
582
|
+
send: (id: string) => Promise<Result<Campaign>>;
|
|
583
|
+
/** Copies a campaign into a new draft. */
|
|
584
|
+
duplicate: (id: string) => Promise<Result<Campaign>>;
|
|
585
|
+
remove: (id: string) => Promise<Result<Deleted>>;
|
|
586
|
+
};
|
|
587
|
+
readonly suppressions: {
|
|
588
|
+
/** Addresses that bounced, complained or were added by hand. Mailumi does not send to them. */
|
|
589
|
+
list: (q?: {
|
|
590
|
+
search?: string;
|
|
591
|
+
}) => Promise<Result<{
|
|
592
|
+
data: Suppression[];
|
|
593
|
+
}>>;
|
|
594
|
+
/** Allows sending to the address again. */
|
|
595
|
+
remove: (address: string) => Promise<Result<{
|
|
596
|
+
object: "suppression";
|
|
597
|
+
address: string;
|
|
598
|
+
deleted: true;
|
|
599
|
+
}>>;
|
|
397
600
|
};
|
|
398
601
|
private readonly key;
|
|
399
602
|
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.1
|
|
5
|
+
export const VERSION = '1.2.1';
|
|
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')
|
|
@@ -76,10 +76,11 @@ export function toApiEmail(o) {
|
|
|
76
76
|
}
|
|
77
77
|
const toApiContact = (c) => ({ email: c.email, first_name: c.firstName ?? c.first_name, last_name: c.lastName ?? c.last_name, unsubscribed: c.unsubscribed });
|
|
78
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}`;
|
|
79
82
|
export class Mailumi {
|
|
80
83
|
constructor(apiKey, options = {}) {
|
|
81
|
-
/** Verifies a webhook signature. Needs no API key; `verifyWebhook` is also exported on its own. */
|
|
82
|
-
this.webhooks = { verify: verifyWebhook };
|
|
83
84
|
const env = globalThis.process?.env ?? {};
|
|
84
85
|
this.key = apiKey ?? env.MAILUMI_API_KEY ?? '';
|
|
85
86
|
this.baseUrl = (options.baseUrl ?? env.MAILUMI_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
@@ -107,6 +108,62 @@ export class Mailumi {
|
|
|
107
108
|
this.domains = {
|
|
108
109
|
/** Lists your domains, their DNS records and whether each is verified. */
|
|
109
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)),
|
|
110
167
|
};
|
|
111
168
|
this.audiences = {
|
|
112
169
|
list: () => this.request('GET', '/audiences'),
|
package/package.json
CHANGED