sently 1.0.1 → 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/AGENTS.md +3 -2
- package/CHANGELOG.md +110 -1
- package/README.md +20 -5
- package/dist/chunk-z1589fjk.js.map +2 -2
- package/dist/core/push-types.d.ts +53 -4
- package/dist/transports/hostinger.d.ts +128 -0
- package/dist/transports/hostinger.js +3 -0
- package/dist/transports/hostinger.js.map +10 -0
- package/dist/transports/inbucket.d.ts +196 -0
- package/dist/transports/inbucket.js +3 -0
- package/dist/transports/inbucket.js.map +10 -0
- package/dist/transports/mailpit.d.ts +108 -8
- package/dist/transports/mailpit.js +2 -2
- package/dist/transports/mailpit.js.map +3 -3
- package/dist/transports/taqnyat-sms.d.ts +85 -0
- package/dist/transports/taqnyat-sms.js +2 -2
- package/dist/transports/taqnyat-sms.js.map +3 -3
- package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
- package/dist/transports/taqnyat-whatsapp.js +2 -2
- package/dist/transports/taqnyat-whatsapp.js.map +3 -3
- package/dist/transports/webpush.d.ts +19 -0
- package/dist/transports/webpush.js +2 -2
- package/dist/transports/webpush.js.map +3 -3
- package/dist/webhooks/sndr.js +2 -2
- package/dist/webhooks/sndr.js.map +3 -3
- package/package.json +12 -2
- package/site/content/docs/ai/llms-txt.mdx +2 -0
- package/site/content/docs/channels/email.mdx +2 -1
- package/site/content/docs/channels/push.mdx +3 -1
- package/site/content/docs/decorators/preview.mdx +2 -1
- package/site/content/docs/get-started/entrypoints.mdx +1 -1
- package/site/content/docs/get-started/support-matrix.mdx +2 -2
- package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
- package/site/content/docs/guides/webhooks.mdx +2 -0
- package/site/content/docs/guides/webpush-interop.mdx +9 -1
- package/site/content/docs/reference/exports.mdx +1 -1
- package/site/content/docs/reference/push-options.mdx +22 -5
- package/site/content/docs/transports/hostinger.mdx +435 -0
- package/site/content/docs/transports/inbucket.mdx +200 -0
- package/site/content/docs/transports/index.mdx +3 -3
- package/site/content/docs/transports/mailpit.mdx +114 -14
- package/site/content/docs/transports/meta.json +6 -4
- package/site/content/docs/transports/smtp.mdx +3 -2
- package/site/content/docs/transports/sndr.mdx +68 -5
- package/site/content/docs/transports/taqnyat.mdx +365 -0
- package/site/content/docs/transports/webpush.mdx +201 -17
- package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
- package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
- package/site/content/docs/transports/taqnyat-whatsapp.mdx +0 -40
|
@@ -8,8 +8,13 @@ source: "src/transports/mailpit.ts"
|
|
|
8
8
|
Catch outbound email in a local [Mailpit](https://github.com/axllent/mailpit) instance while you develop.
|
|
9
9
|
Use it for the welcome email or password-reset flow before you point at a production provider.
|
|
10
10
|
|
|
11
|
+
<LiveVerified>
|
|
12
|
+
Email send against a local Mailpit instance succeeded in sently’s integration suite (SMTP capture + REST list/get).
|
|
13
|
+
</LiveVerified>
|
|
14
|
+
|
|
11
15
|
<Callout title="The one rule">
|
|
12
16
|
Use Mailpit only in development — swap to a production transport before you deploy.
|
|
17
|
+
Vendor extras stay on the `MailpitTransport` instance — never on `createMailer`.
|
|
13
18
|
</Callout>
|
|
14
19
|
|
|
15
20
|
## Quick start
|
|
@@ -72,27 +77,117 @@ console.log(inbox.messages[0]?.Subject);
|
|
|
72
77
|
| `apiAuth` | `{ user, pass }` | — | Basic auth for the UI/API |
|
|
73
78
|
|
|
74
79
|
`provider` is `"mailpit"`. `verify()` checks SMTP; `close()` closes the socket adapter.
|
|
80
|
+
`webUrl` is the UI base (same as `apiUrl`).
|
|
81
|
+
|
|
82
|
+
## Features
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
Pick a branch. Channel send goes through `mailer`; everything else is called on `mailpit`.
|
|
85
|
+
Message ids may be a Mailpit id or `"latest"`.
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
<Tabs items={["Send", "List", "Search", "Message", "Headers", "HTML check", "Link check", "Read", "Delete"]}>
|
|
88
|
+
<Tab value="Send">
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
| --- | --- | --- |
|
|
82
|
-
| `messages({ limit?, start? })` | `GET /api/v1/messages` | List captured messages (newest first) |
|
|
83
|
-
| `getMessage(id)` | `GET /api/v1/message/{id}` | Full message body |
|
|
84
|
-
| `deleteMessages(ids)` | `DELETE /api/v1/messages` | Delete by id |
|
|
85
|
-
| `deleteAll()` | `DELETE` with empty `IDs` | Clear the inbox |
|
|
86
|
-
| `webUrl` | — | UI base URL (same as `apiUrl`) |
|
|
90
|
+
Transactional send via the channel mailer (SMTP into Mailpit).
|
|
87
91
|
|
|
88
92
|
```ts
|
|
93
|
+
await mailer.send({
|
|
94
|
+
from: "dev@example.com",
|
|
95
|
+
to: "you@example.com",
|
|
96
|
+
subject: "Welcome",
|
|
97
|
+
html: "<h1>Hello</h1><a href=\"https://example.com\">Go</a>",
|
|
98
|
+
text: "Hello",
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
</Tab>
|
|
103
|
+
<Tab value="List">
|
|
104
|
+
|
|
105
|
+
List captured messages (`GET /api/v1/messages`), newest first.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const inbox = await mailpit.messages({ limit: 10, start: 0 });
|
|
109
|
+
console.log(inbox.total, inbox.messages[0]?.Subject);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</Tab>
|
|
113
|
+
<Tab value="Search">
|
|
114
|
+
|
|
115
|
+
Find a message with Mailpit’s query syntax (`subject:`, `to:`, `tag:`, …).
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const found = await mailpit.search("subject:Welcome", { limit: 1 });
|
|
119
|
+
console.log(found.messages[0]?.ID);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
</Tab>
|
|
123
|
+
<Tab value="Message">
|
|
124
|
+
|
|
125
|
+
Full body for the message id (or `"latest"`).
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const full = await mailpit.getMessage("latest");
|
|
129
|
+
console.log(full.Text, full.HTML);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
</Tab>
|
|
133
|
+
<Tab value="Headers">
|
|
134
|
+
|
|
135
|
+
Header map for assertions (`Message-Id`, custom headers, …).
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const headers = await mailpit.getHeaders("latest");
|
|
139
|
+
console.log(headers.Subject, headers["Message-Id"]);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
</Tab>
|
|
143
|
+
<Tab value="HTML check">
|
|
144
|
+
|
|
145
|
+
Client HTML/CSS compatibility score from Mailpit’s checker.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const html = await mailpit.htmlCheck("latest");
|
|
149
|
+
console.log(html.Total.Supported, html.Total.Unsupported, html.Warnings.length);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Needs an HTML part — plain-text-only messages can return `400`.
|
|
153
|
+
|
|
154
|
+
</Tab>
|
|
155
|
+
<Tab value="Link check">
|
|
156
|
+
|
|
157
|
+
Probe links and images in the message (`follow` optional).
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const links = await mailpit.linkCheck("latest", { follow: true });
|
|
161
|
+
console.log(links.Errors, links.Links);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
</Tab>
|
|
165
|
+
<Tab value="Read">
|
|
166
|
+
|
|
167
|
+
Mark messages read or unread between assertions.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const inbox = await mailpit.messages({ limit: 1 });
|
|
171
|
+
await mailpit.setRead([inbox.messages[0]!.ID], true);
|
|
172
|
+
// empty ids → update every message
|
|
173
|
+
await mailpit.setRead([], false);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
</Tab>
|
|
177
|
+
<Tab value="Delete">
|
|
178
|
+
|
|
179
|
+
Delete by id, or clear the whole inbox.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await mailpit.deleteMessages(["abc"]);
|
|
89
183
|
await mailpit.deleteAll();
|
|
90
|
-
await mailer.send({ from: "a@test.com", to: "b@test.com", subject: "T", text: "ok" });
|
|
91
|
-
const list = await mailpit.messages({ limit: 1 });
|
|
92
|
-
const full = await mailpit.getMessage(list.messages[0]!.ID);
|
|
93
184
|
```
|
|
94
185
|
|
|
95
|
-
|
|
186
|
+
</Tab>
|
|
187
|
+
</Tabs>
|
|
188
|
+
|
|
189
|
+
REST failures throw `MailpitError` (`provider: "mailpit"`).
|
|
190
|
+
Empty `getMessage("")` / `search("")` throws with status `400`.
|
|
96
191
|
|
|
97
192
|
## Troubleshooting
|
|
98
193
|
|
|
@@ -103,6 +198,9 @@ REST failures throw `MailpitError` (`provider: "mailpit"`). Empty `getMessage(""
|
|
|
103
198
|
<Accordion title="API helpers fail but send works">
|
|
104
199
|
SMTP and the UI/API can bind to different hosts. Set `apiUrl` (and `apiAuth` if the UI requires Basic auth).
|
|
105
200
|
</Accordion>
|
|
201
|
+
<Accordion title="htmlCheck returns 400">
|
|
202
|
+
The message has no HTML part, or Mailpit could not parse it. Send `html` (not only `text`) and retry with `"latest"` or the message id.
|
|
203
|
+
</Accordion>
|
|
106
204
|
<Accordion title="Should I use createSMTPMailer instead?">
|
|
107
205
|
Yes, if you only need SMTP. `MailpitTransport` adds local defaults and REST helpers for tests and inspection.
|
|
108
206
|
</Accordion>
|
|
@@ -110,14 +208,16 @@ REST failures throw `MailpitError` (`provider: "mailpit"`). Empty `getMessage(""
|
|
|
110
208
|
|
|
111
209
|
## Learn more
|
|
112
210
|
|
|
211
|
+
- [Inbucket](./inbucket) — another local SMTP catcher with a mailbox REST API
|
|
113
212
|
- [SMTP](./smtp) — generic SMTP when you are not on Mailpit
|
|
114
213
|
- [Preview](/docs/decorators/preview) — write `.eml` files to disk instead
|
|
115
214
|
- [Email channel](/docs/channels/email) — `createMailer` contract
|
|
215
|
+
- [Mailpit API](https://mailpit.axllent.org/docs/api-v1/) — full REST surface on the catcher
|
|
116
216
|
|
|
117
217
|
## Next
|
|
118
218
|
|
|
119
219
|
<Cards>
|
|
220
|
+
<Card title="Inbucket" href="/docs/transports/inbucket" />
|
|
120
221
|
<Card title="Email channel" href="/docs/channels/email" />
|
|
121
|
-
<Card title="SMTP" href="/docs/transports/smtp" />
|
|
122
222
|
<Card title="Transports" href="/docs/transports" />
|
|
123
223
|
</Cards>
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"icon": "Truck",
|
|
4
4
|
"pages": [
|
|
5
5
|
"index",
|
|
6
|
+
"---Multi-channel---",
|
|
7
|
+
"taqnyat",
|
|
6
8
|
"---Email---",
|
|
7
9
|
"smtp",
|
|
8
10
|
"resend",
|
|
@@ -15,19 +17,19 @@
|
|
|
15
17
|
"plunk",
|
|
16
18
|
"sparkpost",
|
|
17
19
|
"mailtrap",
|
|
18
|
-
"mailpit",
|
|
19
20
|
"loops",
|
|
20
21
|
"cloudflare-email",
|
|
21
22
|
"sndr",
|
|
22
|
-
"
|
|
23
|
+
"hostinger",
|
|
24
|
+
"---Email Dev---",
|
|
25
|
+
"mailpit",
|
|
26
|
+
"inbucket",
|
|
23
27
|
"---SMS---",
|
|
24
28
|
"twilio-sms",
|
|
25
|
-
"taqnyat-sms",
|
|
26
29
|
"msegat",
|
|
27
30
|
"unifonic",
|
|
28
31
|
"---WhatsApp---",
|
|
29
32
|
"whatsapp-cloud",
|
|
30
|
-
"taqnyat-whatsapp",
|
|
31
33
|
"---Push---",
|
|
32
34
|
"webpush",
|
|
33
35
|
"fcm"
|
|
@@ -55,7 +55,7 @@ await mailer.send({
|
|
|
55
55
|
| `pool` | `boolean` | `false` |
|
|
56
56
|
| `dkim` | `DKIMConfig` | optional |
|
|
57
57
|
|
|
58
|
-
For local capture with defaults and REST helpers, use [Mailpit](./mailpit) instead of hand-wiring
|
|
58
|
+
For local capture with defaults and REST helpers, use [Mailpit](./mailpit) or [Inbucket](./inbucket) instead of hand-wiring SMTP ports.
|
|
59
59
|
|
|
60
60
|
## Troubleshooting
|
|
61
61
|
|
|
@@ -67,13 +67,14 @@ For local capture with defaults and REST helpers, use [Mailpit](./mailpit) inste
|
|
|
67
67
|
Pass an explicit transport, or use `createSMTPMailer` for relay configuration.
|
|
68
68
|
</Accordion>
|
|
69
69
|
<Accordion title="Local development without a real relay">
|
|
70
|
-
Use [Mailpit](./mailpit) (
|
|
70
|
+
Use [Mailpit](./mailpit), [Inbucket](./inbucket), or [Preview](/docs/decorators/preview).
|
|
71
71
|
</Accordion>
|
|
72
72
|
</Accordions>
|
|
73
73
|
|
|
74
74
|
## Learn more
|
|
75
75
|
|
|
76
76
|
- [Mailpit](./mailpit) — local SMTP catcher with inbox API
|
|
77
|
+
- [Inbucket](./inbucket) — local SMTP catcher with mailbox REST API
|
|
77
78
|
- [Email channel](/docs/channels/email) — `createMailer` / `createSMTPMailer`
|
|
78
79
|
- [Runtimes](/docs/get-started/runtimes) — socket adapters
|
|
79
80
|
|
|
@@ -5,8 +5,12 @@ icon: Truck
|
|
|
5
5
|
source: "src/transports/sndr.ts"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Use SNDR
|
|
9
|
-
Wire `SndrTransport` into `createMailer
|
|
8
|
+
Use SNDR for app-triggered email — receipts, sign-in mail, and other transactional sends — over HTTPS JSON.
|
|
9
|
+
Wire `SndrTransport` into `createMailer`; do not call `@rkiza/sndr` from application code.
|
|
10
|
+
|
|
11
|
+
<LiveVerified>
|
|
12
|
+
Email send against SNDR’s production API succeeded in sently’s opt-in live suite (verified domain + real recipient).
|
|
13
|
+
</LiveVerified>
|
|
10
14
|
|
|
11
15
|
<Callout title="The one rule">
|
|
12
16
|
Import from `sently/transports/sndr`, pass the transport to `await createMailer(...)`, and send
|
|
@@ -55,7 +59,7 @@ console.log(result.messageId); // e.g. em_…
|
|
|
55
59
|
console.log(result.response); // e.g. queued
|
|
56
60
|
```
|
|
57
61
|
|
|
58
|
-
`from` must use a domain verified in the SNDR dashboard.
|
|
62
|
+
`from` must use a domain verified in the [SNDR dashboard](https://www.sndr.sh/) (SPF, DKIM, DMARC).
|
|
59
63
|
|
|
60
64
|
</Step>
|
|
61
65
|
|
|
@@ -82,10 +86,12 @@ console.log(result.response); // e.g. queued
|
|
|
82
86
|
| `idempotencyKey` / `messageId` | `Idempotency-Key` | Sent when present. |
|
|
83
87
|
| `data` | `variables` | Only when a template id is set. |
|
|
84
88
|
|
|
89
|
+
`POST /v1/send` is idempotent when `Idempotency-Key` is supplied.
|
|
90
|
+
|
|
85
91
|
## Templates
|
|
86
92
|
|
|
87
93
|
Set a template with the `x-sndr-template-id` header (or `defaultTemplateId` on the transport).
|
|
88
|
-
Pass template variables through `data
|
|
94
|
+
Pass template variables through `data` (`{{ variable }}` placeholders on SNDR).
|
|
89
95
|
|
|
90
96
|
```ts
|
|
91
97
|
import { SNDR_TEMPLATE_ID_HEADER } from "sently/transports/sndr";
|
|
@@ -105,13 +111,57 @@ await mailer.send({
|
|
|
105
111
|
|
|
106
112
|
Failed HTTP responses throw `SndrError` (`SentlyError`) with the API `error.message` when present.
|
|
107
113
|
|
|
114
|
+
| SNDR `error.code` (common) | Meaning |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `invalid_request` | Malformed payload |
|
|
117
|
+
| `unauthenticated` | Missing or invalid API key |
|
|
118
|
+
| `rate_limited` | Slow down |
|
|
119
|
+
| `domain_not_verified` | Verify the sending domain first |
|
|
120
|
+
| `recipient_suppressed` | Address is on the suppression list |
|
|
121
|
+
|
|
122
|
+
## Webhooks
|
|
123
|
+
|
|
124
|
+
Import from `sently/webhooks/sndr`. Verify `X-Sndr-Signature` (`t=…,v1=…`) over the **raw** body, then `parse`.
|
|
125
|
+
|
|
126
|
+
| SNDR event | Normalized `EmailEvent.type` |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `email.queued` | `deferred` |
|
|
129
|
+
| `email.delivered` | `delivered` |
|
|
130
|
+
| `email.bounced` | `bounced` |
|
|
131
|
+
| `email.failed` | `unknown` |
|
|
132
|
+
| `email.complained` | `complained` |
|
|
133
|
+
| `email.opened` | `opened` |
|
|
134
|
+
| `email.clicked` | `clicked` |
|
|
135
|
+
| `email.unsubscribed` | `unknown` |
|
|
136
|
+
|
|
137
|
+
See [Webhooks](/docs/guides/webhooks#sndr-signatures) for the HMAC details.
|
|
138
|
+
|
|
139
|
+
## What sently covers vs SNDR platform
|
|
140
|
+
|
|
141
|
+
| Surface | In sently | Notes |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| Send (`POST /v1/send`) | Yes — `SndrTransport` | Channel: `createMailer` |
|
|
144
|
+
| Templates + variables | Yes | Header / `defaultTemplateId` + `data` |
|
|
145
|
+
| Idempotency | Yes | `idempotencyKey` / `messageId` |
|
|
146
|
+
| Domain verify | Yes — `verify()` | `GET /v1/domains` |
|
|
147
|
+
| Delivery webhooks | Yes — `sently/webhooks/sndr` | Signed parse |
|
|
148
|
+
| Contacts / contact groups / broadcasts | No | SNDR dashboard / their API |
|
|
149
|
+
| Analytics / suppressions admin | No | Use SNDR dashboard |
|
|
150
|
+
| Attachments on HTTP send | Not mapped | Prefer SMTP or ask SNDR if their send API gains attachments |
|
|
151
|
+
|
|
108
152
|
## Troubleshooting
|
|
109
153
|
|
|
110
154
|
<Accordions>
|
|
111
155
|
|
|
112
156
|
<Accordion title="from domain is not verified">
|
|
113
157
|
|
|
114
|
-
SNDR rejects sends from unverified domains.
|
|
158
|
+
SNDR rejects sends from unverified domains. Add SPF, DKIM, and DMARC in the dashboard Domains page, click Verify, then retry with that domain in `from`.
|
|
159
|
+
|
|
160
|
+
</Accordion>
|
|
161
|
+
|
|
162
|
+
<Accordion title="recipient_suppressed">
|
|
163
|
+
|
|
164
|
+
The address is on SNDR’s suppression list (bounce/complaint). Remove it only if the recipient opted back in; otherwise pick another address.
|
|
115
165
|
|
|
116
166
|
</Accordion>
|
|
117
167
|
|
|
@@ -129,6 +179,19 @@ No. Use `createMailer` plus `SndrTransport`. Keep vendor extras off the shared e
|
|
|
129
179
|
|
|
130
180
|
</Accordions>
|
|
131
181
|
|
|
182
|
+
## Contact & resources
|
|
183
|
+
|
|
184
|
+
| Contact | Detail |
|
|
185
|
+
| --- | --- |
|
|
186
|
+
| Direct email | [sndr@rkiza.sa](mailto:sndr@rkiza.sa) |
|
|
187
|
+
| Contact form | [sndr.sh/contact](https://www.sndr.sh/contact) |
|
|
188
|
+
| Docs | [sndr.sh/docs](https://www.sndr.sh/docs) |
|
|
189
|
+
| API reference | [API Reference](https://www.sndr.sh/docs/api-reference) |
|
|
190
|
+
| Status | [sndr.sh/status](https://www.sndr.sh/status) |
|
|
191
|
+
| X / Twitter | [@usesndr](https://x.com/usesndr) |
|
|
192
|
+
| LinkedIn | [SNDR company](https://www.linkedin.com/company/104143949) |
|
|
193
|
+
| GitHub | [github.com/rkiza/sndr](https://github.com/rkiza/sndr) |
|
|
194
|
+
|
|
132
195
|
## Learn more
|
|
133
196
|
|
|
134
197
|
- [Email channel](/docs/channels/email) — mailer options and send pipeline
|