@productcraft/mail 0.3.0 → 1.0.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 +188 -150
- package/api.md +3716 -0
- package/dist/index.cjs +3835 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5014 -29
- package/dist/index.d.ts +5014 -29
- package/dist/index.js +3770 -8
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +2383 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +32 -0
- package/dist/testing.d.ts +32 -0
- package/dist/testing.js +2381 -0
- package/dist/testing.js.map +1 -0
- package/manifest.json +2186 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -1,191 +1,229 @@
|
|
|
1
1
|
# @productcraft/mail
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Node.js / TypeScript client for **ProductCraft Mail**: transactional email (raw HTML or templates), sending domains with DKIM, the per-recipient delivery log, contacts, segments, broadcasts, suppression lists and signed webhooks.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every method, type, error class and mock is generated from the production OpenAPI spec. Upgrading from 0.x? See the [1.0 migration guide](https://github.com/clauderanelagh/productcraft-node/blob/main/docs/migrations/mail-1.0.0.md).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
6
10
|
npm install @productcraft/mail
|
|
7
11
|
```
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Node 20.3 or later, Bun, Deno and edge runtimes. Or install the umbrella package for every product: `npm install productcraft`.
|
|
10
14
|
|
|
11
15
|
## Quick start
|
|
12
16
|
|
|
13
|
-
```ts
|
|
17
|
+
```ts
|
|
14
18
|
import { Mail } from "@productcraft/mail";
|
|
15
19
|
|
|
16
|
-
// Reads PRODUCTCRAFT_API_KEY from the environment
|
|
17
|
-
const mail = new Mail();
|
|
20
|
+
// Reads PRODUCTCRAFT_API_KEY (and PRODUCTCRAFT_WORKSPACE_ID) from the environment.
|
|
21
|
+
const mail = new Mail({ workspaceId: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b" });
|
|
18
22
|
|
|
19
|
-
// Send
|
|
20
|
-
const
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
);
|
|
23
|
+
// Send raw HTML (an Idempotency-Key is generated for you, so a retry never sends twice).
|
|
24
|
+
const email = await mail.emails.send({
|
|
25
|
+
from: "Acme <hello@mail.acme.com>",
|
|
26
|
+
to: "success@simulator.amazonses.com",
|
|
27
|
+
subject: "Welcome",
|
|
28
|
+
html: "<p>Thanks for signing up.</p>",
|
|
29
|
+
});
|
|
30
|
+
console.log(email.id);
|
|
31
|
+
|
|
32
|
+
// Or render a stored template with your data.
|
|
33
|
+
await mail.templates.send("welcome", { to: "success@simulator.amazonses.com", data: { first_name: "Ada" } });
|
|
34
|
+
|
|
35
|
+
// Walk the delivery log; pages are fetched as you iterate.
|
|
36
|
+
for await (const message of mail.messages.list({ status: ["bounced", "complained"] })) {
|
|
37
|
+
console.log(message.id, message.status);
|
|
38
|
+
}
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
`workspaceId` is optional for workspace API keys: without it the client looks it up once per process (one extra request on a cold start). Pass it, or set `PRODUCTCRAFT_WORKSPACE_ID`, to skip the lookup. Bearer tokens must always pass it.
|
|
42
|
+
|
|
43
|
+
Nouns: `emails` is the call you made (send, batch, list, retrieve, metrics); `messages` is the per-recipient delivery log (list, retrieve, timeline, body, export, erase). Exports, raw MIME and attachments resolve to the raw `Response` (`.text()`, `.arrayBuffer()`, `.body`).
|
|
44
|
+
|
|
45
|
+
## Credentials
|
|
46
|
+
|
|
47
|
+
Pass `apiKey` (a workspace key, `pcft_live_…`), or omit it and the client reads `PRODUCTCRAFT_API_KEY`. An explicit `auth: { type: "bearer", token }` works for platform user tokens. The environment fallback is server-only: never put a key in `NEXT_PUBLIC_*` or any variable a bundler inlines into browser code (a workspace key in a browser throws unless you set `dangerouslyAllowBrowser: true`).
|
|
48
|
+
|
|
49
|
+
Webhook signing secrets: Mail has a workspace default secret, per-domain secrets and one per endpoint. Pass all that apply as an array (`new Mail({ webhookSecret: [a, b] })` or `mail.webhooks.unwrap({ ..., secret: [a, b] })`); `unwrap` accepts the Standard Webhooks headers and the older `X-Mail-Signature` scheme.
|
|
50
|
+
|
|
51
|
+
<!-- generated:start (scripts/gen-sdk.mjs; edits between the markers are overwritten) -->
|
|
52
|
+
|
|
53
|
+
## Resources
|
|
54
|
+
|
|
55
|
+
Full reference: [api.md](./api.md).
|
|
56
|
+
|
|
57
|
+
- `mail.brand`: `reset`, `retrieve`, `update`
|
|
58
|
+
- `mail.broadcasts`: `audience`, `cancel`, `clickedLinks.list`, `create`, `delete`, `duplicate`, `list`, `recipients.list`, `retrieve`, `send`, `settings.retrieve`, `settings.update`, `update`
|
|
59
|
+
- `mail.contacts`: `create`, `delete`, `imports.create`, `imports.list`, `imports.retrieve`, `list`, `properties.create`, `properties.delete`, `properties.list`, `properties.retrieve`, `properties.update`, `retrieve`, `segments.add`, `segments.delete`, `segments.list`, `topics.list`, `topics.update`, `update`
|
|
60
|
+
- `mail.deliverability`: `retrieve`
|
|
61
|
+
- `mail.domains`: `create`, `delete`, `dnsRecords.apply`, `dnsRecords.check`, `dnsRecords.export`, `dnsRecords.list`, `domainConnect.start`, `events.list`, `list`, `lookup`, `retrieve`, `shareLinks.create`, `shareLinks.delete`, `shareLinks.list`, `smtpConfig.reset`, `smtpConfig.retrieve`, `smtpConfig.update`, `smtpConfig.verifySaved`, `testSend`, `update`, `verify`
|
|
62
|
+
- `mail.emails`: `batch`, `list`, `metrics`, `retrieve`, `send`
|
|
63
|
+
- `mail.integrations`: `delete`, `github.connect`, `github.repos.branches.list`, `github.repos.list`, `github.settings.update`, `github.sync.create`, `github.sync.retrieve`, `list`, `retrieve`
|
|
64
|
+
- `mail.mailboxes`: `create`, `delete`, `list`, `messages.attachments.retrieve`, `messages.delete`, `messages.list`, `messages.raw`, `messages.read`, `messages.retrieve`, `retrieve`, `update`
|
|
65
|
+
- `mail.messages`: `body`, `erase`, `export`, `list`, `retrieve`, `settings.retrieve`, `settings.update`, `timeline`
|
|
66
|
+
- `mail.received`: `attachments.retrieve`, `delete`, `export`, `list`, `raw`, `read`, `retrieve`
|
|
67
|
+
- `mail.segments`: `contacts.list`, `create`, `delete`, `list`, `metrics`, `preview`, `retrieve`, `update`
|
|
68
|
+
- `mail.sendingDefaults`: `retrieve`, `update`
|
|
69
|
+
- `mail.smtpConfig`: `list`, `reset`, `retrieve`, `update`, `verify`, `verifySaved`
|
|
70
|
+
- `mail.stats`: `retrieve`
|
|
71
|
+
- `mail.suppression`: `create`, `delete`, `list`
|
|
72
|
+
- `mail.templates`: `create`, `delete`, `lint`, `list`, `preview`, `render`, `retrieve`, `send`, `sendBatch`, `tags.list`, `testSend`, `variables.list`
|
|
73
|
+
- `mail.topics`: `create`, `delete`, `list`, `retrieve`, `update`
|
|
74
|
+
- `mail.webhooks`: `attempts.list`, `endpoints.create`, `endpoints.delete`, `endpoints.list`, `endpoints.retrieve`, `endpoints.rotateSecret`, `endpoints.test`, `endpoints.update`, `eventTypes.list`, `events.list`, `events.replay`, `events.retrieve`
|
|
40
75
|
|
|
41
|
-
##
|
|
76
|
+
## Pagination
|
|
42
77
|
|
|
43
|
-
|
|
78
|
+
List methods return a `PagePromise`: `for await` walks every item across pages, `await` gives one page.
|
|
44
79
|
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
PRODUCTCRAFT_API_KEY
|
|
80
|
+
```ts
|
|
81
|
+
import { Mail } from "@productcraft/mail";
|
|
82
|
+
const mail = new Mail({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
|
|
83
|
+
for await (const item of mail.broadcasts.list({ limit: 50 })) console.log(item);
|
|
84
|
+
const page = await mail.broadcasts.list();
|
|
85
|
+
if (page.hasNextPage()) await page.getNextPage();
|
|
86
|
+
const all = await mail.broadcasts.list().toArray({ maxItems: 500 });
|
|
48
87
|
```
|
|
49
88
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Custom fetch implementation (undici with retry, mock in tests, ...)
|
|
66
|
-
fetch: customFetch,
|
|
67
|
-
});
|
|
89
|
+
## Errors
|
|
90
|
+
|
|
91
|
+
Every failure is a class you can catch. `APIError` carries `status`, `code`, `message`, `errors[]`, `requestId` and `docUrl`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Mail, NotFoundError, RateLimitError, APIError } from "@productcraft/mail";
|
|
95
|
+
const mail = new Mail();
|
|
96
|
+
try {
|
|
97
|
+
await mail.broadcasts.retrieve("00000000-0000-0000-0000-000000000000");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err instanceof NotFoundError) console.log(err.code, err.requestId, err.docUrl);
|
|
100
|
+
else if (err instanceof RateLimitError) console.log(err.retryAfter);
|
|
101
|
+
else if (err instanceof APIError) console.log(err.status, err.errors);
|
|
102
|
+
else throw err;
|
|
103
|
+
}
|
|
68
104
|
```
|
|
69
105
|
|
|
70
|
-
##
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await mail.client.GET(
|
|
79
|
-
"/v1/workspaces/{workspace_id}/templates",
|
|
80
|
-
{ params: { path: { workspace_id: "ws_..." } } },
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// Lint a template before saving — the body field is `body_html`
|
|
84
|
-
// (wire) / `bodyHtml` (camelCase TS DTO), NOT `html`.
|
|
85
|
-
await mail.client.POST(
|
|
86
|
-
"/v1/workspaces/{workspace_id}/templates/lint",
|
|
87
|
-
{
|
|
88
|
-
params: { path: { workspace_id: "<workspace-uuid>" } },
|
|
89
|
-
body: { body_html: "<p>Hello {{name}}</p>" },
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
// → { score, findings: [...] }
|
|
93
|
-
|
|
94
|
-
// Preview / render with sample data
|
|
95
|
-
await mail.client.POST(
|
|
96
|
-
"/v1/workspaces/{workspace_id}/templates/{name}/render",
|
|
97
|
-
{
|
|
98
|
-
params: { path: { workspace_id: "ws_...", name: "welcome" } },
|
|
99
|
-
body: { data: { name: "Alice" } },
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
// Send to one address (template-rendered)
|
|
104
|
-
await mail.client.POST(
|
|
105
|
-
"/v1/workspaces/{workspace_id}/templates/{name}/send",
|
|
106
|
-
{ params: { ... }, body: { from, to, data } },
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Send the same template to many addresses (batch)
|
|
110
|
-
await mail.client.POST(
|
|
111
|
-
"/v1/workspaces/{workspace_id}/templates/{name}/send-batch",
|
|
112
|
-
{ params: { ... }, body: { recipients: [{ to, data }, ...] } },
|
|
113
|
-
);
|
|
106
|
+
## Retries and timeouts
|
|
107
|
+
|
|
108
|
+
Network errors, 408, 429, 5xx and `409 IDEMPOTENCY_IN_PROGRESS` are retried twice by default with exponential backoff and jitter, honouring `Retry-After` up to 60 s. A POST or PATCH without an idempotency key is never retried. Each attempt times out after 60 s, so the worst case with defaults is about 3 minutes; pass `signal: AbortSignal.timeout(ms)` for an overall deadline.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Mail } from "@productcraft/mail";
|
|
112
|
+
const mail = new Mail({ maxRetries: 3, timeout: 20_000 });
|
|
113
|
+
await mail.broadcasts.list({}, { maxRetries: 0, signal: AbortSignal.timeout(5_000) });
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
## Idempotency
|
|
117
|
+
|
|
118
|
+
Writes that support it send an `Idempotency-Key` automatically and reuse it on every retry. Pass your own to make a retry across processes safe. If the API's idempotency store is unavailable it runs the request without it (fail-open), so a retry during that window can execute twice.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Mail } from "@productcraft/mail";
|
|
122
|
+
const mail = new Mail({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
|
|
123
|
+
const broadcast = await mail.broadcasts.create({ name: "October product update" }, { idempotencyKey: "order-1234" });
|
|
124
|
+
```
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
// List domains
|
|
120
|
-
await mail.client.GET("/v1/workspaces/{workspace_id}/domains", { ... });
|
|
126
|
+
## Polling
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
await mail.client.POST(
|
|
124
|
-
"/v1/workspaces/{workspace_id}/domains",
|
|
125
|
-
{ params: { ... }, body: { name: "mail.yourbrand.com" } },
|
|
126
|
-
);
|
|
128
|
+
`domains.poll(id)` retrieves until `status` is terminal (`active`, `disabled`), honouring `Retry-After`; `timeoutMs` (default 5 min) throws `APITimeoutError`.
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
130
|
+
```ts
|
|
131
|
+
import { Mail } from "@productcraft/mail";
|
|
132
|
+
const mail = new Mail({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
|
|
133
|
+
const done = await mail.domains.poll("9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b", { intervalMs: 2_000, timeoutMs: 600_000 });
|
|
134
|
+
console.log(done.status);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Webhooks
|
|
138
|
+
|
|
139
|
+
`mail.webhooks.unwrap()` verifies the signature against the **raw** body and returns a typed `Mail.UnwrapWebhookEvent`. The secret comes from `{ secret }`, the client's `webhookSecret`, or `PRODUCTCRAFT_MAIL_WEBHOOK_SECRET`.
|
|
140
|
+
|
|
141
|
+
Next.js route handler:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { Mail } from "@productcraft/mail";
|
|
145
|
+
const mail = new Mail();
|
|
146
|
+
export async function POST(req: Request) {
|
|
147
|
+
const event = await mail.webhooks.unwrap({ payload: await req.text(), headers: req.headers });
|
|
148
|
+
switch (event.type) {
|
|
149
|
+
case "contact.created":
|
|
150
|
+
console.log(event.data);
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
break; // UnknownWebhookEvent: types added later
|
|
154
|
+
}
|
|
155
|
+
return new Response(null, { status: 204 });
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Express (keep the body raw):
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import express from "express";
|
|
163
|
+
import { Mail } from "@productcraft/mail";
|
|
164
|
+
const mail = new Mail();
|
|
165
|
+
const app = express();
|
|
166
|
+
app.post("/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
|
|
167
|
+
const event = await mail.webhooks.unwrap({ payload: req.body, headers: req.headers });
|
|
168
|
+
console.log(event.type);
|
|
169
|
+
res.sendStatus(204);
|
|
170
|
+
});
|
|
133
171
|
```
|
|
134
172
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/attachments/{position}",
|
|
148
|
-
{ ... },
|
|
149
|
-
);
|
|
173
|
+
Hono:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Hono } from "hono";
|
|
177
|
+
import { Mail } from "@productcraft/mail";
|
|
178
|
+
const mail = new Mail();
|
|
179
|
+
const app = new Hono();
|
|
180
|
+
app.post("/webhooks", async (c) => {
|
|
181
|
+
const event = await mail.webhooks.unwrap({ payload: await c.req.text(), headers: c.req.raw.headers });
|
|
182
|
+
console.log(event.type);
|
|
183
|
+
return c.body(null, 204);
|
|
184
|
+
});
|
|
150
185
|
```
|
|
151
186
|
|
|
152
|
-
|
|
187
|
+
## Raw responses
|
|
188
|
+
|
|
189
|
+
Every returned object has a non-enumerable `lastResponse` (`requestId`, `status`, `headers`). `.withResponse()` adds the `Response` and parsed rate-limit headers.
|
|
153
190
|
|
|
154
|
-
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
191
|
+
```ts
|
|
192
|
+
import { Mail } from "@productcraft/mail";
|
|
193
|
+
const mail = new Mail({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
|
|
194
|
+
const id = "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b";
|
|
195
|
+
const obj = await mail.broadcasts.retrieve(id);
|
|
196
|
+
console.log(obj.lastResponse.requestId);
|
|
197
|
+
const { data, response, requestId, rateLimit } = await mail.broadcasts.retrieve(id).withResponse();
|
|
198
|
+
console.log(data, response.status, requestId, rateLimit?.remaining);
|
|
160
199
|
```
|
|
161
200
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Per-domain webhook overrides + secret rotation
|
|
175
|
-
await mail.client.POST(
|
|
176
|
-
"/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}/rotate-secret",
|
|
177
|
-
{ ... },
|
|
178
|
-
);
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
`@productcraft/mail/testing` exports `createMockFetch()`, a `fetch` that answers every operation with its spec example. Override one operation by name.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { Mail } from "@productcraft/mail";
|
|
207
|
+
import { createMockFetch } from "@productcraft/mail/testing";
|
|
208
|
+
const fetch = createMockFetch();
|
|
209
|
+
const mail = new Mail({ apiKey: "test", workspaceId: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b", fetch });
|
|
210
|
+
await mail.broadcasts.list();
|
|
211
|
+
console.log(fetch.calls.length);
|
|
179
212
|
```
|
|
180
213
|
|
|
181
|
-
##
|
|
214
|
+
## Escape hatch
|
|
182
215
|
|
|
183
|
-
|
|
216
|
+
`.client` is the typed `openapi-fetch` client over the same credential and base URL (no retries). Use it for anything the resource methods do not cover.
|
|
184
217
|
|
|
185
|
-
|
|
218
|
+
```ts
|
|
219
|
+
import { Mail } from "@productcraft/mail";
|
|
220
|
+
const mail = new Mail({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
|
|
221
|
+
const { data, error } = await mail.client.GET("/v1/workspaces/{workspace_id}/brand", { params: { path: { workspace_id: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b" } } });
|
|
222
|
+
console.log(data, error);
|
|
223
|
+
```
|
|
186
224
|
|
|
187
|
-
|
|
225
|
+
<!-- generated:end -->
|
|
188
226
|
|
|
189
227
|
## License
|
|
190
228
|
|
|
191
|
-
|
|
229
|
+
MIT
|