sently 1.0.0 → 1.1.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/AGENTS.md +3 -2
- package/CHANGELOG.md +97 -0
- 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/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 +24 -1
- 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 +7 -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/inbucket.mdx +200 -0
- package/site/content/docs/transports/index.mdx +2 -3
- package/site/content/docs/transports/mailpit.mdx +114 -14
- package/site/content/docs/transports/meta.json +5 -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 +361 -0
- package/site/content/docs/transports/webpush.mdx +208 -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
|
@@ -29,11 +29,11 @@ FCM uses the current Firebase HTTP API (service-account JWT, no Google SDK). Tha
|
|
|
29
29
|
|
|
30
30
|
| Channel | Available examples |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| Email | Mailgun, Brevo, MailerSend, Plunk, SparkPost, Mailtrap, Mailpit (dev), Loops, SNDR, Taqnyat Mail, Cloudflare Email, … |
|
|
32
|
+
| Email | Mailgun, Brevo, MailerSend, Plunk, SparkPost, Mailtrap, Mailpit (dev), Inbucket (dev), Loops, SNDR, Taqnyat Mail, Cloudflare Email, … |
|
|
33
33
|
| SMS | Taqnyat SMS, Msegat |
|
|
34
34
|
| WhatsApp | Taqnyat WhatsApp |
|
|
35
35
|
| Decorators | `WeightedFallbackTransport` (advanced); preview / idempotency remain email-only |
|
|
36
|
-
| Local email | [Mailpit](/docs/transports/mailpit) (SMTP
|
|
36
|
+
| Local email | [Mailpit](/docs/transports/mailpit) / [Inbucket](/docs/transports/inbucket) (SMTP catchers), [Preview](/docs/decorators/preview) (disk) |
|
|
37
37
|
|
|
38
38
|
Available transports stay in the package. They are promoted to Supported when docs, operability, and smoke coverage meet the bar above.
|
|
39
39
|
|
|
@@ -14,5 +14,6 @@ await msegat.verifyOtp({ id: otp.id, code: "1234", lang: "En" });
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Taqnyat SMS also exposes `sendOtp` and `verifyOtp` on its transport.
|
|
17
|
+
See [Taqnyat](/docs/transports/taqnyat#sms-vendor-extras) for balance, senders, schedule, WhatsApp templates, opt-in, and failover extras.
|
|
17
18
|
|
|
18
|
-
<Cards><Card title="Msegat" href="/docs/transports/msegat" /><Card title="Taqnyat
|
|
19
|
+
<Cards><Card title="Msegat" href="/docs/transports/msegat" /><Card title="Taqnyat" href="/docs/transports/taqnyat#sms" /></Cards>
|
|
@@ -99,6 +99,8 @@ The digest is HMAC-SHA256 over `` `${t}.${rawBody}` ``.
|
|
|
99
99
|
|
|
100
100
|
**Consequence:** Re-serializing JSON changes whitespace and key order and breaks verification — always use the raw body string.
|
|
101
101
|
|
|
102
|
+
Normalized event types include queued → `deferred`, delivered, bounced, complained, opened, clicked, and failed / unsubscribed → `unknown`. See [SNDR](/docs/transports/sndr#webhooks).
|
|
103
|
+
|
|
102
104
|
Default timestamp tolerance is 300 seconds. Pass `{ toleranceSeconds: 0 }` to disable the freshness check.
|
|
103
105
|
|
|
104
106
|
## Troubleshooting
|
|
@@ -8,10 +8,18 @@ source: "src/transports/webpush.ts"
|
|
|
8
8
|
<Callout title="The one rule">Pass the Push API subscription endpoint and both keys exactly as the browser returned them.</Callout>
|
|
9
9
|
|
|
10
10
|
The transport accepts the standard `endpoint`, `keys.p256dh`, and `keys.auth` shape and encrypts payloads using Web Push standards.
|
|
11
|
+
Generate server keys with `generateVapidKeys()` from `sently/transports/webpush` and use the same public key in `pushManager.subscribe({ applicationServerKey })`.
|
|
11
12
|
|
|
12
13
|
## Troubleshooting
|
|
13
14
|
|
|
14
|
-
<Accordions
|
|
15
|
+
<Accordions>
|
|
16
|
+
<Accordion title="Can I allow a private push relay?">
|
|
17
|
+
Provide exact hostnames with `allowedEndpointHosts`.
|
|
18
|
+
</Accordion>
|
|
19
|
+
<Accordion title="Can I send without showing a notification?">
|
|
20
|
+
Pass `data` without `title` / `body`, or set `silent: true` with `data`. The service worker must handle `push` without calling `showNotification`.
|
|
21
|
+
</Accordion>
|
|
22
|
+
</Accordions>
|
|
15
23
|
|
|
16
24
|
## Next
|
|
17
25
|
|
|
@@ -37,7 +37,7 @@ The main `sently` package is for shared types and factories — not every provid
|
|
|
37
37
|
|
|
38
38
|
| Import | Use |
|
|
39
39
|
| ------ | --- |
|
|
40
|
-
| `sently/transports/<name>` | One provider or decorator (e.g. `sndr`, `
|
|
40
|
+
| `sently/transports/<name>` | One provider or decorator (e.g. `sndr`, `inbucket`, `fcm`, `retry`) |
|
|
41
41
|
|
|
42
42
|
HTTP providers such as SNDR, Resend, and Plunk are **not** re-exported from `sently`.
|
|
43
43
|
|
|
@@ -16,9 +16,9 @@ Pick the shape that matches your transport.
|
|
|
16
16
|
|
|
17
17
|
| Field | Type | Required |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| `title` | `string` |
|
|
20
|
-
| `body` | `string` |
|
|
21
|
-
| `data` | `Record<string, unknown>` | no |
|
|
19
|
+
| `title` | `string` | FCM always; Web Push for visible notifications |
|
|
20
|
+
| `body` | `string` | FCM always; Web Push for visible notifications |
|
|
21
|
+
| `data` | `Record<string, unknown>` | no (required for Web Push `silent` / data-only) |
|
|
22
22
|
| `icon` | `string` | no |
|
|
23
23
|
| `ttl` | `number` | no |
|
|
24
24
|
| `messageId` | `string` | no |
|
|
@@ -28,14 +28,27 @@ Pick the shape that matches your transport.
|
|
|
28
28
|
| Field | Type | Required |
|
|
29
29
|
| --- | --- | --- |
|
|
30
30
|
| `subscription` | `PushSubscription` | yes |
|
|
31
|
+
| `urgency` | `"very-low" \| "low" \| "normal" \| "high"` | no — RFC 8030 header |
|
|
32
|
+
| `topic` | `string` | no — 1–32 printable ASCII collapse key |
|
|
33
|
+
| `badge` | `string` | no |
|
|
34
|
+
| `image` | `string` | no |
|
|
35
|
+
| `tag` | `string` | no |
|
|
36
|
+
| `actions` | `{ action, title, icon? }[]` | no |
|
|
37
|
+
| `requireInteraction` | `boolean` | no |
|
|
38
|
+
| `renotify` | `boolean` | no |
|
|
39
|
+
| `silent` | `boolean` | no — encrypt only `data` |
|
|
31
40
|
|
|
32
41
|
A subscription contains `endpoint`, `keys.p256dh`, and `keys.auth`.
|
|
33
42
|
|
|
43
|
+
Omit `title` / `body` and pass `data` for a data-only Web Push (or set `silent: true`).
|
|
44
|
+
|
|
34
45
|
## FCM
|
|
35
46
|
|
|
36
47
|
| Field | Type | Required |
|
|
37
48
|
| --- | --- | --- |
|
|
38
49
|
| `token` | `string` | yes |
|
|
50
|
+
| `title` | `string` | yes |
|
|
51
|
+
| `body` | `string` | yes |
|
|
39
52
|
| `image` | `string` | no |
|
|
40
53
|
|
|
41
54
|
FCM stringifies non-string `data` values before send.
|
|
@@ -58,18 +71,22 @@ Map any channel result with [channel send result](./channel-result).
|
|
|
58
71
|
<Accordion title="Can I send both subscription and token?">
|
|
59
72
|
No. Each call is either Web Push (`subscription`) or FCM (`token`). Match the shape to the transport.
|
|
60
73
|
</Accordion>
|
|
74
|
+
<Accordion title="Can FCM send silent / data-only the same way?">
|
|
75
|
+
No. FCM still requires `title` and `body` on this union. Use Web Push `silent` / data-only for background browser sync.
|
|
76
|
+
</Accordion>
|
|
61
77
|
</Accordions>
|
|
62
78
|
|
|
63
79
|
## Learn more
|
|
64
80
|
|
|
65
81
|
- [Push channel](/docs/channels/push)
|
|
66
|
-
- [
|
|
82
|
+
- [Web Push transport](/docs/transports/webpush)
|
|
83
|
+
- [Stability policy](/docs/get-started/stability) — optional fields may grow in 1.x
|
|
67
84
|
- [Channel send result](./channel-result)
|
|
68
85
|
|
|
69
86
|
## Next
|
|
70
87
|
|
|
71
88
|
<Cards>
|
|
72
89
|
<Card title="Push channel" href="/docs/channels/push" />
|
|
73
|
-
<Card title="
|
|
90
|
+
<Card title="Web Push transport" href="/docs/transports/webpush" />
|
|
74
91
|
<Card title="Channel send result" href="/docs/reference/channel-result" />
|
|
75
92
|
</Cards>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inbucket
|
|
3
|
+
description: Catch outbound email in a local Inbucket instance during development.
|
|
4
|
+
icon: Inbox
|
|
5
|
+
source: "src/transports/inbucket.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Catch outbound email in a local [Inbucket](https://inbucket.org/) instance while you develop.
|
|
9
|
+
Use it for the welcome email or password-reset flow before you point at a production provider.
|
|
10
|
+
|
|
11
|
+
<LiveVerified>
|
|
12
|
+
Email send against a local Inbucket instance succeeded in sently’s live suite (SMTP capture + REST list/get/source/markSeen/purge).
|
|
13
|
+
</LiveVerified>
|
|
14
|
+
|
|
15
|
+
<Callout title="The one rule">
|
|
16
|
+
Use Inbucket only in development — swap to a production transport before you deploy.
|
|
17
|
+
Vendor extras stay on the `InbucketTransport` instance — never on `createMailer`.
|
|
18
|
+
</Callout>
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Start Inbucket (SMTP `2500`, UI `9000`):
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
docker run -d --rm --name inbucket -p 9000:9000 -p 2500:2500 -p 1100:1100 inbucket/inbucket
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
<Steps>
|
|
29
|
+
<Step title="Create the transport">
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createMailer } from "sently/mailer";
|
|
33
|
+
import { InbucketTransport } from "sently/transports/inbucket";
|
|
34
|
+
|
|
35
|
+
const inbucket = new InbucketTransport();
|
|
36
|
+
const mailer = await createMailer({ transport: inbucket });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
</Step>
|
|
40
|
+
<Step title="Send through the mailer">
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
await mailer.send({
|
|
44
|
+
from: "dev@example.com",
|
|
45
|
+
to: "you@example.com",
|
|
46
|
+
subject: "Hello",
|
|
47
|
+
text: "Captured by Inbucket",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
<Step title="Inspect the mailbox">
|
|
53
|
+
|
|
54
|
+
Open `http://localhost:9000`, or list messages from code.
|
|
55
|
+
Stock Inbucket stores `you@example.com` under mailbox `you`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const mailbox = inbucket.mailboxForAddress("you@example.com");
|
|
59
|
+
const inbox = await inbucket.listMailbox(mailbox);
|
|
60
|
+
console.log(inbox[0]?.subject);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
</Step>
|
|
64
|
+
</Steps>
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Meaning |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `host` | `string` | `"localhost"` | SMTP hostname |
|
|
71
|
+
| `port` | `number` | `2500` | SMTP port |
|
|
72
|
+
| `secure` | `boolean` | `false` | Implicit TLS on connect |
|
|
73
|
+
| `requireTLS` | `boolean` | `false` | Refuse AUTH without TLS |
|
|
74
|
+
| `auth` | `SMTPAuth` | — | Optional SMTP credentials |
|
|
75
|
+
| `tls` | `TLSOptions` | — | TLS options when TLS is enabled |
|
|
76
|
+
| `connectionTimeout` | `number` | — | Socket connect timeout (ms) |
|
|
77
|
+
| `adapter` | `SocketAdapter` | auto-detected | Runtime TCP adapter |
|
|
78
|
+
| `apiUrl` | `string` | `"http://localhost:9000"` | Web UI / REST API base |
|
|
79
|
+
| `mailboxNaming` | `"local" \| "full" \| "domain"` | `"local"` | How `mailboxForAddress` maps an email |
|
|
80
|
+
|
|
81
|
+
`provider` is `"inbucket"`. `verify()` checks SMTP; `close()` closes the socket adapter.
|
|
82
|
+
`webUrl` is the UI base (same as `apiUrl`).
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
Pick a branch. Channel send goes through `mailer`; everything else is called on `inbucket`.
|
|
87
|
+
Inbucket is mailbox-centric — pass a mailbox name (or derive it with `mailboxForAddress`).
|
|
88
|
+
|
|
89
|
+
<Tabs items={["Send", "List", "Message", "Source", "Mark seen", "Delete", "Purge"]}>
|
|
90
|
+
<Tab value="Send">
|
|
91
|
+
|
|
92
|
+
Transactional send via the channel mailer (SMTP into Inbucket).
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await mailer.send({
|
|
96
|
+
from: "dev@example.com",
|
|
97
|
+
to: "you@example.com",
|
|
98
|
+
subject: "Welcome",
|
|
99
|
+
html: "<h1>Hello</h1>",
|
|
100
|
+
text: "Hello",
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
</Tab>
|
|
105
|
+
<Tab value="List">
|
|
106
|
+
|
|
107
|
+
List messages in a mailbox (`GET /api/v1/mailbox/{name}`).
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const mailbox = inbucket.mailboxForAddress("you@example.com");
|
|
111
|
+
const inbox = await inbucket.listMailbox(mailbox);
|
|
112
|
+
console.log(inbox.length, inbox[0]?.subject);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
</Tab>
|
|
116
|
+
<Tab value="Message">
|
|
117
|
+
|
|
118
|
+
Full body, headers, and attachments for a message id.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const full = await inbucket.getMessage(mailbox, inbox[0]!.id);
|
|
122
|
+
console.log(full.body.text, full.body.html, full.header.Subject);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
</Tab>
|
|
126
|
+
<Tab value="Source">
|
|
127
|
+
|
|
128
|
+
Raw RFC822 source (`GET …/source`) for MIME assertions.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const source = await inbucket.getSource(mailbox, inbox[0]!.id);
|
|
132
|
+
console.log(source.includes("Subject: Welcome"));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</Tab>
|
|
136
|
+
<Tab value="Mark seen">
|
|
137
|
+
|
|
138
|
+
Mark one message as seen (`PATCH` with `{ seen: true }`).
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
await inbucket.markSeen(mailbox, inbox[0]!.id);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
</Tab>
|
|
145
|
+
<Tab value="Delete">
|
|
146
|
+
|
|
147
|
+
Delete one message by id.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await inbucket.deleteMessage(mailbox, inbox[0]!.id);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</Tab>
|
|
154
|
+
<Tab value="Purge">
|
|
155
|
+
|
|
156
|
+
Clear every message in a mailbox.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await inbucket.purgeMailbox(mailbox);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
</Tab>
|
|
163
|
+
</Tabs>
|
|
164
|
+
|
|
165
|
+
REST failures throw `InbucketError` (`provider: "inbucket"`).
|
|
166
|
+
Empty mailbox / id arguments throw with status `400`.
|
|
167
|
+
|
|
168
|
+
## Troubleshooting
|
|
169
|
+
|
|
170
|
+
<Accordions>
|
|
171
|
+
<Accordion title="Connection refused on port 2500">
|
|
172
|
+
Inbucket is not running, or the SMTP port is remapped. Start the container above, or set `host` / `port` to match your install.
|
|
173
|
+
</Accordion>
|
|
174
|
+
<Accordion title="listMailbox returns empty after send">
|
|
175
|
+
Check mailbox naming. Stock Inbucket uses the local-part (`you` for `you@example.com`).
|
|
176
|
+
If your instance sets `INBUCKET_MAILBOXNAMING=full` or `domain`, match that with `mailboxNaming`.
|
|
177
|
+
</Accordion>
|
|
178
|
+
<Accordion title="API helpers fail but send works">
|
|
179
|
+
SMTP and the UI/API can bind to different hosts. Set `apiUrl` to the web base (default `http://localhost:9000`).
|
|
180
|
+
</Accordion>
|
|
181
|
+
<Accordion title="Should I use createSMTPMailer instead?">
|
|
182
|
+
Yes, if you only need SMTP. `InbucketTransport` adds local defaults and REST helpers for tests and inspection.
|
|
183
|
+
</Accordion>
|
|
184
|
+
</Accordions>
|
|
185
|
+
|
|
186
|
+
## Learn more
|
|
187
|
+
|
|
188
|
+
- [Mailpit](./mailpit) — another local SMTP catcher with a different REST shape
|
|
189
|
+
- [SMTP](./smtp) — generic SMTP when you are not on a catcher
|
|
190
|
+
- [Preview](/docs/decorators/preview) — write `.eml` files to disk instead
|
|
191
|
+
- [Email channel](/docs/channels/email) — `createMailer` contract
|
|
192
|
+
- [Inbucket REST API](https://github.com/inbucket/inbucket/wiki/REST-API) — mailbox endpoints on the catcher
|
|
193
|
+
|
|
194
|
+
## Next
|
|
195
|
+
|
|
196
|
+
<Cards>
|
|
197
|
+
<Card title="Mailpit" href="/docs/transports/mailpit" />
|
|
198
|
+
<Card title="Email channel" href="/docs/channels/email" />
|
|
199
|
+
<Card title="Transports" href="/docs/transports" />
|
|
200
|
+
</Cards>
|
|
@@ -57,18 +57,17 @@ Every import below is an exported package subpath.
|
|
|
57
57
|
| [SparkPost](./sparkpost) | Email | `sently/transports/sparkpost` |
|
|
58
58
|
| [Mailtrap](./mailtrap) | Email | `sently/transports/mailtrap` |
|
|
59
59
|
| [Mailpit](./mailpit) | Email (dev) | `sently/transports/mailpit` |
|
|
60
|
+
| [Inbucket](./inbucket) | Email (dev) | `sently/transports/inbucket` |
|
|
60
61
|
| [Loops](./loops) | Email | `sently/transports/loops` |
|
|
61
62
|
| [Cloudflare Email](./cloudflare-email) | Email | `sently/transports/cloudflare-email` |
|
|
62
63
|
| [SNDR](./sndr) | Email | `sently/transports/sndr` |
|
|
63
|
-
| [Taqnyat Mail](./taqnyat-mail) | Email | `sently/transports/taqnyat-mail` |
|
|
64
64
|
| [Twilio SMS](./twilio-sms) | SMS | `sently/transports/twilio-sms` |
|
|
65
|
-
| [Taqnyat SMS](./taqnyat-sms) | SMS | `sently/transports/taqnyat-sms` |
|
|
66
65
|
| [Msegat](./msegat) | SMS | `sently/transports/msegat` |
|
|
67
66
|
| [Unifonic](./unifonic) | SMS | `sently/transports/unifonic` |
|
|
68
67
|
| [WhatsApp Cloud](./whatsapp-cloud) | WhatsApp | `sently/transports/whatsapp-cloud` |
|
|
69
|
-
| [Taqnyat WhatsApp](./taqnyat-whatsapp) | WhatsApp | `sently/transports/taqnyat-whatsapp` |
|
|
70
68
|
| [Web Push](./webpush) | Push | `sently/transports/webpush` |
|
|
71
69
|
| [FCM](./fcm) | Push | `sently/transports/fcm` |
|
|
70
|
+
| [Taqnyat](./taqnyat) | Email · SMS · WhatsApp | `sently/transports/taqnyat-mail`, `taqnyat-sms`, `taqnyat-whatsapp` |
|
|
72
71
|
|
|
73
72
|
For retry, fallback, preview, and idempotency wrappers, see [Decorators](../decorators).
|
|
74
73
|
|
|
@@ -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,18 @@
|
|
|
15
17
|
"plunk",
|
|
16
18
|
"sparkpost",
|
|
17
19
|
"mailtrap",
|
|
18
|
-
"mailpit",
|
|
19
20
|
"loops",
|
|
20
21
|
"cloudflare-email",
|
|
21
22
|
"sndr",
|
|
22
|
-
"
|
|
23
|
+
"---Email Dev---",
|
|
24
|
+
"mailpit",
|
|
25
|
+
"inbucket",
|
|
23
26
|
"---SMS---",
|
|
24
27
|
"twilio-sms",
|
|
25
|
-
"taqnyat-sms",
|
|
26
28
|
"msegat",
|
|
27
29
|
"unifonic",
|
|
28
30
|
"---WhatsApp---",
|
|
29
31
|
"whatsapp-cloud",
|
|
30
|
-
"taqnyat-whatsapp",
|
|
31
32
|
"---Push---",
|
|
32
33
|
"webpush",
|
|
33
34
|
"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
|
|