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.
Files changed (45) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +20 -5
  4. package/dist/chunk-z1589fjk.js.map +2 -2
  5. package/dist/core/push-types.d.ts +53 -4
  6. package/dist/transports/inbucket.d.ts +196 -0
  7. package/dist/transports/inbucket.js +3 -0
  8. package/dist/transports/inbucket.js.map +10 -0
  9. package/dist/transports/mailpit.d.ts +108 -8
  10. package/dist/transports/mailpit.js +2 -2
  11. package/dist/transports/mailpit.js.map +3 -3
  12. package/dist/transports/taqnyat-sms.d.ts +85 -0
  13. package/dist/transports/taqnyat-sms.js +2 -2
  14. package/dist/transports/taqnyat-sms.js.map +3 -3
  15. package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
  16. package/dist/transports/taqnyat-whatsapp.js +2 -2
  17. package/dist/transports/taqnyat-whatsapp.js.map +3 -3
  18. package/dist/transports/webpush.d.ts +24 -1
  19. package/dist/transports/webpush.js +2 -2
  20. package/dist/transports/webpush.js.map +3 -3
  21. package/dist/webhooks/sndr.js +2 -2
  22. package/dist/webhooks/sndr.js.map +3 -3
  23. package/package.json +7 -2
  24. package/site/content/docs/ai/llms-txt.mdx +2 -0
  25. package/site/content/docs/channels/email.mdx +2 -1
  26. package/site/content/docs/channels/push.mdx +3 -1
  27. package/site/content/docs/decorators/preview.mdx +2 -1
  28. package/site/content/docs/get-started/entrypoints.mdx +1 -1
  29. package/site/content/docs/get-started/support-matrix.mdx +2 -2
  30. package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
  31. package/site/content/docs/guides/webhooks.mdx +2 -0
  32. package/site/content/docs/guides/webpush-interop.mdx +9 -1
  33. package/site/content/docs/reference/exports.mdx +1 -1
  34. package/site/content/docs/reference/push-options.mdx +22 -5
  35. package/site/content/docs/transports/inbucket.mdx +200 -0
  36. package/site/content/docs/transports/index.mdx +2 -3
  37. package/site/content/docs/transports/mailpit.mdx +114 -14
  38. package/site/content/docs/transports/meta.json +5 -4
  39. package/site/content/docs/transports/smtp.mdx +3 -2
  40. package/site/content/docs/transports/sndr.mdx +68 -5
  41. package/site/content/docs/transports/taqnyat.mdx +361 -0
  42. package/site/content/docs/transports/webpush.mdx +208 -17
  43. package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
  44. package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
  45. 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 catcher), [Preview](/docs/decorators/preview) (disk) |
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 SMS" href="/docs/transports/taqnyat-sms" /></Cards>
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><Accordion title="Can I allow a private push relay?">Provide exact hostnames with `allowedEndpointHosts`.</Accordion></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`, `mailpit`, `fcm`, `retry`) |
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` | yes |
20
- | `body` | `string` | yes |
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
- - [Stability policy](/docs/get-started/stability) — union shape is frozen at 1.x
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="FCM transport" href="/docs/transports/fcm" />
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
- ## REST helpers
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
- Vendor extras stay on the transport instance (not on `createMailer`):
87
+ <Tabs items={["Send", "List", "Search", "Message", "Headers", "HTML check", "Link check", "Read", "Delete"]}>
88
+ <Tab value="Send">
79
89
 
80
- | Method | Mailpit API | Meaning |
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
- REST failures throw `MailpitError` (`provider: "mailpit"`). Empty `getMessage("")` throws with status `400`.
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
- "taqnyat-mail",
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 `localhost:1025`.
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) (`sently/transports/mailpit`) or [Preview](/docs/decorators/preview).
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