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
package/AGENTS.md
CHANGED
|
@@ -24,10 +24,11 @@ Providers are transports under those senders. Vendor extras (OTP, account utilit
|
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
26
|
bun install
|
|
27
|
-
bun test
|
|
27
|
+
bun test # unit/integration only — never runs *.live.test.ts
|
|
28
|
+
bun run test:live # local-only provider live suites (requires *_LIVE=1)
|
|
28
29
|
bun run site:dev
|
|
29
30
|
bun run site:build
|
|
30
|
-
bun run verify
|
|
31
|
+
bun run verify # lint, typecheck, build, site:build, then bun test
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
Do not invent APIs. Prefer imports from published subpaths (`sently/mailer`, `sently/sms`, `sently/transports/*`).
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,115 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [1.2.0] — 2026-08-03
|
|
4
|
+
|
|
5
|
+
### ✨ Added
|
|
6
|
+
|
|
7
|
+
- **Hostinger transport** — `sently/transports/hostinger` sends from a managed
|
|
8
|
+
Hostinger mailbox through the Hostinger Mail API (Bearer token +
|
|
9
|
+
mailbox resource ID); `listMailboxes()` discovers mailbox IDs and `verify()`
|
|
10
|
+
checks token scope; vendor extras `sendReply` / `sendForward` thread by
|
|
11
|
+
folder + IMAP UID; `hostingerSmtpConfig()` fills ready SMTP settings for
|
|
12
|
+
`createSMTPMailer` (`smtp.hostinger.com`, ports 465/587)
|
|
13
|
+
- **Hostinger brand mark** — React `HostingerLogo` (full wordmark from
|
|
14
|
+
hostinger.com) / `HostingerLogoIcon` (purple H); docs title badge uses the
|
|
15
|
+
wordmark, sidebar/marquee use the H mark
|
|
16
|
+
- **Hostinger docs** — Mail API + SMTP feature tabs (send, HTML, attachments,
|
|
17
|
+
CC/BCC, reply/forward, mailboxes, verify, SSL/STARTTLS/pool) at the same
|
|
18
|
+
depth as Taqnyat; links to Hostinger developers / Mail API / SMTP tutorial;
|
|
19
|
+
green `LiveVerified` on SMTP (production relay previously proven); homepage
|
|
20
|
+
marquee marks Hostinger verified
|
|
21
|
+
- **Taqnyat Email live verified** — opt-in live suite delivered a real message
|
|
22
|
+
via `mailSend.php` with an approved portal sender; docs show the green
|
|
23
|
+
`LiveVerified` callout for Email alongside SMS and WhatsApp
|
|
24
|
+
|
|
25
|
+
## [1.1.1] — 2026-08-02
|
|
26
|
+
|
|
27
|
+
### 🐛 Fixed
|
|
28
|
+
|
|
29
|
+
- **Publish CI Test job** — build `dist` before `bun test` so parallel Verify no
|
|
30
|
+
longer fails api-surface suites that import published bundles
|
|
31
|
+
- **`bun test` skips live suites** — clears `TAQNYAT_LIVE` / `SNDR_LIVE` /
|
|
32
|
+
`INBUCKET_LIVE` and ignores `**/*.live.test.ts`; `bun run test:live` is
|
|
33
|
+
local-only (refuses `CI` / `GITHUB_ACTIONS`)
|
|
34
|
+
|
|
35
|
+
## [1.1.0] — 2026-08-02
|
|
36
|
+
|
|
37
|
+
### ✨ Added
|
|
38
|
+
|
|
39
|
+
- **Web Push live verified** — homepage transport strip marks Web Push verified
|
|
40
|
+
after okengine delivered a real browser notification with VAPID; docs page
|
|
41
|
+
shows the green `LiveVerified` callout
|
|
42
|
+
- **Web Push brand mark** — React `WebPushLogo` / `WebPushLogoIcon` (blue
|
|
43
|
+
notification tile + bell + badge); docs title badge + sidebar/marquee use
|
|
44
|
+
the mark
|
|
45
|
+
- **Web Push protocol knobs** — `urgency` / `topic` headers (RFC 8030), rich
|
|
46
|
+
Notification fields (`badge`, `image`, `tag`, `actions`, `requireInteraction`,
|
|
47
|
+
`renotify`), `silent` / data-only payloads, and `generateVapidKeys()` on
|
|
48
|
+
`sently/transports/webpush`
|
|
49
|
+
- **Inbucket transport** — `sently/transports/inbucket` for local SMTP capture
|
|
50
|
+
(defaults `localhost:2500` / UI `http://localhost:9000`) with REST helpers
|
|
51
|
+
`listMailbox`, `getMessage`, `getSource`, `markSeen`, `deleteMessage`,
|
|
52
|
+
`purgeMailbox`, and `mailboxForAddress`; opt-in live suite via
|
|
53
|
+
`INBUCKET_LIVE=1`; docs under a new **Email Dev** sidebar group with Mailpit
|
|
54
|
+
- **Mailpit vendor extras** — `search`, `getHeaders`, `htmlCheck`, `linkCheck`,
|
|
55
|
+
and `setRead` on `MailpitTransport` for local inbox assertions; docs cover
|
|
56
|
+
the helpers and link to the Mailpit API
|
|
57
|
+
- **Mailpit brand mark** — React `MailpitLogo` / `MailpitLogoIcon` from the
|
|
58
|
+
official SVG; docs title badge + sidebar/marquee use the mark; Mailpit is
|
|
59
|
+
live-verified in the transport strip
|
|
60
|
+
- **Taqnyat live suite** — opt-in `tests/integration/taqnyat.live.test.ts`
|
|
61
|
+
(`TAQNYAT_LIVE=1`) with free preflight before SMS / Mail / WhatsApp sends;
|
|
62
|
+
live env keys documented in `.env.example`
|
|
63
|
+
- **Taqnyat transport docs** — single `/docs/transports/taqnyat` page under a
|
|
64
|
+
new **Multi-channel** sidebar group (SMS / WhatsApp / Email sections);
|
|
65
|
+
green `LiveVerified` on SMS and WhatsApp only
|
|
66
|
+
- **Taqnyat SMS vendor extras** — `getBalance`, `listSenders`, `schedule`,
|
|
67
|
+
`deleteScheduled` on `TaqnyatSmsTransport` (OTP helpers unchanged)
|
|
68
|
+
- **Taqnyat WhatsApp vendor extras** — `listTemplates`, `createTemplate`,
|
|
69
|
+
`deleteTemplate`, `optIn` / `optOut`, `sendWithFailover` on
|
|
70
|
+
`TaqnyatWhatsAppTransport`
|
|
71
|
+
- **Taqnyat brand marks** — React `TaqnyatLogo` / `TaqnyatLogoIcon` replace the
|
|
72
|
+
`TQ` monogram; marquee uses the full wordmark alone, sidebar uses a square
|
|
73
|
+
single-dot icon (docs page keeps the brand wordmark)
|
|
74
|
+
- **Transport strip tiers** — homepage marquee legend:
|
|
75
|
+
color = live verified, muted = available, sponsors reserved;
|
|
76
|
+
verified chips use the same icon+label rhythm as the rest (brand-colored
|
|
77
|
+
mark); full wordmarks stay on docs pages; priority marks sit after a ~25%
|
|
78
|
+
available lead-in
|
|
79
|
+
- **SNDR brand mark** — React `SndrLogo` / `SndrLogoIcon` from
|
|
80
|
+
[sndr.sh](https://www.sndr.sh/sndr-logo.svg); docs page rewritten with
|
|
81
|
+
LiveVerified, contact resources, and platform coverage table
|
|
82
|
+
- **SNDR webhooks** — map `email.queued`, `failed`, `complained`, `opened`,
|
|
83
|
+
and `clicked` into normalized `EmailEvent` types
|
|
84
|
+
|
|
85
|
+
### ♻️ Changed
|
|
86
|
+
|
|
87
|
+
- **Homepage messaging** — clearer TypeScript notification/messaging
|
|
88
|
+
positioning (Email · SMS · WhatsApp · Push), one-package signal, provider
|
|
89
|
+
swap strip (`SMTP → SES → Resend`), tighter why cards; no section removals
|
|
90
|
+
- **`/llms.txt`** — agent preamble states notification/messaging library
|
|
91
|
+
(not a queue); Fumadocs handbook index unchanged underneath
|
|
92
|
+
- **Publish CI** — `.github/workflows/publish.yml` runs only on `v*` tag
|
|
93
|
+
pushes; Verify (typecheck, lint, build, test) and npm / JSR publish jobs
|
|
94
|
+
run in parallel (no longer sequential npm-then-JSR on push to `main`)
|
|
95
|
+
- **README** — shieldcn grid/left header with cleaned logo SVGs (no
|
|
96
|
+
clip-path square), drop Bundlephobia badge, add npm downloads chart;
|
|
97
|
+
version badges match the okengine pattern (`/npm/…`, `/jsr/…`)
|
|
98
|
+
|
|
99
|
+
### 🔥 Removed
|
|
100
|
+
|
|
101
|
+
- **`test.yml` workflow** — PR / push CI matrix removed; release Verify lives
|
|
102
|
+
in `publish.yml` on version tags
|
|
103
|
+
|
|
104
|
+
### 🐛 Fixed
|
|
105
|
+
|
|
106
|
+
- **Taqnyat WhatsApp queued accept** — treat `statuses: "PENDING"` (and
|
|
107
|
+
template/text accepts without `message_id` yet) as success instead of an
|
|
108
|
+
empty id edge case
|
|
109
|
+
- **`bun run verify` never hits provider live suites** — clears `TAQNYAT_LIVE` /
|
|
110
|
+
`SNDR_LIVE` / `INBUCKET_LIVE` and ignores `**/*.live.test.ts` so a leftover
|
|
111
|
+
shell export cannot send real SMS / email / WhatsApp during verify; use
|
|
112
|
+
`bun run test:live` to opt in
|
|
4
113
|
|
|
5
114
|
## [1.0.1] — 2026-08-01
|
|
6
115
|
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<picture>
|
|
3
3
|
<source
|
|
4
4
|
media="(prefers-color-scheme: dark)"
|
|
5
|
-
srcset="https://shieldcn.dev/header/
|
|
5
|
+
srcset="https://shieldcn.dev/header/grid.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/dev/site/public/sentlyIconLogo-w.svg&theme=zinc&size=banner&mode=dark&font=geist&align=left"
|
|
6
6
|
/>
|
|
7
7
|
<img
|
|
8
8
|
alt="sently — One API. Four channels. Every runtime."
|
|
9
|
-
src="https://shieldcn.dev/header/
|
|
9
|
+
src="https://shieldcn.dev/header/grid.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/dev/site/public/sentlyIconLogo-k.svg&theme=zinc&size=banner&mode=light&font=geist&align=left"
|
|
10
10
|
width="750"
|
|
11
11
|
/>
|
|
12
12
|
</picture>
|
|
@@ -15,13 +15,28 @@
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
<a href="https://www.npmjs.com/package/sently"><img alt="npm" src="https://shieldcn.dev/npm/sently.svg?size=sm&variant=outline" /></a>
|
|
17
17
|
<a href="https://jsr.io/@alialnaghmoush/sently"><img alt="JSR" src="https://shieldcn.dev/jsr/@alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
|
|
18
|
-
<a href="https://bundlephobia.com/package/sently"><img alt="bundle" src="https://shieldcn.dev/bundlephobia/minzip/sently.svg?size=sm&variant=outline" /></a>
|
|
19
18
|
<a href="https://opensource.org/licenses/MIT"><img alt="MIT" src="https://shieldcn.dev/npm/license/sently.svg?size=sm&variant=outline" /></a>
|
|
20
|
-
<a href="https://bun.sh"><img alt="Bun" src="https://shieldcn.dev/badge/Bun-ready
|
|
19
|
+
<a href="https://bun.sh"><img alt="Bun" src="https://shieldcn.dev/badge/Bun-ready.svg?logo=bun&size=sm&variant=outline" /></a>
|
|
21
20
|
<a href="https://github.com/alialnaghmoush/sently/stargazers"><img alt="stars" src="https://shieldcn.dev/github/stars/alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
|
|
22
21
|
<a href="https://github.com/alialnaghmoush/sently/actions"><img alt="CI" src="https://shieldcn.dev/github/ci/alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
|
|
23
22
|
</p>
|
|
24
23
|
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://www.npmjs.com/package/sently">
|
|
26
|
+
<picture>
|
|
27
|
+
<source
|
|
28
|
+
media="(prefers-color-scheme: dark)"
|
|
29
|
+
srcset="https://shieldcn.dev/chart/npm/sently.svg?theme=zinc&mode=dark&width=750&height=280"
|
|
30
|
+
/>
|
|
31
|
+
<img
|
|
32
|
+
alt="npm downloads"
|
|
33
|
+
src="https://shieldcn.dev/chart/npm/sently.svg?theme=zinc&mode=light&width=750&height=280"
|
|
34
|
+
width="750"
|
|
35
|
+
/>
|
|
36
|
+
</picture>
|
|
37
|
+
</a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
25
40
|
<p align="center">
|
|
26
41
|
<em>Start with email, add SMS and push later — one sender shape, one error model, one retry path. Swap the transport; keep your call sites. Node, Bun, Deno, Workers.</em>
|
|
27
42
|
</p>
|
|
@@ -122,4 +137,4 @@ More detail: [Compare](https://sently.omqkhafi.dev/docs/guides/compare) · [Stab
|
|
|
122
137
|
|
|
123
138
|
Local docs: `bun run site:dev`. Verify: `bun run verify`.
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
Semver-stable channel contracts. Published on [npm](https://www.npmjs.com/package/sently) (provenance + Trusted Publishing) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/core/push-types.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * Shared push channel contracts — sibling types to email MailOptions/Transport/Hooks.\n *\n * Sently-first: this contract is the stable push surface. Provider transports\n * implement {@link PushTransport}. Web Push uses a browser subscription; FCM\n * uses a device registration token.\n */\nimport type { VerifyResult } from \"./types.js\";\n\n/** Browser push subscription as returned by the Push API. */\nexport interface PushSubscription {\n /** Push service endpoint URL. */\n endpoint: string;\n /** Client encryption keys for RFC 8291 payload encryption. */\n keys: {\n /** Base64url-encoded P-256 ECDH public key. */\n p256dh: string;\n /** Base64url-encoded authentication secret. */\n auth: string;\n };\n}\n\n/** Shared notification fields for Web Push and FCM. */\ninterface PushNotificationFields {\n
|
|
5
|
+
"/**\n * @module\n * Shared push channel contracts — sibling types to email MailOptions/Transport/Hooks.\n *\n * Sently-first: this contract is the stable push surface. Provider transports\n * implement {@link PushTransport}. Web Push uses a browser subscription; FCM\n * uses a device registration token.\n */\nimport type { VerifyResult } from \"./types.js\";\n\n/** Browser push subscription as returned by the Push API. */\nexport interface PushSubscription {\n /** Push service endpoint URL. */\n endpoint: string;\n /** Client encryption keys for RFC 8291 payload encryption. */\n keys: {\n /** Base64url-encoded P-256 ECDH public key. */\n p256dh: string;\n /** Base64url-encoded authentication secret. */\n auth: string;\n };\n}\n\n/** Shared notification fields for Web Push and FCM. */\ninterface PushNotificationFields {\n /**\n * Notification title.\n * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.\n */\n title?: string;\n /**\n * Notification body text.\n * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.\n */\n body?: string;\n /** Arbitrary application data attached to the notification. */\n data?: Record<string, unknown>;\n /** Notification icon URL (Web Push / some FCM platforms). */\n icon?: string;\n /** Time-to-live in seconds for the push message. */\n ttl?: number;\n /** Client-supplied message identifier when known. */\n messageId?: string;\n}\n\n/** RFC 8030 `Urgency` values for Web Push delivery priority. */\nexport type WebPushUrgency = \"very-low\" | \"low\" | \"normal\" | \"high\";\n\n/** Browser Notification API action button. */\nexport interface WebPushAction {\n /** Action identifier delivered to the service worker. */\n action: string;\n /** Button label. */\n title: string;\n /** Optional action icon URL. */\n icon?: string;\n}\n\n/** Options for sending a Web Push notification (VAPID / browser subscription). */\nexport interface WebPushOptions extends PushNotificationFields {\n /** Target browser push subscription. */\n subscription: PushSubscription;\n /**\n * RFC 8030 `Urgency` header — delivery priority hint for the push service.\n * Defaults to omitting the header (service default, typically `normal`).\n */\n urgency?: WebPushUrgency;\n /**\n * RFC 8030 `Topic` header — collapse key so a newer message replaces a pending\n * one with the same topic (ASCII, max 32 characters).\n */\n topic?: string;\n /** Notification badge URL (small monochrome icon). */\n badge?: string;\n /** Large image URL shown with the notification. */\n image?: string;\n /** Tag for replacing an existing notification with the same tag. */\n tag?: string;\n /** Action buttons shown on the notification. */\n actions?: WebPushAction[];\n /** Keep the notification open until the user interacts. */\n requireInteraction?: boolean;\n /** Re-alert when replacing a notification with the same `tag`. */\n renotify?: boolean;\n /**\n * Encrypt only `data` (no visible notification fields). Requires `data`.\n * Use for background sync without showing a system notification — the service\n * worker must handle `push` without calling `showNotification`.\n */\n silent?: boolean;\n}\n\n/**\n * Options for sending via Firebase Cloud Messaging (device token).\n * Data values are stringified by the FCM transport when needed.\n */\nexport interface FcmPushOptions extends PushNotificationFields {\n /** FCM registration token for the target device. */\n token: string;\n /** Notification title (required for FCM). */\n title: string;\n /** Notification body text (required for FCM). */\n body: string;\n /** Optional image URL for the notification. */\n image?: string;\n}\n\n/** Options for sending a push notification (Web Push or FCM). */\nexport type PushOptions = WebPushOptions | FcmPushOptions;\n\n/** True when options target FCM (device token) rather than Web Push. */\nexport function isFcmPushOptions(options: PushOptions): options is FcmPushOptions {\n return \"token\" in options && typeof (options as FcmPushOptions).token === \"string\";\n}\n\n/** True when options target Web Push (browser subscription). */\nexport function isWebPushOptions(options: PushOptions): options is WebPushOptions {\n return (\n \"subscription\" in options &&\n typeof (options as WebPushOptions).subscription?.endpoint === \"string\"\n );\n}\n\n/** Result returned after a push notification is accepted by the push service. */\nexport interface PushSendResult {\n /** Client-generated or assigned message identifier. */\n messageId: string;\n /** Delivery status string (e.g. `\"accepted\"`). */\n status: string;\n /** Human-readable or raw status text (often the HTTP status). */\n response: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider?: string;\n /**\n * Zero-based index of the transport that handled the send in a fallback chain.\n * Set by {@link FallbackTransport}.\n */\n providerIndex?: number;\n}\n\n/** Pluggable push delivery backend (Web Push, FCM, …). */\nexport interface PushTransport {\n /**\n * Stable provider identifier for observability (e.g. `\"webpush\"`, `\"fcm\"`).\n * Prefer this over constructor-name inference in hooks.\n */\n readonly provider?: string;\n /** Send a push notification through this transport. */\n send(options: PushOptions): Promise<PushSendResult>;\n /** Test connectivity and credentials without sending. */\n verify?(): Promise<VerifyResult>;\n /** Release resources held by the transport. */\n close?(): Promise<void>;\n}\n\n/**\n * Context passed to push lifecycle hooks.\n * Uses a redacted subscription endpoint or device-token fingerprint (no natural `to`).\n */\nexport interface PushHookContext {\n /** Message-ID when known (from options or send result). */\n messageId?: string;\n /**\n * Redacted push target for observability: `origin/#<short-hash>` (Web Push)\n * or `fcm:#<short-hash>` (FCM device token).\n * Never the full endpoint URL or raw device token.\n */\n endpoint: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider: string;\n}\n\n/** Optional lifecycle hooks for metrics, tracing, and observability on every push send. */\nexport interface PushHooks {\n /** Fired before the transport sends the notification. */\n onSend?: (ctx: PushHookContext) => void | Promise<void>;\n /**\n * Fired after a successful send.\n * @param durationMs — elapsed milliseconds from send start to success (optional third argument).\n */\n onSuccess?: (\n ctx: PushHookContext,\n result: PushSendResult,\n durationMs?: number,\n ) => void | Promise<void>;\n /**\n * Fired when a send throws (error is re-thrown after the hook runs).\n * @param durationMs — elapsed milliseconds from send start to failure (optional third argument).\n */\n onError?: (ctx: PushHookContext, error: unknown, durationMs?: number) => void | Promise<void>;\n /** Fired before each retry attempt (requires {@link RetryTransport}). */\n onRetry?: (ctx: PushHookContext, attempt: number, error: unknown) => void | Promise<void>;\n /**\n * Fired when {@link FallbackTransport} fails over to the next provider.\n * Requires a fallback (or weighted fallback) transport in the push stack.\n */\n onFallback?: (\n ctx: PushHookContext,\n failedProvider: string,\n nextProvider: string,\n error: unknown,\n ) => void | Promise<void>;\n}\n\n/**\n * A push plugin transforms {@link PushOptions} before the transport sends.\n * Plugins run sequentially. Each receives the output of the previous.\n * Return a new options object — do not mutate the input.\n */\nexport type PushPlugin =\n | ((options: PushOptions) => PushOptions)\n | ((options: PushOptions) => Promise<PushOptions>);\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
7
|
+
"mappings": "AA+GO,SAAS,CAAgB,CAAC,EAAiD,CAChF,MAAO,UAAW,GAAW,OAAQ,EAA2B,QAAU,SAIrE,SAAS,CAAgB,CAAC,EAAiD,CAChF,MACE,iBAAkB,GAClB,OAAQ,EAA2B,cAAc,WAAa",
|
|
8
8
|
"debugId": "F4A2DC6CC43BC46D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -21,10 +21,16 @@ export interface PushSubscription {
|
|
|
21
21
|
}
|
|
22
22
|
/** Shared notification fields for Web Push and FCM. */
|
|
23
23
|
interface PushNotificationFields {
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Notification title.
|
|
26
|
+
* Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.
|
|
27
|
+
*/
|
|
28
|
+
title?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Notification body text.
|
|
31
|
+
* Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.
|
|
32
|
+
*/
|
|
33
|
+
body?: string;
|
|
28
34
|
/** Arbitrary application data attached to the notification. */
|
|
29
35
|
data?: Record<string, unknown>;
|
|
30
36
|
/** Notification icon URL (Web Push / some FCM platforms). */
|
|
@@ -34,10 +40,49 @@ interface PushNotificationFields {
|
|
|
34
40
|
/** Client-supplied message identifier when known. */
|
|
35
41
|
messageId?: string;
|
|
36
42
|
}
|
|
43
|
+
/** RFC 8030 `Urgency` values for Web Push delivery priority. */
|
|
44
|
+
export type WebPushUrgency = "very-low" | "low" | "normal" | "high";
|
|
45
|
+
/** Browser Notification API action button. */
|
|
46
|
+
export interface WebPushAction {
|
|
47
|
+
/** Action identifier delivered to the service worker. */
|
|
48
|
+
action: string;
|
|
49
|
+
/** Button label. */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Optional action icon URL. */
|
|
52
|
+
icon?: string;
|
|
53
|
+
}
|
|
37
54
|
/** Options for sending a Web Push notification (VAPID / browser subscription). */
|
|
38
55
|
export interface WebPushOptions extends PushNotificationFields {
|
|
39
56
|
/** Target browser push subscription. */
|
|
40
57
|
subscription: PushSubscription;
|
|
58
|
+
/**
|
|
59
|
+
* RFC 8030 `Urgency` header — delivery priority hint for the push service.
|
|
60
|
+
* Defaults to omitting the header (service default, typically `normal`).
|
|
61
|
+
*/
|
|
62
|
+
urgency?: WebPushUrgency;
|
|
63
|
+
/**
|
|
64
|
+
* RFC 8030 `Topic` header — collapse key so a newer message replaces a pending
|
|
65
|
+
* one with the same topic (ASCII, max 32 characters).
|
|
66
|
+
*/
|
|
67
|
+
topic?: string;
|
|
68
|
+
/** Notification badge URL (small monochrome icon). */
|
|
69
|
+
badge?: string;
|
|
70
|
+
/** Large image URL shown with the notification. */
|
|
71
|
+
image?: string;
|
|
72
|
+
/** Tag for replacing an existing notification with the same tag. */
|
|
73
|
+
tag?: string;
|
|
74
|
+
/** Action buttons shown on the notification. */
|
|
75
|
+
actions?: WebPushAction[];
|
|
76
|
+
/** Keep the notification open until the user interacts. */
|
|
77
|
+
requireInteraction?: boolean;
|
|
78
|
+
/** Re-alert when replacing a notification with the same `tag`. */
|
|
79
|
+
renotify?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Encrypt only `data` (no visible notification fields). Requires `data`.
|
|
82
|
+
* Use for background sync without showing a system notification — the service
|
|
83
|
+
* worker must handle `push` without calling `showNotification`.
|
|
84
|
+
*/
|
|
85
|
+
silent?: boolean;
|
|
41
86
|
}
|
|
42
87
|
/**
|
|
43
88
|
* Options for sending via Firebase Cloud Messaging (device token).
|
|
@@ -46,6 +91,10 @@ export interface WebPushOptions extends PushNotificationFields {
|
|
|
46
91
|
export interface FcmPushOptions extends PushNotificationFields {
|
|
47
92
|
/** FCM registration token for the target device. */
|
|
48
93
|
token: string;
|
|
94
|
+
/** Notification title (required for FCM). */
|
|
95
|
+
title: string;
|
|
96
|
+
/** Notification body text (required for FCM). */
|
|
97
|
+
body: string;
|
|
49
98
|
/** Optional image URL for the notification. */
|
|
50
99
|
image?: string;
|
|
51
100
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SentlyError } from "../core/errors.js";
|
|
2
|
+
import type { MailOptions, SendResult, SMTPConfig, Transport, VerifyResult } from "../core/types.js";
|
|
3
|
+
/** Hostinger SMTP hostname. */
|
|
4
|
+
export declare const HOSTINGER_SMTP_HOST = "smtp.hostinger.com";
|
|
5
|
+
/** SSL/TLS-on-connect submission port (default for {@link hostingerSmtpConfig}). */
|
|
6
|
+
export declare const HOSTINGER_SMTP_PORT_SSL = 465;
|
|
7
|
+
/** STARTTLS submission port. */
|
|
8
|
+
export declare const HOSTINGER_SMTP_PORT_STARTTLS = 587;
|
|
9
|
+
/** Default Hostinger Mail API base URL. */
|
|
10
|
+
export declare const HOSTINGER_API_BASE_URL = "https://api.mail.hostinger.com";
|
|
11
|
+
/** Hostinger Mail API configuration. */
|
|
12
|
+
export interface HostingerConfig {
|
|
13
|
+
/** API token from hPanel → Emails → Agentic Mail → API access (shown once at creation). */
|
|
14
|
+
token: string;
|
|
15
|
+
/** Resource ID of the managed mailbox to send from (e.g. `"AC1a2b3c4d5e6f7g"`). */
|
|
16
|
+
mailbox: string;
|
|
17
|
+
/** API base URL. Default: {@link HOSTINGER_API_BASE_URL}. */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ready SMTP options for Hostinger Email.
|
|
22
|
+
* Pass the result of {@link hostingerSmtpConfig} to `createSMTPMailer`.
|
|
23
|
+
*/
|
|
24
|
+
export interface HostingerSmtpOptions {
|
|
25
|
+
/** Full mailbox address — this is the SMTP username. */
|
|
26
|
+
user: string;
|
|
27
|
+
/** Mailbox password from hPanel → Emails → Configuration settings. */
|
|
28
|
+
pass: string;
|
|
29
|
+
/**
|
|
30
|
+
* Submission port.
|
|
31
|
+
* - `465` — SSL/TLS on connect (default)
|
|
32
|
+
* - `587` — STARTTLS
|
|
33
|
+
*/
|
|
34
|
+
port?: 465 | 587;
|
|
35
|
+
/** Enable the SMTP connection pool. Default: `false`. */
|
|
36
|
+
pool?: boolean;
|
|
37
|
+
/** Max simultaneous SMTP connections when `pool` is true. Default: `5`. */
|
|
38
|
+
maxConnections?: number;
|
|
39
|
+
}
|
|
40
|
+
/** A mailbox the API token can manage, as returned by {@link HostingerTransport.listMailboxes}. */
|
|
41
|
+
export interface HostingerMailbox {
|
|
42
|
+
/** Mailbox resource ID — pass it as {@link HostingerConfig.mailbox}. */
|
|
43
|
+
resourceId: string;
|
|
44
|
+
/** Email address of the mailbox. */
|
|
45
|
+
address: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reference to a source message by IMAP UID within a folder.
|
|
49
|
+
* Used by {@link HostingerTransport.sendReply} and {@link HostingerTransport.sendForward}.
|
|
50
|
+
*/
|
|
51
|
+
export interface HostingerMessageRef {
|
|
52
|
+
/** Folder containing the source message (e.g. `"INBOX"`). */
|
|
53
|
+
folder: string;
|
|
54
|
+
/** IMAP UID of the source message. */
|
|
55
|
+
uid: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a ready {@link SMTPConfig} for Hostinger Email.
|
|
59
|
+
*
|
|
60
|
+
* Defaults to port `465` with `secure: true`. Hostinger supports `465` and
|
|
61
|
+
* `587` only — not `2525`.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { createSMTPMailer } from "sently/smtp";
|
|
66
|
+
* import { hostingerSmtpConfig } from "sently/transports/hostinger";
|
|
67
|
+
*
|
|
68
|
+
* const mailer = await createSMTPMailer(
|
|
69
|
+
* hostingerSmtpConfig({
|
|
70
|
+
* user: "you@yourdomain.com",
|
|
71
|
+
* pass: process.env.HOSTINGER_SMTP_PASSWORD!,
|
|
72
|
+
* // port: 587, // STARTTLS instead of SSL
|
|
73
|
+
* }),
|
|
74
|
+
* );
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function hostingerSmtpConfig(options: HostingerSmtpOptions): SMTPConfig;
|
|
78
|
+
/** Error thrown when the Hostinger Mail API returns a non-success response. */
|
|
79
|
+
export declare class HostingerError extends SentlyError {
|
|
80
|
+
readonly statusCode: number;
|
|
81
|
+
readonly apiError: unknown;
|
|
82
|
+
/** Creates a Hostinger Mail API error with status code and response payload. */
|
|
83
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Hostinger Mail API transport.
|
|
87
|
+
*
|
|
88
|
+
* Sends through `POST /api/v1/mailboxes/{mailbox}/send`. There is no batch
|
|
89
|
+
* endpoint — {@link Mailer.sendBulk} falls back to individual sends.
|
|
90
|
+
*
|
|
91
|
+
* For SMTP relay, use {@link hostingerSmtpConfig} with `createSMTPMailer`
|
|
92
|
+
* instead of this class.
|
|
93
|
+
*/
|
|
94
|
+
export declare class HostingerTransport implements Transport {
|
|
95
|
+
readonly provider = "hostinger";
|
|
96
|
+
/** Hostinger Mail API token for Bearer authentication. */
|
|
97
|
+
private readonly token;
|
|
98
|
+
/** Resource ID of the managed mailbox to send from. */
|
|
99
|
+
private readonly mailbox;
|
|
100
|
+
/** Hostinger Mail API base URL. */
|
|
101
|
+
private readonly baseUrl;
|
|
102
|
+
/** Creates a Hostinger transport with the given API token and mailbox. */
|
|
103
|
+
constructor(config: HostingerConfig);
|
|
104
|
+
/** List the mailboxes this token can manage — use it to discover your mailbox resource ID. */
|
|
105
|
+
listMailboxes(): Promise<HostingerMailbox[]>;
|
|
106
|
+
/** Build the JSON body for a single Hostinger email. */
|
|
107
|
+
private buildEmailBody;
|
|
108
|
+
/** Map a 204 No Content success to a normalized SendResult. */
|
|
109
|
+
private toSendResult;
|
|
110
|
+
/** POST the send body and map the response. */
|
|
111
|
+
private postSend;
|
|
112
|
+
/** Sends an email via the Hostinger Mail API. */
|
|
113
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
114
|
+
/**
|
|
115
|
+
* Reply to a mailbox message.
|
|
116
|
+
* Copies Message-Id / References into In-Reply-To / References and flags the
|
|
117
|
+
* source `\Answered`. Mutually exclusive with {@link sendForward}.
|
|
118
|
+
*/
|
|
119
|
+
sendReply(options: MailOptions, inReplyTo: HostingerMessageRef): Promise<SendResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Forward a mailbox message.
|
|
122
|
+
* Copies Message-Id / References into In-Reply-To / References and flags the
|
|
123
|
+
* source `$forwarded`. Mutually exclusive with {@link sendReply}.
|
|
124
|
+
*/
|
|
125
|
+
sendForward(options: MailOptions, forwardOf: HostingerMessageRef): Promise<SendResult>;
|
|
126
|
+
/** Verifies the API token and that the configured mailbox is in its scope. */
|
|
127
|
+
verify(): Promise<VerifyResult>;
|
|
128
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{K as k}from"../chunk-1ke3nmyg.js";import{S as W,U as J}from"../chunk-8kpgbrba.js";import{da as Y}from"../chunk-hnzmn4s4.js";import{ja as Z,ka as $}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var U="smtp.hostinger.com",X=465,j=587,V="https://api.mail.hostinger.com";function G(q){let z=q.port??X;return{host:U,port:z,secure:z===X,auth:{user:q.user,pass:q.pass},...q.pool!==void 0?{pool:q.pool}:{},...q.maxConnections!==void 0?{maxConnections:q.maxConnections}:{}}}class Q extends Z{statusCode;apiError;constructor(q,z,F){super(q,$(z),{statusCode:z,provider:"hostinger",cause:F});this.statusCode=z;this.apiError=F;this.name="HostingerError"}}class v{provider="hostinger";token;mailbox;baseUrl;constructor(q){this.token=q.token,this.mailbox=q.mailbox,this.baseUrl=q.baseUrl??V}async listMailboxes(){let q=await fetch(`${this.baseUrl}/api/v1/me`,{headers:{Authorization:`Bearer ${this.token}`}}),z=await q.json().catch(()=>({}));if(!q.ok)throw new Q(z.error??`Hostinger API error (HTTP ${q.status})`,q.status,z);return z.data?.mailboxes??[]}async buildEmailBody(q,z){if(z?.inReplyTo&&z.forwardOf)throw new Q("inReplyTo and forwardOf are mutually exclusive",422,{code:"MUTUALLY_EXCLUSIVE",params:{inReplyTo:["conflicts with forwardOf"]}});let F=await k(q.attachments),K=W(q.from)[0];return{to:J(q.to),...K?.name?{displayName:K.name}:{},...q.cc?{cc:J(q.cc)}:{},...q.bcc?{bcc:J(q.bcc)}:{},subject:q.subject,...q.text?{text:q.text}:{},...q.html?{html:q.html}:{},...F.length>0?{attachments:F.map((D)=>({filename:D.filename,content:D.content instanceof Uint8Array||typeof D.content==="string"?Y(D.content).replace(/\r\n/g,""):"",...D.contentType?{contentType:D.contentType}:{},...D.contentId?{cid:D.contentId.replace(/^<|>$/g,"")}:{}}))}:{},...z?.inReplyTo?{inReplyTo:z.inReplyTo}:{},...z?.forwardOf?{forwardOf:z.forwardOf}:{}}}toSendResult(q){let z=W(q.from)[0];return{messageId:q.messageId??"",accepted:J(q.to),rejected:[],response:"Message sent and saved to the Sent folder",envelope:{from:z?.address??"",to:[...J(q.to),...q.cc?J(q.cc):[],...q.bcc?J(q.bcc):[]]}}}async postSend(q,z){let F=await this.buildEmailBody(q,z),K=await fetch(`${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,{method:"POST",headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:JSON.stringify(F)});if(K.ok)return this.toSendResult(q);let D=await K.json().catch(()=>({}));throw new Q(D.error??`Hostinger API error (HTTP ${K.status})`,K.status,D)}async send(q){return this.postSend(q)}async sendReply(q,z){return this.postSend(q,{inReplyTo:z})}async sendForward(q,z){return this.postSend(q,{forwardOf:z})}async verify(){try{let q=await this.listMailboxes(),z=q.find((F)=>F.resourceId===this.mailbox);if(!z)return{ok:!1,provider:"hostinger",message:`Mailbox "${this.mailbox}" is not in this token's scope`,raw:q};return{ok:!0,provider:"hostinger",message:`API token is valid — sending as ${z.address}`,raw:q}}catch(q){if(q instanceof Q)return{ok:!1,provider:"hostinger",message:q.message};return{ok:!1,provider:"hostinger",message:q instanceof Error?q.message:String(q)}}}}export{G as hostingerSmtpConfig,v as HostingerTransport,Q as HostingerError,j as HOSTINGER_SMTP_PORT_STARTTLS,X as HOSTINGER_SMTP_PORT_SSL,U as HOSTINGER_SMTP_HOST,V as HOSTINGER_API_BASE_URL};
|
|
2
|
+
|
|
3
|
+
//# debugId=5E9B71EE50CFF7E764756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/hostinger.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Hostinger email — Mail API transport (`api.mail.hostinger.com`) plus ready\n * SMTP config for `createSMTPMailer` (`smtp.hostinger.com`, ports 465 / 587).\n *\n * The Mail API sends from the managed mailbox itself, so `MailOptions.from`\n * only contributes the sender display name. Vendor extras (`listMailboxes`,\n * `sendReply`, `sendForward`) stay on this class — never on the channel sender.\n *\n * @example Mail API\n * ```ts\n * import { HostingerTransport } from \"sently/transports/hostinger\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const hostinger = new HostingerTransport({\n * token: process.env.HOSTINGER_API_TOKEN!,\n * mailbox: process.env.HOSTINGER_MAILBOX_ID!, // e.g. \"AC1a2b3c4d5e6f7g\"\n * });\n * const mailer = await createMailer({ transport: hostinger });\n *\n * await mailer.send({\n * from: \"you@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Hostinger</p>\",\n * });\n * ```\n *\n * @example SMTP\n * ```ts\n * import { createSMTPMailer } from \"sently/smtp\";\n * import { hostingerSmtpConfig } from \"sently/transports/hostinger\";\n *\n * const mailer = await createSMTPMailer(\n * hostingerSmtpConfig({\n * user: \"you@yourdomain.com\",\n * pass: process.env.HOSTINGER_SMTP_PASSWORD!,\n * }),\n * );\n * ```\n */\nimport { extractEmails, parseAddresses } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Hostinger SMTP hostname. */\nexport const HOSTINGER_SMTP_HOST = \"smtp.hostinger.com\";\n\n/** SSL/TLS-on-connect submission port (default for {@link hostingerSmtpConfig}). */\nexport const HOSTINGER_SMTP_PORT_SSL = 465;\n\n/** STARTTLS submission port. */\nexport const HOSTINGER_SMTP_PORT_STARTTLS = 587;\n\n/** Default Hostinger Mail API base URL. */\nexport const HOSTINGER_API_BASE_URL = \"https://api.mail.hostinger.com\";\n\n/** Hostinger Mail API configuration. */\nexport interface HostingerConfig {\n /** API token from hPanel → Emails → Agentic Mail → API access (shown once at creation). */\n token: string;\n /** Resource ID of the managed mailbox to send from (e.g. `\"AC1a2b3c4d5e6f7g\"`). */\n mailbox: string;\n /** API base URL. Default: {@link HOSTINGER_API_BASE_URL}. */\n baseUrl?: string;\n}\n\n/**\n * Ready SMTP options for Hostinger Email.\n * Pass the result of {@link hostingerSmtpConfig} to `createSMTPMailer`.\n */\nexport interface HostingerSmtpOptions {\n /** Full mailbox address — this is the SMTP username. */\n user: string;\n /** Mailbox password from hPanel → Emails → Configuration settings. */\n pass: string;\n /**\n * Submission port.\n * - `465` — SSL/TLS on connect (default)\n * - `587` — STARTTLS\n */\n port?: 465 | 587;\n /** Enable the SMTP connection pool. Default: `false`. */\n pool?: boolean;\n /** Max simultaneous SMTP connections when `pool` is true. Default: `5`. */\n maxConnections?: number;\n}\n\n/** A mailbox the API token can manage, as returned by {@link HostingerTransport.listMailboxes}. */\nexport interface HostingerMailbox {\n /** Mailbox resource ID — pass it as {@link HostingerConfig.mailbox}. */\n resourceId: string;\n /** Email address of the mailbox. */\n address: string;\n}\n\n/**\n * Reference to a source message by IMAP UID within a folder.\n * Used by {@link HostingerTransport.sendReply} and {@link HostingerTransport.sendForward}.\n */\nexport interface HostingerMessageRef {\n /** Folder containing the source message (e.g. `\"INBOX\"`). */\n folder: string;\n /** IMAP UID of the source message. */\n uid: number;\n}\n\n/** Error envelope returned by the Hostinger Mail API on non-success responses. */\ninterface HostingerErrorEnvelope {\n error?: string;\n code?: string;\n params?: Record<string, unknown>;\n}\n\n/** Optional reply / forward threading fields for the Mail API send body. */\ninterface HostingerSendExtras {\n inReplyTo?: HostingerMessageRef;\n forwardOf?: HostingerMessageRef;\n}\n\n/**\n * Build a ready {@link SMTPConfig} for Hostinger Email.\n *\n * Defaults to port `465` with `secure: true`. Hostinger supports `465` and\n * `587` only — not `2525`.\n *\n * @example\n * ```ts\n * import { createSMTPMailer } from \"sently/smtp\";\n * import { hostingerSmtpConfig } from \"sently/transports/hostinger\";\n *\n * const mailer = await createSMTPMailer(\n * hostingerSmtpConfig({\n * user: \"you@yourdomain.com\",\n * pass: process.env.HOSTINGER_SMTP_PASSWORD!,\n * // port: 587, // STARTTLS instead of SSL\n * }),\n * );\n * ```\n */\nexport function hostingerSmtpConfig(options: HostingerSmtpOptions): SMTPConfig {\n const port = options.port ?? HOSTINGER_SMTP_PORT_SSL;\n return {\n host: HOSTINGER_SMTP_HOST,\n port,\n secure: port === HOSTINGER_SMTP_PORT_SSL,\n auth: { user: options.user, pass: options.pass },\n ...(options.pool !== undefined ? { pool: options.pool } : {}),\n ...(options.maxConnections !== undefined ? { maxConnections: options.maxConnections } : {}),\n };\n}\n\n/** Error thrown when the Hostinger Mail API returns a non-success response. */\nexport class HostingerError extends SentlyError {\n /** Creates a Hostinger Mail API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"hostinger\",\n cause: apiError,\n });\n this.name = \"HostingerError\";\n }\n}\n\n/**\n * Hostinger Mail API transport.\n *\n * Sends through `POST /api/v1/mailboxes/{mailbox}/send`. There is no batch\n * endpoint — {@link Mailer.sendBulk} falls back to individual sends.\n *\n * For SMTP relay, use {@link hostingerSmtpConfig} with `createSMTPMailer`\n * instead of this class.\n */\nexport class HostingerTransport implements Transport {\n readonly provider = \"hostinger\";\n\n /** Hostinger Mail API token for Bearer authentication. */\n private readonly token: string;\n /** Resource ID of the managed mailbox to send from. */\n private readonly mailbox: string;\n /** Hostinger Mail API base URL. */\n private readonly baseUrl: string;\n\n /** Creates a Hostinger transport with the given API token and mailbox. */\n constructor(config: HostingerConfig) {\n this.token = config.token;\n this.mailbox = config.mailbox;\n this.baseUrl = config.baseUrl ?? HOSTINGER_API_BASE_URL;\n }\n\n /** List the mailboxes this token can manage — use it to discover your mailbox resource ID. */\n async listMailboxes(): Promise<HostingerMailbox[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/me`, {\n headers: { Authorization: `Bearer ${this.token}` },\n });\n\n const payload = (await response.json().catch(() => ({}))) as {\n data?: { mailboxes?: HostingerMailbox[] };\n } & HostingerErrorEnvelope;\n\n if (!response.ok) {\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n return payload.data?.mailboxes ?? [];\n }\n\n /** Build the JSON body for a single Hostinger email. */\n private async buildEmailBody(\n options: MailOptions,\n extras?: HostingerSendExtras,\n ): Promise<Record<string, unknown>> {\n if (extras?.inReplyTo && extras.forwardOf) {\n throw new HostingerError(\"inReplyTo and forwardOf are mutually exclusive\", 422, {\n code: \"MUTUALLY_EXCLUSIVE\",\n params: { inReplyTo: [\"conflicts with forwardOf\"] },\n });\n }\n\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n return {\n to: extractEmails(options.to),\n ...(from?.name ? { displayName: from.name } : {}),\n ...(options.cc ? { cc: extractEmails(options.cc) } : {}),\n ...(options.bcc ? { bcc: extractEmails(options.bcc) } : {}),\n subject: options.subject,\n ...(options.text ? { text: options.text } : {}),\n ...(options.html ? { html: options.html } : {}),\n ...(attachments.length > 0\n ? {\n attachments: attachments.map((att) => ({\n filename: att.filename,\n content:\n att.content instanceof Uint8Array || typeof att.content === \"string\"\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : \"\",\n ...(att.contentType ? { contentType: att.contentType } : {}),\n ...(att.contentId ? { cid: att.contentId.replace(/^<|>$/g, \"\") } : {}),\n })),\n }\n : {}),\n ...(extras?.inReplyTo ? { inReplyTo: extras.inReplyTo } : {}),\n ...(extras?.forwardOf ? { forwardOf: extras.forwardOf } : {}),\n };\n }\n\n /** Map a 204 No Content success to a normalized SendResult. */\n private toSendResult(options: MailOptions): SendResult {\n const from = parseAddresses(options.from)[0];\n return {\n messageId: options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: \"Message sent and saved to the Sent folder\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** POST the send body and map the response. */\n private async postSend(options: MailOptions, extras?: HostingerSendExtras): Promise<SendResult> {\n const body = await this.buildEmailBody(options, extras);\n\n const response = await fetch(\n `${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n },\n );\n\n if (response.ok) {\n return this.toSendResult(options);\n }\n\n const payload = (await response.json().catch(() => ({}))) as HostingerErrorEnvelope;\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n /** Sends an email via the Hostinger Mail API. */\n async send(options: MailOptions): Promise<SendResult> {\n return this.postSend(options);\n }\n\n /**\n * Reply to a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `\\Answered`. Mutually exclusive with {@link sendForward}.\n */\n async sendReply(options: MailOptions, inReplyTo: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { inReplyTo });\n }\n\n /**\n * Forward a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `$forwarded`. Mutually exclusive with {@link sendReply}.\n */\n async sendForward(options: MailOptions, forwardOf: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { forwardOf });\n }\n\n /** Verifies the API token and that the configured mailbox is in its scope. */\n async verify(): Promise<VerifyResult> {\n try {\n const mailboxes = await this.listMailboxes();\n const configured = mailboxes.find((mailbox) => mailbox.resourceId === this.mailbox);\n\n if (!configured) {\n return {\n ok: false,\n provider: \"hostinger\",\n message: `Mailbox \"${this.mailbox}\" is not in this token's scope`,\n raw: mailboxes,\n };\n }\n\n return {\n ok: true,\n provider: \"hostinger\",\n message: `API token is valid — sending as ${configured.address}`,\n raw: mailboxes,\n };\n } catch (err) {\n if (err instanceof HostingerError) {\n return { ok: false, provider: \"hostinger\", message: err.message };\n }\n return {\n ok: false,\n provider: \"hostinger\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "5E9B71EE50CFF7E764756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|