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.
Files changed (49) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +110 -1
  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/hostinger.d.ts +128 -0
  7. package/dist/transports/hostinger.js +3 -0
  8. package/dist/transports/hostinger.js.map +10 -0
  9. package/dist/transports/inbucket.d.ts +196 -0
  10. package/dist/transports/inbucket.js +3 -0
  11. package/dist/transports/inbucket.js.map +10 -0
  12. package/dist/transports/mailpit.d.ts +108 -8
  13. package/dist/transports/mailpit.js +2 -2
  14. package/dist/transports/mailpit.js.map +3 -3
  15. package/dist/transports/taqnyat-sms.d.ts +85 -0
  16. package/dist/transports/taqnyat-sms.js +2 -2
  17. package/dist/transports/taqnyat-sms.js.map +3 -3
  18. package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
  19. package/dist/transports/taqnyat-whatsapp.js +2 -2
  20. package/dist/transports/taqnyat-whatsapp.js.map +3 -3
  21. package/dist/transports/webpush.d.ts +19 -0
  22. package/dist/transports/webpush.js +2 -2
  23. package/dist/transports/webpush.js.map +3 -3
  24. package/dist/webhooks/sndr.js +2 -2
  25. package/dist/webhooks/sndr.js.map +3 -3
  26. package/package.json +12 -2
  27. package/site/content/docs/ai/llms-txt.mdx +2 -0
  28. package/site/content/docs/channels/email.mdx +2 -1
  29. package/site/content/docs/channels/push.mdx +3 -1
  30. package/site/content/docs/decorators/preview.mdx +2 -1
  31. package/site/content/docs/get-started/entrypoints.mdx +1 -1
  32. package/site/content/docs/get-started/support-matrix.mdx +2 -2
  33. package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
  34. package/site/content/docs/guides/webhooks.mdx +2 -0
  35. package/site/content/docs/guides/webpush-interop.mdx +9 -1
  36. package/site/content/docs/reference/exports.mdx +1 -1
  37. package/site/content/docs/reference/push-options.mdx +22 -5
  38. package/site/content/docs/transports/hostinger.mdx +435 -0
  39. package/site/content/docs/transports/inbucket.mdx +200 -0
  40. package/site/content/docs/transports/index.mdx +3 -3
  41. package/site/content/docs/transports/mailpit.mdx +114 -14
  42. package/site/content/docs/transports/meta.json +6 -4
  43. package/site/content/docs/transports/smtp.mdx +3 -2
  44. package/site/content/docs/transports/sndr.mdx +68 -5
  45. package/site/content/docs/transports/taqnyat.mdx +365 -0
  46. package/site/content/docs/transports/webpush.mdx +201 -17
  47. package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
  48. package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
  49. 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
- ## [Unreleased]
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/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-w.svg&theme=zinc&size=banner&mode=dark&font=geist"
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/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-k.svg&theme=zinc&size=banner&mode=light&font=geist"
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-000000.svg?logo=bun&size=sm&variant=outline" /></a>
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
- **1.0.0** — semver-stable channel contracts. Published on [npm](https://www.npmjs.com/package/sently) (provenance + Trusted Publishing) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
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 /** Notification title. */\n title: string;\n /** Notification body text. */\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/** 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\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 /** 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"
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": "AA4DO,SAAS,CAAgB,CAAC,EAAiD,CAChF,MAAO,UAAW,GAAW,OAAQ,EAA2B,QAAU,SAIrE,SAAS,CAAgB,CAAC,EAAiD,CAChF,MACE,iBAAkB,GAClB,OAAQ,EAA2B,cAAc,WAAa",
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
- /** Notification title. */
25
- title: string;
26
- /** Notification body text. */
27
- body: string;
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
+ }