@nanbon/fetan 0.0.0-stage → 0.1.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/README.md +105 -3
- package/dist/index.d.ts +241 -0
- package/dist/index.js +251 -0
- package/package.json +30 -6
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @nanbon/fetan
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK for the [Fetan](https://fetan.et) transactional email API. Zero dependencies, full TypeScript types, automatic retries.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nanbon/fetan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Mailer } from "@nanbon/fetan";
|
|
15
|
+
|
|
16
|
+
const mailer = new Mailer("em_live_xxxxxxxx", {
|
|
17
|
+
baseUrl: "https://mail.example.com", // your Fetan deployment
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { id, status } = await mailer.emails.send({
|
|
21
|
+
from: "hello@yourdomain.com",
|
|
22
|
+
to: "customer@example.com",
|
|
23
|
+
subject: "ሰላም from Fetan",
|
|
24
|
+
html: "<strong>It works!</strong>",
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Emails
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// Send with everything
|
|
32
|
+
await mailer.emails.send({
|
|
33
|
+
from: "billing@yourdomain.com",
|
|
34
|
+
to: ["a@example.com", "b@example.com"],
|
|
35
|
+
cc: "manager@example.com",
|
|
36
|
+
reply_to: "support@yourdomain.com",
|
|
37
|
+
subject: "Your invoice",
|
|
38
|
+
html: "<p>…</p>",
|
|
39
|
+
text: "…",
|
|
40
|
+
tags: { kind: "invoice" },
|
|
41
|
+
scheduled_at: "2026-10-15T09:00:00Z",
|
|
42
|
+
attachments: [{ filename: "invoice.pdf", content: "<base64>" }],
|
|
43
|
+
}, { idempotencyKey: "invoice-1234" }); // safe to retry
|
|
44
|
+
|
|
45
|
+
// Templates
|
|
46
|
+
await mailer.emails.send({
|
|
47
|
+
from: "hello@yourdomain.com",
|
|
48
|
+
to: "user@example.com",
|
|
49
|
+
subject: "Welcome",
|
|
50
|
+
template_id: "tpl_...",
|
|
51
|
+
variables: { name: "Abebe" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Read & manage
|
|
55
|
+
const email = await mailer.emails.get(id);
|
|
56
|
+
const { emails, total } = await mailer.emails.list({ status: "bounced", limit: 50 });
|
|
57
|
+
await mailer.emails.cancel(id); // only while still queued
|
|
58
|
+
|
|
59
|
+
// Batch (up to 100)
|
|
60
|
+
await mailer.batch.send([{ from, to, subject, html }, ...]);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Domains, keys, templates, webhooks
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const domain = await mailer.domains.create("yourdomain.com");
|
|
67
|
+
const checked = await mailer.domains.verify(domain.id); // live DNS check
|
|
68
|
+
|
|
69
|
+
const key = await mailer.apiKeys.create({ name: "prod", permission: "sending_only" });
|
|
70
|
+
console.log(key.key); // shown once — store it
|
|
71
|
+
|
|
72
|
+
const tpl = await mailer.templates.create({ name: "OTP", subject: "Your code", html: "<p>{{code}}</p>" });
|
|
73
|
+
|
|
74
|
+
const hook = await mailer.webhooks.create({
|
|
75
|
+
url: "https://yourapp.com/hooks/fetan",
|
|
76
|
+
events: ["email.delivered", "email.bounced"],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Errors
|
|
81
|
+
|
|
82
|
+
All failures throw `FetanError` with a machine-readable `code`:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { FetanError } from "@nanbon/fetan";
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await mailer.emails.send({ from: "a@unverified.com", to: "b@c.com", subject: "x", html: "x" });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof FetanError && err.code === "domain_not_verified") {
|
|
91
|
+
// err.message → "The domain unverified.com is not verified yet"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Codes: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `domain_not_verified`, `rate_limited`, `conflict`, `server_error`, `network_error`.
|
|
97
|
+
|
|
98
|
+
Network errors and 5xx responses are retried automatically (exponential backoff, 3 attempts by default — configurable via `maxRetries`).
|
|
99
|
+
|
|
100
|
+
## Build
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm install
|
|
104
|
+
npm run build # outputs dist/
|
|
105
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetan — official Node.js SDK for the Fetan transactional email API.
|
|
3
|
+
* Zero dependencies; uses the global fetch (Node 18+).
|
|
4
|
+
*/
|
|
5
|
+
export interface FetanOptions {
|
|
6
|
+
/** Base URL of your Fetan deployment, e.g. https://mail.example.com */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Max automatic retries for network errors and 5xx responses. Default 3. */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** Request timeout in milliseconds. Default 15000. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export type EmailStatus = "queued" | "sent" | "delivered" | "bounced" | "failed" | "complained" | "cancelled";
|
|
14
|
+
export interface Attachment {
|
|
15
|
+
filename: string;
|
|
16
|
+
/** Base64-encoded file content */
|
|
17
|
+
content: string;
|
|
18
|
+
content_type?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SendEmailParams {
|
|
21
|
+
from: string;
|
|
22
|
+
to: string | string[];
|
|
23
|
+
cc?: string | string[];
|
|
24
|
+
bcc?: string | string[];
|
|
25
|
+
reply_to?: string | string[];
|
|
26
|
+
subject: string;
|
|
27
|
+
html?: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
attachments?: Attachment[];
|
|
31
|
+
/** ISO 8601 datetime to schedule the email for later */
|
|
32
|
+
scheduled_at?: string;
|
|
33
|
+
tags?: Record<string, string>;
|
|
34
|
+
template_id?: string;
|
|
35
|
+
variables?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export interface Email {
|
|
38
|
+
id: string;
|
|
39
|
+
from: string;
|
|
40
|
+
to: string[];
|
|
41
|
+
cc: string[];
|
|
42
|
+
bcc: string[];
|
|
43
|
+
reply_to: string[];
|
|
44
|
+
subject: string;
|
|
45
|
+
html: string | null;
|
|
46
|
+
text: string | null;
|
|
47
|
+
status: EmailStatus;
|
|
48
|
+
scheduled_at: string | null;
|
|
49
|
+
tags: Record<string, string>;
|
|
50
|
+
template_id: string | null;
|
|
51
|
+
variables: Record<string, string>;
|
|
52
|
+
bounce_reason: string | null;
|
|
53
|
+
attempts: number;
|
|
54
|
+
last_error: string | null;
|
|
55
|
+
created_at: string;
|
|
56
|
+
updated_at: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SendEmailResponse {
|
|
59
|
+
id: string;
|
|
60
|
+
status: EmailStatus;
|
|
61
|
+
}
|
|
62
|
+
export interface BatchSendResponse {
|
|
63
|
+
ids: string[];
|
|
64
|
+
count: number;
|
|
65
|
+
}
|
|
66
|
+
export interface ListEmailsParams {
|
|
67
|
+
status?: EmailStatus;
|
|
68
|
+
domain?: string;
|
|
69
|
+
search?: string;
|
|
70
|
+
limit?: number;
|
|
71
|
+
offset?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface Domain {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
status: "pending" | "verified" | "failed";
|
|
77
|
+
records: {
|
|
78
|
+
type: string;
|
|
79
|
+
name: string;
|
|
80
|
+
value: string;
|
|
81
|
+
status: string;
|
|
82
|
+
}[];
|
|
83
|
+
created_at: string;
|
|
84
|
+
}
|
|
85
|
+
export interface ApiKey {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
prefix: string;
|
|
89
|
+
permission: "full_access" | "sending_only";
|
|
90
|
+
domain_id: string | null;
|
|
91
|
+
last_used_at: string | null;
|
|
92
|
+
created_at: string;
|
|
93
|
+
}
|
|
94
|
+
export interface Template {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
subject: string;
|
|
98
|
+
html: string;
|
|
99
|
+
variables: string[];
|
|
100
|
+
version: number;
|
|
101
|
+
created_at: string;
|
|
102
|
+
updated_at: string;
|
|
103
|
+
}
|
|
104
|
+
export interface Webhook {
|
|
105
|
+
id: string;
|
|
106
|
+
url: string;
|
|
107
|
+
events: string[];
|
|
108
|
+
active: boolean;
|
|
109
|
+
created_at: string;
|
|
110
|
+
}
|
|
111
|
+
export interface WebhookDelivery {
|
|
112
|
+
id: string;
|
|
113
|
+
event: string;
|
|
114
|
+
status: "success" | "failed" | "pending";
|
|
115
|
+
response_code: number | null;
|
|
116
|
+
attempted_at: string;
|
|
117
|
+
}
|
|
118
|
+
export type FetanErrorCode = "unauthorized" | "forbidden" | "not_found" | "validation_error" | "domain_not_verified" | "rate_limited" | "conflict" | "server_error" | "network_error";
|
|
119
|
+
export declare class FetanError extends Error {
|
|
120
|
+
readonly code: FetanErrorCode;
|
|
121
|
+
readonly status: number;
|
|
122
|
+
constructor(message: string, code: FetanErrorCode, status: number);
|
|
123
|
+
}
|
|
124
|
+
export declare class Mailer {
|
|
125
|
+
private readonly apiKey;
|
|
126
|
+
private readonly baseUrl;
|
|
127
|
+
private readonly maxRetries;
|
|
128
|
+
private readonly timeoutMs;
|
|
129
|
+
readonly emails: EmailsResource;
|
|
130
|
+
readonly batch: BatchResource;
|
|
131
|
+
readonly domains: DomainsResource;
|
|
132
|
+
readonly apiKeys: ApiKeysResource;
|
|
133
|
+
readonly templates: TemplatesResource;
|
|
134
|
+
readonly webhooks: WebhooksResource;
|
|
135
|
+
constructor(apiKey: string, options?: FetanOptions);
|
|
136
|
+
/** @internal */
|
|
137
|
+
request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
|
|
138
|
+
}
|
|
139
|
+
declare class EmailsResource {
|
|
140
|
+
private readonly m;
|
|
141
|
+
constructor(m: Mailer);
|
|
142
|
+
/** Send an email. Returns 202-style { id, status: "queued" }. */
|
|
143
|
+
send(params: SendEmailParams, opts?: {
|
|
144
|
+
idempotencyKey?: string;
|
|
145
|
+
}): Promise<SendEmailResponse>;
|
|
146
|
+
get(id: string): Promise<Email>;
|
|
147
|
+
list(params?: ListEmailsParams): Promise<{
|
|
148
|
+
emails: Email[];
|
|
149
|
+
total: number;
|
|
150
|
+
limit: number;
|
|
151
|
+
offset: number;
|
|
152
|
+
}>;
|
|
153
|
+
cancel(id: string): Promise<{
|
|
154
|
+
id: string;
|
|
155
|
+
status: "cancelled";
|
|
156
|
+
}>;
|
|
157
|
+
}
|
|
158
|
+
declare class BatchResource {
|
|
159
|
+
private readonly m;
|
|
160
|
+
constructor(m: Mailer);
|
|
161
|
+
/** Send up to 100 emails in one request. */
|
|
162
|
+
send(emails: SendEmailParams[]): Promise<BatchSendResponse>;
|
|
163
|
+
}
|
|
164
|
+
declare class DomainsResource {
|
|
165
|
+
private readonly m;
|
|
166
|
+
constructor(m: Mailer);
|
|
167
|
+
list(): Promise<{
|
|
168
|
+
domains: Domain[];
|
|
169
|
+
}>;
|
|
170
|
+
get(id: string): Promise<Domain>;
|
|
171
|
+
create(name: string): Promise<Domain>;
|
|
172
|
+
/** Trigger a DNS verification check; returns the domain with per-record status. */
|
|
173
|
+
verify(id: string): Promise<Domain>;
|
|
174
|
+
delete(id: string): Promise<{
|
|
175
|
+
deleted: true;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
declare class ApiKeysResource {
|
|
179
|
+
private readonly m;
|
|
180
|
+
constructor(m: Mailer);
|
|
181
|
+
list(): Promise<{
|
|
182
|
+
api_keys: ApiKey[];
|
|
183
|
+
}>;
|
|
184
|
+
/** Creates a key. The full key is returned once, in `key` — store it immediately. */
|
|
185
|
+
create(params: {
|
|
186
|
+
name: string;
|
|
187
|
+
permission?: "full_access" | "sending_only";
|
|
188
|
+
domain_id?: string;
|
|
189
|
+
}): Promise<ApiKey & {
|
|
190
|
+
key: string;
|
|
191
|
+
}>;
|
|
192
|
+
revoke(id: string): Promise<{
|
|
193
|
+
revoked: true;
|
|
194
|
+
}>;
|
|
195
|
+
}
|
|
196
|
+
declare class TemplatesResource {
|
|
197
|
+
private readonly m;
|
|
198
|
+
constructor(m: Mailer);
|
|
199
|
+
list(): Promise<{
|
|
200
|
+
templates: Template[];
|
|
201
|
+
}>;
|
|
202
|
+
get(id: string): Promise<Template>;
|
|
203
|
+
create(params: {
|
|
204
|
+
name: string;
|
|
205
|
+
subject: string;
|
|
206
|
+
html: string;
|
|
207
|
+
}): Promise<Template>;
|
|
208
|
+
update(id: string, params: Partial<{
|
|
209
|
+
name: string;
|
|
210
|
+
subject: string;
|
|
211
|
+
html: string;
|
|
212
|
+
}>): Promise<Template>;
|
|
213
|
+
delete(id: string): Promise<{
|
|
214
|
+
deleted: true;
|
|
215
|
+
}>;
|
|
216
|
+
}
|
|
217
|
+
declare class WebhooksResource {
|
|
218
|
+
private readonly m;
|
|
219
|
+
constructor(m: Mailer);
|
|
220
|
+
list(): Promise<{
|
|
221
|
+
webhooks: Webhook[];
|
|
222
|
+
}>;
|
|
223
|
+
create(params: {
|
|
224
|
+
url: string;
|
|
225
|
+
events: string[];
|
|
226
|
+
}): Promise<Webhook & {
|
|
227
|
+
secret: string;
|
|
228
|
+
}>;
|
|
229
|
+
update(id: string, params: Partial<{
|
|
230
|
+
url: string;
|
|
231
|
+
events: string[];
|
|
232
|
+
active: boolean;
|
|
233
|
+
}>): Promise<Webhook>;
|
|
234
|
+
delete(id: string): Promise<{
|
|
235
|
+
deleted: true;
|
|
236
|
+
}>;
|
|
237
|
+
deliveries(id: string): Promise<{
|
|
238
|
+
deliveries: WebhookDelivery[];
|
|
239
|
+
}>;
|
|
240
|
+
}
|
|
241
|
+
export default Mailer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetan — official Node.js SDK for the Fetan transactional email API.
|
|
3
|
+
* Zero dependencies; uses the global fetch (Node 18+).
|
|
4
|
+
*/
|
|
5
|
+
export class FetanError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, code, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "FetanError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function codeFor(status, message) {
|
|
16
|
+
if (status === 401)
|
|
17
|
+
return "unauthorized";
|
|
18
|
+
if (status === 403)
|
|
19
|
+
return "forbidden";
|
|
20
|
+
if (status === 404)
|
|
21
|
+
return "not_found";
|
|
22
|
+
if (status === 409)
|
|
23
|
+
return "conflict";
|
|
24
|
+
if (status === 429)
|
|
25
|
+
return "rate_limited";
|
|
26
|
+
if (status === 400) {
|
|
27
|
+
return /not verified|not added/i.test(message) ? "domain_not_verified" : "validation_error";
|
|
28
|
+
}
|
|
29
|
+
return "server_error";
|
|
30
|
+
}
|
|
31
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
export class Mailer {
|
|
33
|
+
apiKey;
|
|
34
|
+
baseUrl;
|
|
35
|
+
maxRetries;
|
|
36
|
+
timeoutMs;
|
|
37
|
+
emails;
|
|
38
|
+
batch;
|
|
39
|
+
domains;
|
|
40
|
+
apiKeys;
|
|
41
|
+
templates;
|
|
42
|
+
webhooks;
|
|
43
|
+
constructor(apiKey, options = {}) {
|
|
44
|
+
if (!apiKey)
|
|
45
|
+
throw new FetanError("An API key is required: new Mailer('em_live_...')", "validation_error", 0);
|
|
46
|
+
this.apiKey = apiKey;
|
|
47
|
+
this.baseUrl = (options.baseUrl ?? "https://api.fetan.et").replace(/\/+$/, "");
|
|
48
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
49
|
+
this.timeoutMs = options.timeoutMs ?? 15000;
|
|
50
|
+
this.emails = new EmailsResource(this);
|
|
51
|
+
this.batch = new BatchResource(this);
|
|
52
|
+
this.domains = new DomainsResource(this);
|
|
53
|
+
this.apiKeys = new ApiKeysResource(this);
|
|
54
|
+
this.templates = new TemplatesResource(this);
|
|
55
|
+
this.webhooks = new WebhooksResource(this);
|
|
56
|
+
}
|
|
57
|
+
/** @internal */
|
|
58
|
+
async request(method, path, body, extraHeaders = {}) {
|
|
59
|
+
const base = this.baseUrl.replace(/\/+$/, "");
|
|
60
|
+
let url;
|
|
61
|
+
if (base.endsWith("/api/v1") || base.endsWith("/api/public/v1")) {
|
|
62
|
+
url = `${base}${path}`;
|
|
63
|
+
}
|
|
64
|
+
else if (base.endsWith("/api")) {
|
|
65
|
+
url = `${base}/v1${path}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
url = `${base}/api/v1${path}`;
|
|
69
|
+
}
|
|
70
|
+
let lastError = null;
|
|
71
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method,
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
...extraHeaders,
|
|
82
|
+
},
|
|
83
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
let data = null;
|
|
88
|
+
try {
|
|
89
|
+
data = text ? JSON.parse(text) : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
data = null;
|
|
93
|
+
}
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const rawError = data?.error;
|
|
96
|
+
let message = `Request failed with status ${res.status}`;
|
|
97
|
+
if (typeof rawError === "string") {
|
|
98
|
+
message = rawError;
|
|
99
|
+
}
|
|
100
|
+
else if (typeof rawError?.message === "string") {
|
|
101
|
+
message = rawError.message;
|
|
102
|
+
}
|
|
103
|
+
else if (typeof data?.message === "string") {
|
|
104
|
+
message = data.message;
|
|
105
|
+
}
|
|
106
|
+
// Retry only on server errors; client errors fail immediately.
|
|
107
|
+
if (res.status >= 500 && attempt < this.maxRetries) {
|
|
108
|
+
lastError = new FetanError(message, "server_error", res.status);
|
|
109
|
+
await sleep(2 ** attempt * 300);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw new FetanError(message, codeFor(res.status, message), res.status);
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
if (err instanceof FetanError)
|
|
119
|
+
throw err;
|
|
120
|
+
lastError = new FetanError(err instanceof Error ? err.message : "Network error", "network_error", 0);
|
|
121
|
+
if (attempt < this.maxRetries) {
|
|
122
|
+
await sleep(2 ** attempt * 300);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw lastError ?? new FetanError("Request failed", "network_error", 0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
class EmailsResource {
|
|
131
|
+
m;
|
|
132
|
+
constructor(m) {
|
|
133
|
+
this.m = m;
|
|
134
|
+
}
|
|
135
|
+
/** Send an email. Returns 202-style { id, status: "queued" }. */
|
|
136
|
+
send(params, opts) {
|
|
137
|
+
return this.m.request("POST", "/emails", params, opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : {});
|
|
138
|
+
}
|
|
139
|
+
get(id) {
|
|
140
|
+
return this.m.request("GET", `/emails/${encodeURIComponent(id)}`);
|
|
141
|
+
}
|
|
142
|
+
list(params = {}) {
|
|
143
|
+
const qs = new URLSearchParams();
|
|
144
|
+
if (params.status)
|
|
145
|
+
qs.set("status", params.status);
|
|
146
|
+
if (params.domain)
|
|
147
|
+
qs.set("domain", params.domain);
|
|
148
|
+
if (params.search)
|
|
149
|
+
qs.set("search", params.search);
|
|
150
|
+
if (params.limit !== undefined)
|
|
151
|
+
qs.set("limit", String(params.limit));
|
|
152
|
+
if (params.offset !== undefined)
|
|
153
|
+
qs.set("offset", String(params.offset));
|
|
154
|
+
const suffix = qs.size ? `?${qs.toString()}` : "";
|
|
155
|
+
return this.m.request("GET", `/emails${suffix}`);
|
|
156
|
+
}
|
|
157
|
+
cancel(id) {
|
|
158
|
+
return this.m.request("POST", `/emails/${encodeURIComponent(id)}/cancel`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
class BatchResource {
|
|
162
|
+
m;
|
|
163
|
+
constructor(m) {
|
|
164
|
+
this.m = m;
|
|
165
|
+
}
|
|
166
|
+
/** Send up to 100 emails in one request. */
|
|
167
|
+
send(emails) {
|
|
168
|
+
return this.m.request("POST", "/emails/batch", { emails });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class DomainsResource {
|
|
172
|
+
m;
|
|
173
|
+
constructor(m) {
|
|
174
|
+
this.m = m;
|
|
175
|
+
}
|
|
176
|
+
list() {
|
|
177
|
+
return this.m.request("GET", "/domains");
|
|
178
|
+
}
|
|
179
|
+
get(id) {
|
|
180
|
+
return this.m.request("GET", `/domains/${encodeURIComponent(id)}`);
|
|
181
|
+
}
|
|
182
|
+
create(name) {
|
|
183
|
+
return this.m.request("POST", "/domains", { name });
|
|
184
|
+
}
|
|
185
|
+
/** Trigger a DNS verification check; returns the domain with per-record status. */
|
|
186
|
+
verify(id) {
|
|
187
|
+
return this.m.request("POST", `/domains/${encodeURIComponent(id)}/verify`);
|
|
188
|
+
}
|
|
189
|
+
delete(id) {
|
|
190
|
+
return this.m.request("DELETE", `/domains/${encodeURIComponent(id)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
class ApiKeysResource {
|
|
194
|
+
m;
|
|
195
|
+
constructor(m) {
|
|
196
|
+
this.m = m;
|
|
197
|
+
}
|
|
198
|
+
list() {
|
|
199
|
+
return this.m.request("GET", "/api-keys");
|
|
200
|
+
}
|
|
201
|
+
/** Creates a key. The full key is returned once, in `key` — store it immediately. */
|
|
202
|
+
create(params) {
|
|
203
|
+
return this.m.request("POST", "/api-keys", params);
|
|
204
|
+
}
|
|
205
|
+
revoke(id) {
|
|
206
|
+
return this.m.request("DELETE", `/api-keys/${encodeURIComponent(id)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
class TemplatesResource {
|
|
210
|
+
m;
|
|
211
|
+
constructor(m) {
|
|
212
|
+
this.m = m;
|
|
213
|
+
}
|
|
214
|
+
list() {
|
|
215
|
+
return this.m.request("GET", "/templates");
|
|
216
|
+
}
|
|
217
|
+
get(id) {
|
|
218
|
+
return this.m.request("GET", `/templates/${encodeURIComponent(id)}`);
|
|
219
|
+
}
|
|
220
|
+
create(params) {
|
|
221
|
+
return this.m.request("POST", "/templates", params);
|
|
222
|
+
}
|
|
223
|
+
update(id, params) {
|
|
224
|
+
return this.m.request("PATCH", `/templates/${encodeURIComponent(id)}`, params);
|
|
225
|
+
}
|
|
226
|
+
delete(id) {
|
|
227
|
+
return this.m.request("DELETE", `/templates/${encodeURIComponent(id)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
class WebhooksResource {
|
|
231
|
+
m;
|
|
232
|
+
constructor(m) {
|
|
233
|
+
this.m = m;
|
|
234
|
+
}
|
|
235
|
+
list() {
|
|
236
|
+
return this.m.request("GET", "/webhooks");
|
|
237
|
+
}
|
|
238
|
+
create(params) {
|
|
239
|
+
return this.m.request("POST", "/webhooks", params);
|
|
240
|
+
}
|
|
241
|
+
update(id, params) {
|
|
242
|
+
return this.m.request("PATCH", `/webhooks/${encodeURIComponent(id)}`, params);
|
|
243
|
+
}
|
|
244
|
+
delete(id) {
|
|
245
|
+
return this.m.request("DELETE", `/webhooks/${encodeURIComponent(id)}`);
|
|
246
|
+
}
|
|
247
|
+
deliveries(id) {
|
|
248
|
+
return this.m.request("GET", `/webhooks/${encodeURIComponent(id)}/deliveries`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export default Mailer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@nanbon/fetan",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@nanbon/fetan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Node.js SDK for the Fetan transactional email API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"engines": { "node": ">=18" },
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.5.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["email", "transactional", "smtp", "mailer", "fetan", "ethiopia"],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/nanbon/fetan-email"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://fetan.et",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|