sently 1.1.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/CHANGELOG.md +21 -1
- package/dist/transports/hostinger.d.ts +128 -0
- package/dist/transports/hostinger.js +3 -0
- package/dist/transports/hostinger.js.map +10 -0
- package/package.json +6 -1
- package/site/content/docs/get-started/support-matrix.mdx +1 -1
- package/site/content/docs/transports/hostinger.mdx +435 -0
- package/site/content/docs/transports/index.mdx +1 -0
- package/site/content/docs/transports/meta.json +1 -0
- package/site/content/docs/transports/taqnyat.mdx +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
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
|
|
4
24
|
|
|
5
25
|
## [1.1.1] — 2026-08-02
|
|
6
26
|
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sently",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Runtime-agnostic channel-delivery library for Node.js, Bun, Deno, and Cloudflare Workers. One sender shape, one error model, and one retry path across email, SMS, WhatsApp, and push — with pluggable provider transports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"ses",
|
|
85
85
|
"aws-ses",
|
|
86
86
|
"brevo",
|
|
87
|
+
"hostinger",
|
|
87
88
|
"twilio",
|
|
88
89
|
"dkim",
|
|
89
90
|
"oauth2",
|
|
@@ -205,6 +206,10 @@
|
|
|
205
206
|
"import": "./dist/transports/sndr.js",
|
|
206
207
|
"types": "./dist/transports/sndr.d.ts"
|
|
207
208
|
},
|
|
209
|
+
"./transports/hostinger": {
|
|
210
|
+
"import": "./dist/transports/hostinger.js",
|
|
211
|
+
"types": "./dist/transports/hostinger.d.ts"
|
|
212
|
+
},
|
|
208
213
|
"./transports/twilio-sms": {
|
|
209
214
|
"import": "./dist/transports/twilio-sms.js",
|
|
210
215
|
"types": "./dist/transports/twilio-sms.d.ts"
|
|
@@ -29,7 +29,7 @@ FCM uses the current Firebase HTTP API (service-account JWT, no Google SDK). Tha
|
|
|
29
29
|
|
|
30
30
|
| Channel | Available examples |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| Email | Mailgun, Brevo, MailerSend, Plunk, SparkPost, Mailtrap, Mailpit (dev), Inbucket (dev), Loops, SNDR, Taqnyat Mail, Cloudflare Email, … |
|
|
32
|
+
| Email | Mailgun, Brevo, MailerSend, Plunk, SparkPost, Mailtrap, Mailpit (dev), Inbucket (dev), Loops, SNDR, Hostinger, Taqnyat Mail, Cloudflare Email, … |
|
|
33
33
|
| SMS | Taqnyat SMS, Msegat |
|
|
34
34
|
| WhatsApp | Taqnyat WhatsApp |
|
|
35
35
|
| Decorators | `WeightedFallbackTransport` (advanced); preview / idempotency remain email-only |
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hostinger
|
|
3
|
+
description: Send email from a Hostinger mailbox through the Mail API or ready SMTP config.
|
|
4
|
+
icon: Truck
|
|
5
|
+
source: "src/transports/hostinger.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Hostinger Email gives you a branded mailbox. Wire it into sently two ways — the Mail API over HTTPS, or the SMTP relay with Hostinger defaults already filled in.
|
|
9
|
+
|
|
10
|
+
<Callout title="The one rule">
|
|
11
|
+
Use `createMailer` with `HostingerTransport` for the Mail API, or `createSMTPMailer` with `hostingerSmtpConfig` for SMTP.
|
|
12
|
+
Vendor extras (`listMailboxes`, `sendReply`, `sendForward`) stay on the transport instance — never on the channel sender.
|
|
13
|
+
</Callout>
|
|
14
|
+
|
|
15
|
+
| Path | Import | When to use |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Mail API | `sently/transports/hostinger` → `HostingerTransport` | Tokens, Agentic Mail, reply/forward threading |
|
|
18
|
+
| SMTP | `hostingerSmtpConfig` + `sently/smtp` | Classic relay, clients, apps that already speak SMTP |
|
|
19
|
+
|
|
20
|
+
Official references: [Hostinger API](https://developers.hostinger.com/), [Mail API](https://api.mail.hostinger.com/), [SMTP ports](https://www.hostinger.com/tutorials/smtp-port/).
|
|
21
|
+
|
|
22
|
+
## Mail API
|
|
23
|
+
|
|
24
|
+
| Option | Type | Default or requirement |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `token` | `string` | required — Agentic Mail API token (shown once) |
|
|
27
|
+
| `mailbox` | `string` | required — mailbox resource ID, e.g. `AC1a2b3c4d5e6f7g` |
|
|
28
|
+
| `baseUrl` | `string` | `https://api.mail.hostinger.com` |
|
|
29
|
+
|
|
30
|
+
The API sends **from the managed mailbox**. `from` only contributes the sender display name. A copy is saved to the Sent folder on every successful send (`204 No Content`).
|
|
31
|
+
|
|
32
|
+
### Setup
|
|
33
|
+
|
|
34
|
+
<Steps>
|
|
35
|
+
<Step title="Create an API token">
|
|
36
|
+
|
|
37
|
+
In hPanel open **Emails → your domain → Agentic Mail → API access**, create a token scoped to the mailbox, and copy it — it is shown only once.
|
|
38
|
+
|
|
39
|
+
</Step>
|
|
40
|
+
<Step title="Discover the mailbox resource ID">
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { HostingerTransport } from "sently/transports/hostinger";
|
|
44
|
+
|
|
45
|
+
const hostinger = new HostingerTransport({
|
|
46
|
+
token: process.env.HOSTINGER_API_TOKEN!,
|
|
47
|
+
mailbox: "AC_placeholder", // replaced after listMailboxes()
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const mailboxes = await hostinger.listMailboxes();
|
|
51
|
+
// [{ resourceId: "AC1a2b3c4d5e6f7g", address: "you@yourdomain.com" }]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
</Step>
|
|
55
|
+
<Step title="Create the mailer and send">
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createMailer } from "sently/mailer";
|
|
59
|
+
import { HostingerTransport } from "sently/transports/hostinger";
|
|
60
|
+
|
|
61
|
+
const hostinger = new HostingerTransport({
|
|
62
|
+
token: process.env.HOSTINGER_API_TOKEN!,
|
|
63
|
+
mailbox: process.env.HOSTINGER_MAILBOX_ID!,
|
|
64
|
+
});
|
|
65
|
+
const mailer = await createMailer({ transport: hostinger });
|
|
66
|
+
|
|
67
|
+
const result = await mailer.send({
|
|
68
|
+
from: "Acme <you@yourdomain.com>",
|
|
69
|
+
to: "person@example.com",
|
|
70
|
+
subject: "Hello",
|
|
71
|
+
text: "Sent through the Hostinger Mail API",
|
|
72
|
+
});
|
|
73
|
+
console.log(result.response); // Message sent and saved to the Sent folder
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
</Step>
|
|
77
|
+
</Steps>
|
|
78
|
+
|
|
79
|
+
### Features
|
|
80
|
+
|
|
81
|
+
Pick a branch. Channel send goes through `mailer`; extras stay on `hostinger`.
|
|
82
|
+
|
|
83
|
+
<Tabs items={["Send", "HTML", "Attachments", "CC / BCC", "Reply", "Forward", "Mailboxes", "Verify"]}>
|
|
84
|
+
<Tab value="Send">
|
|
85
|
+
|
|
86
|
+
Transactional email via the channel mailer.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
await mailer.send({
|
|
90
|
+
from: "you@yourdomain.com",
|
|
91
|
+
to: "person@example.com",
|
|
92
|
+
subject: "Order confirmed",
|
|
93
|
+
text: "Thanks for your order.",
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
At least one of `to`, `cc`, or `bcc` must be present. There is no batch endpoint — `sendBulk` sends one by one.
|
|
98
|
+
|
|
99
|
+
</Tab>
|
|
100
|
+
<Tab value="HTML">
|
|
101
|
+
|
|
102
|
+
Send HTML, plain text, or both. A display name on `from` becomes API `displayName`.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
await mailer.send({
|
|
106
|
+
from: "Acme Billing <billing@yourdomain.com>",
|
|
107
|
+
to: "person@example.com",
|
|
108
|
+
subject: "Invoice ready",
|
|
109
|
+
text: "Your invoice is ready.",
|
|
110
|
+
html: "<p>Your invoice is <strong>ready</strong>.</p>",
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`replyTo`, custom `headers`, and `priority` are not mapped by the Mail API — use SMTP if you need them.
|
|
115
|
+
|
|
116
|
+
</Tab>
|
|
117
|
+
<Tab value="Attachments">
|
|
118
|
+
|
|
119
|
+
Attachments are base64-encoded for you. Inline images use `contentId` → API `cid`.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
await mailer.send({
|
|
123
|
+
from: "you@yourdomain.com",
|
|
124
|
+
to: "person@example.com",
|
|
125
|
+
subject: "Report",
|
|
126
|
+
html: '<p>Logo: <img src="cid:logo" /></p>',
|
|
127
|
+
attachments: [
|
|
128
|
+
{
|
|
129
|
+
filename: "report.pdf",
|
|
130
|
+
content: pdfBytes,
|
|
131
|
+
contentType: "application/pdf",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
filename: "logo.png",
|
|
135
|
+
content: logoBytes,
|
|
136
|
+
contentType: "image/png",
|
|
137
|
+
contentId: "logo",
|
|
138
|
+
inline: true,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
</Tab>
|
|
145
|
+
<Tab value="CC / BCC">
|
|
146
|
+
|
|
147
|
+
Carbon-copy and blind carbon-copy map to API `cc` / `bcc` arrays.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await mailer.send({
|
|
151
|
+
from: "you@yourdomain.com",
|
|
152
|
+
to: "person@example.com",
|
|
153
|
+
cc: ["ops@example.com", "lead@example.com"],
|
|
154
|
+
bcc: "audit@example.com",
|
|
155
|
+
subject: "Weekly update",
|
|
156
|
+
text: "Status for the week.",
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
</Tab>
|
|
161
|
+
<Tab value="Reply">
|
|
162
|
+
|
|
163
|
+
Reply to a mailbox message by folder + IMAP UID. Flags the source `\Answered`.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await hostinger.sendReply(
|
|
167
|
+
{
|
|
168
|
+
from: "you@yourdomain.com",
|
|
169
|
+
to: "person@example.com",
|
|
170
|
+
subject: "Re: Support request",
|
|
171
|
+
text: "Thanks — we are looking into it.",
|
|
172
|
+
},
|
|
173
|
+
{ folder: "INBOX", uid: 42 },
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Mutually exclusive with **Forward**. Call this on the transport, not on `mailer`.
|
|
178
|
+
|
|
179
|
+
</Tab>
|
|
180
|
+
<Tab value="Forward">
|
|
181
|
+
|
|
182
|
+
Forward a mailbox message by folder + IMAP UID. Flags the source `$forwarded`.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await hostinger.sendForward(
|
|
186
|
+
{
|
|
187
|
+
from: "you@yourdomain.com",
|
|
188
|
+
to: "team@example.com",
|
|
189
|
+
subject: "Fwd: Support request",
|
|
190
|
+
text: "Passing this along.",
|
|
191
|
+
},
|
|
192
|
+
{ folder: "INBOX", uid: 42 },
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Mutually exclusive with **Reply**.
|
|
197
|
+
|
|
198
|
+
</Tab>
|
|
199
|
+
<Tab value="Mailboxes">
|
|
200
|
+
|
|
201
|
+
List every mailbox the token can manage — required to learn the `resourceId`.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const mailboxes = await hostinger.listMailboxes();
|
|
205
|
+
for (const box of mailboxes) {
|
|
206
|
+
console.log(box.resourceId, box.address);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Resource IDs look like `AC1a2b3c4d5e6f7g`. Pass the matching one as `mailbox` in the transport config.
|
|
211
|
+
|
|
212
|
+
</Tab>
|
|
213
|
+
<Tab value="Verify">
|
|
214
|
+
|
|
215
|
+
Check the token and that the configured `mailbox` is in its scope — without sending mail.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const check = await hostinger.verify();
|
|
219
|
+
// { ok: true, provider: "hostinger",
|
|
220
|
+
// message: "API token is valid — sending as you@yourdomain.com" }
|
|
221
|
+
|
|
222
|
+
const viaMailer = await mailer.verify(); // same check through the channel sender
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
</Tab>
|
|
226
|
+
</Tabs>
|
|
227
|
+
|
|
228
|
+
### Mail options mapping
|
|
229
|
+
|
|
230
|
+
| Mail option | Hostinger field | Notes |
|
|
231
|
+
| --- | --- | --- |
|
|
232
|
+
| `from` name | `displayName` | Address is the managed mailbox |
|
|
233
|
+
| `to` / `cc` / `bcc` | `to` / `cc` / `bcc` | Email arrays |
|
|
234
|
+
| `subject` | `subject` | |
|
|
235
|
+
| `text` / `html` | `text` / `html` | Either or both |
|
|
236
|
+
| `attachments` | `attachments` | Base64 `content`, optional `contentType` / `cid` |
|
|
237
|
+
| `messageId` | — | Kept on `SendResult` (API returns empty body) |
|
|
238
|
+
| `replyTo` / `headers` / `priority` | — | Not supported on the Mail API |
|
|
239
|
+
|
|
240
|
+
## SMTP
|
|
241
|
+
|
|
242
|
+
<LiveVerified>
|
|
243
|
+
SMTP send against Hostinger’s production relay (`smtp.hostinger.com`) succeeded previously with a real mailbox — SSL port 465 and STARTTLS port 587.
|
|
244
|
+
</LiveVerified>
|
|
245
|
+
|
|
246
|
+
Ready Hostinger relay settings — no host/port guesswork. Pass `hostingerSmtpConfig(...)` straight into `createSMTPMailer`.
|
|
247
|
+
|
|
248
|
+
| Setting | Value |
|
|
249
|
+
| --- | --- |
|
|
250
|
+
| Host | `smtp.hostinger.com` |
|
|
251
|
+
| Port `465` | SSL/TLS on connect — **default** |
|
|
252
|
+
| Port `587` | STARTTLS |
|
|
253
|
+
| Username | Full mailbox address |
|
|
254
|
+
| Password | Mailbox password from hPanel |
|
|
255
|
+
|
|
256
|
+
Hostinger supports ports **465** and **587** only — not `2525` ([SMTP ports guide](https://www.hostinger.com/tutorials/smtp-port/)).
|
|
257
|
+
|
|
258
|
+
### Setup
|
|
259
|
+
|
|
260
|
+
<Steps>
|
|
261
|
+
<Step title="Copy SMTP credentials from hPanel">
|
|
262
|
+
|
|
263
|
+
**Emails → your domain → Configuration settings → Manual Configuration** — take the outgoing server host, port, and mailbox password.
|
|
264
|
+
|
|
265
|
+
</Step>
|
|
266
|
+
<Step title="Create the SMTP mailer">
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
270
|
+
import { hostingerSmtpConfig } from "sently/transports/hostinger";
|
|
271
|
+
|
|
272
|
+
const mailer = await createSMTPMailer(
|
|
273
|
+
hostingerSmtpConfig({
|
|
274
|
+
user: "you@yourdomain.com",
|
|
275
|
+
pass: process.env.HOSTINGER_SMTP_PASSWORD!,
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
</Step>
|
|
281
|
+
<Step title="Send with the channel API">
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
await mailer.send({
|
|
285
|
+
from: "you@yourdomain.com",
|
|
286
|
+
to: "person@example.com",
|
|
287
|
+
subject: "Hello",
|
|
288
|
+
text: "Sent through Hostinger SMTP",
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
</Step>
|
|
293
|
+
</Steps>
|
|
294
|
+
|
|
295
|
+
### Features
|
|
296
|
+
|
|
297
|
+
<Tabs items={["SSL 465", "STARTTLS 587", "Pool", "Full MIME"]}>
|
|
298
|
+
<Tab value="SSL 465">
|
|
299
|
+
|
|
300
|
+
Default — implicit TLS on connect.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const mailer = await createSMTPMailer(
|
|
304
|
+
hostingerSmtpConfig({
|
|
305
|
+
user: "you@yourdomain.com",
|
|
306
|
+
pass: process.env.HOSTINGER_SMTP_PASSWORD!,
|
|
307
|
+
// port: 465, // default
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Exports: `HOSTINGER_SMTP_HOST`, `HOSTINGER_SMTP_PORT_SSL` (`465`).
|
|
313
|
+
|
|
314
|
+
</Tab>
|
|
315
|
+
<Tab value="STARTTLS 587">
|
|
316
|
+
|
|
317
|
+
Plain connect, then upgrade with STARTTLS.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const mailer = await createSMTPMailer(
|
|
321
|
+
hostingerSmtpConfig({
|
|
322
|
+
user: "you@yourdomain.com",
|
|
323
|
+
pass: process.env.HOSTINGER_SMTP_PASSWORD!,
|
|
324
|
+
port: 587,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`secure` is set to `false` automatically. Constant: `HOSTINGER_SMTP_PORT_STARTTLS`.
|
|
330
|
+
|
|
331
|
+
</Tab>
|
|
332
|
+
<Tab value="Pool">
|
|
333
|
+
|
|
334
|
+
Reuse SMTP connections under load.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const mailer = await createSMTPMailer(
|
|
338
|
+
hostingerSmtpConfig({
|
|
339
|
+
user: "you@yourdomain.com",
|
|
340
|
+
pass: process.env.HOSTINGER_SMTP_PASSWORD!,
|
|
341
|
+
pool: true,
|
|
342
|
+
maxConnections: 3,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
</Tab>
|
|
348
|
+
<Tab value="Full MIME">
|
|
349
|
+
|
|
350
|
+
SMTP carries the full MIME message — `replyTo`, custom headers, `priority`, DKIM, and attachments work as on any other SMTP relay.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
await mailer.send({
|
|
354
|
+
from: "Acme <you@yourdomain.com>",
|
|
355
|
+
to: "person@example.com",
|
|
356
|
+
replyTo: "support@yourdomain.com",
|
|
357
|
+
subject: "Hello",
|
|
358
|
+
html: "<p>Hi</p>",
|
|
359
|
+
headers: { "X-Campaign": "welcome" },
|
|
360
|
+
priority: "high",
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
See [SMTP](./smtp) for pooling, DKIM, and adapter details.
|
|
365
|
+
|
|
366
|
+
</Tab>
|
|
367
|
+
</Tabs>
|
|
368
|
+
|
|
369
|
+
### Config helper
|
|
370
|
+
|
|
371
|
+
| Option | Type | Default |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| `user` | `string` | required — full mailbox address |
|
|
374
|
+
| `pass` | `string` | required — mailbox password |
|
|
375
|
+
| `port` | `465 \| 587` | `465` |
|
|
376
|
+
| `pool` | `boolean` | unset |
|
|
377
|
+
| `maxConnections` | `number` | unset (SMTP default `5` when pooled) |
|
|
378
|
+
|
|
379
|
+
## Mail API vs SMTP
|
|
380
|
+
|
|
381
|
+
| Need | Prefer |
|
|
382
|
+
| --- | --- |
|
|
383
|
+
| Agentic Mail token / mailbox resource ID | Mail API |
|
|
384
|
+
| Reply / forward by IMAP UID | Mail API (`sendReply` / `sendForward`) |
|
|
385
|
+
| `replyTo`, custom headers, `priority`, DKIM | SMTP |
|
|
386
|
+
| Existing SMTP client / form stack | SMTP |
|
|
387
|
+
| Sent-folder copy via Hostinger’s API | Mail API (automatic) |
|
|
388
|
+
|
|
389
|
+
## Troubleshooting
|
|
390
|
+
|
|
391
|
+
<Accordions>
|
|
392
|
+
<Accordion title="401 — Missing or invalid credentials">
|
|
393
|
+
The Mail API token is wrong or revoked. Create a fresh token under Agentic Mail → API access; tokens are shown only once.
|
|
394
|
+
</Accordion>
|
|
395
|
+
<Accordion title="403 — Token is not authorized to manage the requested mailbox">
|
|
396
|
+
The `mailbox` resource ID is outside the token's scope. Open the **Mailboxes** branch (`listMailboxes`) or recreate the token with access to that mailbox.
|
|
397
|
+
</Accordion>
|
|
398
|
+
<Accordion title="422 — Request payload failed validation">
|
|
399
|
+
At least one of `to`, `cc`, or `bcc` must be present. The error's `params` map names the fields that failed. Reply and Forward are mutually exclusive.
|
|
400
|
+
</Accordion>
|
|
401
|
+
<Accordion title="502 — Upstream service unavailable">
|
|
402
|
+
Hostinger’s upstream mail service returned an unexpected response. Retry with a [Retry](/docs/decorators/retry) decorator, or fall back to SMTP.
|
|
403
|
+
</Accordion>
|
|
404
|
+
<Accordion title="SMTP auth fails">
|
|
405
|
+
Username must be the **full** mailbox address. Copy the password from hPanel → Configuration settings → Manual Configuration. Use port `465` (`secure: true`) or `587` only.
|
|
406
|
+
</Accordion>
|
|
407
|
+
<Accordion title="Should I call the provider SDK?">
|
|
408
|
+
No. Use the matching sently channel sender; open a feature branch above for vendor extras on the transport.
|
|
409
|
+
</Accordion>
|
|
410
|
+
</Accordions>
|
|
411
|
+
|
|
412
|
+
## Contact & resources
|
|
413
|
+
|
|
414
|
+
| Resource | Link |
|
|
415
|
+
| --- | --- |
|
|
416
|
+
| Developers portal | [developers.hostinger.com](https://developers.hostinger.com/) |
|
|
417
|
+
| Mail API reference | [api.mail.hostinger.com](https://api.mail.hostinger.com/) |
|
|
418
|
+
| SMTP ports tutorial | [hostinger.com/tutorials/smtp-port](https://www.hostinger.com/tutorials/smtp-port/) |
|
|
419
|
+
| Business email product | [hostinger.com/business-email](https://www.hostinger.com/business-email) |
|
|
420
|
+
| hPanel | Emails → domain → Agentic Mail / Configuration settings |
|
|
421
|
+
|
|
422
|
+
## Learn more
|
|
423
|
+
|
|
424
|
+
- [Email channel](/docs/channels/email) — mailer options and send pipeline
|
|
425
|
+
- [SMTP](./smtp) — relay pooling, DKIM, adapters
|
|
426
|
+
- [Retry](/docs/decorators/retry) — wrap any transport on 429 / 5xx
|
|
427
|
+
- [Support matrix](/docs/get-started/support-matrix) — Supported vs Available
|
|
428
|
+
|
|
429
|
+
## Next
|
|
430
|
+
|
|
431
|
+
<Cards>
|
|
432
|
+
<Card title="Email channel" href="/docs/channels/email" />
|
|
433
|
+
<Card title="SMTP" href="/docs/transports/smtp" />
|
|
434
|
+
<Card title="Transports" href="/docs/transports" />
|
|
435
|
+
</Cards>
|
|
@@ -61,6 +61,7 @@ Every import below is an exported package subpath.
|
|
|
61
61
|
| [Loops](./loops) | Email | `sently/transports/loops` |
|
|
62
62
|
| [Cloudflare Email](./cloudflare-email) | Email | `sently/transports/cloudflare-email` |
|
|
63
63
|
| [SNDR](./sndr) | Email | `sently/transports/sndr` |
|
|
64
|
+
| [Hostinger](./hostinger) | Email | `sently/transports/hostinger` |
|
|
64
65
|
| [Twilio SMS](./twilio-sms) | SMS | `sently/transports/twilio-sms` |
|
|
65
66
|
| [Msegat](./msegat) | SMS | `sently/transports/msegat` |
|
|
66
67
|
| [Unifonic](./unifonic) | SMS | `sently/transports/unifonic` |
|
|
@@ -270,6 +270,10 @@ Keep `campaign` aligned with email `campaignName` when both channels share a cam
|
|
|
270
270
|
|
|
271
271
|
## Email
|
|
272
272
|
|
|
273
|
+
<LiveVerified>
|
|
274
|
+
Email send against Taqnyat’s production API succeeded in sently’s opt-in live suite (approved sender + real recipient).
|
|
275
|
+
</LiveVerified>
|
|
276
|
+
|
|
273
277
|
| Option | Type | Default or requirement |
|
|
274
278
|
| --- | --- | --- |
|
|
275
279
|
| `bearerToken` | `string` | required |
|