@suphark/notify 0.10.0 → 0.11.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 +24 -0
- package/dist/{chunk-CWZP54ZN.js → chunk-SBQOHUW4.js} +431 -401
- package/dist/{chunk-NNMV7FZ3.js → chunk-WXINJXF4.js} +7 -1
- package/dist/index.d.ts +98 -57
- package/dist/index.js +54 -2
- package/dist/next.js +2 -2
- package/dist/{notify-D-PhiDzT.d.ts → notify-DA7BsQBD.d.ts} +27 -4
- package/dist/template.d.ts +6 -1
- package/dist/template.js +3 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
builtinFallback
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WXINJXF4.js";
|
|
4
4
|
|
|
5
5
|
// src/address.ts
|
|
6
6
|
var NAMED_RE = /^\s*(?:"?([^"<]*?)"?\s*)?<([^<>\s]+@[^<>\s]+)>\s*$/;
|
|
@@ -36,6 +36,346 @@ function formatAddress(a) {
|
|
|
36
36
|
return `${name} <${a.email}>`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var MailError = class extends Error {
|
|
41
|
+
kind;
|
|
42
|
+
provider;
|
|
43
|
+
status;
|
|
44
|
+
/** เวลาที่ปลายทางขอให้รอก่อนลองใหม่ (วินาที จาก Retry-After) — mailer ใช้เป็นเวลาพักแทน backoff */
|
|
45
|
+
retryAfterSec;
|
|
46
|
+
constructor(message, opts = {}) {
|
|
47
|
+
super(message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
48
|
+
this.name = "MailError";
|
|
49
|
+
this.kind = opts.kind ?? "unknown";
|
|
50
|
+
this.provider = opts.provider;
|
|
51
|
+
this.status = opts.status;
|
|
52
|
+
this.retryAfterSec = opts.retryAfterSec;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function retryAfterOf(res, body) {
|
|
56
|
+
if (res.status !== 429 && res.status !== 503) return void 0;
|
|
57
|
+
const fromHeader = Number(res.headers?.get?.("Retry-After")?.trim() ?? "");
|
|
58
|
+
if (Number.isFinite(fromHeader) && fromHeader > 0) return Math.ceil(fromHeader);
|
|
59
|
+
const fromBody = body?.error?.retryAfterSec;
|
|
60
|
+
return typeof fromBody === "number" && fromBody > 0 ? Math.ceil(fromBody) : void 0;
|
|
61
|
+
}
|
|
62
|
+
var AllProvidersFailedError = class extends MailError {
|
|
63
|
+
attempts;
|
|
64
|
+
constructor(attempts) {
|
|
65
|
+
const tried = attempts.filter((a) => !a.skipped);
|
|
66
|
+
const summary = attempts.length === 0 ? "no providers configured" : attempts.map(
|
|
67
|
+
(a) => a.skipped ? `${a.provider}: skipped (${a.skipped})` : `${a.provider}: ${a.error}`
|
|
68
|
+
).join(" | ");
|
|
69
|
+
super(`All mail providers failed (${tried.length}/${attempts.length} tried): ${summary}`);
|
|
70
|
+
this.name = "AllProvidersFailedError";
|
|
71
|
+
this.attempts = attempts;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var QUOTA_RE = /quota|limit exceeded|exceeded.*(limit|quota)|daily.*limit|monthly.*limit|sending limit|too many messages/i;
|
|
75
|
+
var RATE_RE = /rate.?limit|too many requests|throttl|slow down/i;
|
|
76
|
+
var AUTH_RE = /authenticat|credential|password|invalid.*(key|token)|api key|unauthori[sz]ed|forbidden|not verified|verify.*domain|domain.*verif/i;
|
|
77
|
+
var NET_CODE_RE = /^(ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|ESOCKET|EDNS|ECONNECTION)/i;
|
|
78
|
+
var NET_RE = /ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket|timed? ?out|network|fetch failed/i;
|
|
79
|
+
var INVALID_RE = /invalid.*(email|address|recipient)|no recipients|recipient.*(rejected|invalid)|malformed|missing.*(to|subject|from)/i;
|
|
80
|
+
function classifyError(err) {
|
|
81
|
+
if (err instanceof MailError)
|
|
82
|
+
return {
|
|
83
|
+
kind: err.kind,
|
|
84
|
+
message: err.message,
|
|
85
|
+
status: err.status,
|
|
86
|
+
retryAfterSec: err.retryAfterSec
|
|
87
|
+
};
|
|
88
|
+
const e = typeof err === "object" && err !== null ? err : {};
|
|
89
|
+
const message = String(e.message ?? e.response ?? err ?? "unknown error");
|
|
90
|
+
const code = typeof e.code === "string" ? e.code : void 0;
|
|
91
|
+
const status = [e.responseCode, e.status, e.statusCode].find(
|
|
92
|
+
(v) => typeof v === "number"
|
|
93
|
+
);
|
|
94
|
+
const text = `${code ?? ""} ${message} ${typeof e.response === "string" ? e.response : ""}`;
|
|
95
|
+
if (code && NET_CODE_RE.test(code)) return { kind: "network", message, status };
|
|
96
|
+
if (status === 552 || QUOTA_RE.test(text)) return { kind: "quota", message, status };
|
|
97
|
+
if (status === 429 || RATE_RE.test(text)) return { kind: "rate_limit", message, status };
|
|
98
|
+
if (status === 401 || status === 403 || status === 534 || status === 535 || AUTH_RE.test(text))
|
|
99
|
+
return { kind: "auth", message, status };
|
|
100
|
+
if (INVALID_RE.test(text)) return { kind: "invalid", message, status };
|
|
101
|
+
if (status !== void 0) {
|
|
102
|
+
if ([400, 422, 550, 553, 554].includes(status)) return { kind: "invalid", message, status };
|
|
103
|
+
if (status >= 420 && status < 460) return { kind: "server", message, status };
|
|
104
|
+
if (status >= 500) return { kind: "server", message, status };
|
|
105
|
+
}
|
|
106
|
+
if (NET_RE.test(text)) return { kind: "network", message, status };
|
|
107
|
+
return { kind: "unknown", message, status };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/providers/hub.ts
|
|
111
|
+
var DEFAULT_HUB_URL = "https://notify.suphark.com";
|
|
112
|
+
function createHubProvider(opts) {
|
|
113
|
+
const name = opts.name ?? "hub";
|
|
114
|
+
const url = `${(opts.url ?? DEFAULT_HUB_URL).replace(/\/$/, "")}/api/v1/send`;
|
|
115
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
type: "hub",
|
|
119
|
+
from: opts.from,
|
|
120
|
+
optionalFrom: true,
|
|
121
|
+
async send(msg) {
|
|
122
|
+
const body = {
|
|
123
|
+
from: msg.from,
|
|
124
|
+
to: msg.to,
|
|
125
|
+
cc: msg.cc.length ? msg.cc : void 0,
|
|
126
|
+
bcc: msg.bcc.length ? msg.bcc : void 0,
|
|
127
|
+
replyTo: msg.replyTo.length ? msg.replyTo : void 0,
|
|
128
|
+
subject: msg.subject || (msg.template ? void 0 : msg.subject),
|
|
129
|
+
html: msg.html,
|
|
130
|
+
text: msg.text,
|
|
131
|
+
attachments: msg.attachments.length ? msg.attachments : void 0,
|
|
132
|
+
headers: Object.keys(msg.headers).length ? msg.headers : void 0,
|
|
133
|
+
tags: Object.keys(msg.tags).length ? msg.tags : void 0,
|
|
134
|
+
prefer: msg.prefer,
|
|
135
|
+
deliverWithin: msg.deliverWithin,
|
|
136
|
+
template: msg.template,
|
|
137
|
+
data: msg.template ? msg.data ?? {} : void 0
|
|
138
|
+
};
|
|
139
|
+
const res = await doFetch(url, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
...msg.idempotencyKey ? { "Idempotency-Key": msg.idempotencyKey } : {}
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
// mailer หมดเวลา → abort คำขอนี้ทิ้ง (Pi Notify อาจส่งไปแล้ว — แอปที่มีตัวสำรองในเครื่องอาจได้เมลซ้ำ ยอมรับได้)
|
|
148
|
+
signal: msg.signal
|
|
149
|
+
});
|
|
150
|
+
const data = await res.json().catch(() => ({}));
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
const err = data.error;
|
|
153
|
+
const kind = err?.kind ?? (res.status === 401 || res.status === 403 ? "auth" : res.status === 429 ? "rate_limit" : res.status === 400 || res.status === 422 ? "invalid" : "server");
|
|
154
|
+
throw new MailError(`Pi Notify ${res.status}: ${err?.message ?? res.statusText}`, {
|
|
155
|
+
kind,
|
|
156
|
+
provider: name,
|
|
157
|
+
status: res.status,
|
|
158
|
+
cause: data,
|
|
159
|
+
retryAfterSec: retryAfterOf(res, data)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const ok = data;
|
|
163
|
+
if (ok.status === "queued") return { id: ok.deliveryId, queued: true };
|
|
164
|
+
return { id: ok.id };
|
|
165
|
+
},
|
|
166
|
+
async listProviders() {
|
|
167
|
+
const res = await doFetch(url.replace(/\/send$/, "/providers"), {
|
|
168
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok)
|
|
171
|
+
throw new MailError(`Pi Notify ${res.status}: ${res.statusText}`, {
|
|
172
|
+
kind: res.status === 401 ? "auth" : "server",
|
|
173
|
+
provider: name,
|
|
174
|
+
status: res.status
|
|
175
|
+
});
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
return data.providers.map((p) => ({ ...p, via: name }));
|
|
178
|
+
},
|
|
179
|
+
async verify() {
|
|
180
|
+
const res = await doFetch(url.replace(/\/send$/, "/status"), {
|
|
181
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
throw new MailError(`Pi Notify ${res.status}: ${res.statusText}`, {
|
|
185
|
+
kind: res.status === 401 ? "auth" : "server",
|
|
186
|
+
provider: name,
|
|
187
|
+
status: res.status
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/recipients.ts
|
|
195
|
+
var emailOf = (r) => {
|
|
196
|
+
const raw = typeof r === "string" ? r : r?.email;
|
|
197
|
+
const email = raw?.trim().toLowerCase();
|
|
198
|
+
return email ? email : null;
|
|
199
|
+
};
|
|
200
|
+
function toEmails(to) {
|
|
201
|
+
if (Array.isArray(to)) {
|
|
202
|
+
const emails = [...new Set(to.map(emailOf).filter((e) => e !== null))];
|
|
203
|
+
if (emails.length === 0) throw invalidRecipient("\u0E44\u0E21\u0E48\u0E21\u0E35\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E17\u0E35\u0E48\u0E21\u0E35\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E40\u0E25\u0E22");
|
|
204
|
+
return emails;
|
|
205
|
+
}
|
|
206
|
+
const email = emailOf(to);
|
|
207
|
+
if (!email) throw invalidRecipient("\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2D\u0E35\u0E40\u0E21\u0E25");
|
|
208
|
+
return email;
|
|
209
|
+
}
|
|
210
|
+
var invalidRecipient = (why) => new MailError(`notify(): ${why}`, { kind: "invalid", provider: "hub" });
|
|
211
|
+
function toEmail(to) {
|
|
212
|
+
const email = toEmails(to);
|
|
213
|
+
if (Array.isArray(email)) throw invalidRecipient("\u0E15\u0E49\u0E2D\u0E07\u0E40\u0E1B\u0E47\u0E19\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E04\u0E19\u0E40\u0E14\u0E35\u0E22\u0E27");
|
|
214
|
+
return email;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/notify.ts
|
|
218
|
+
var NOTIFY_TIMEOUT_MS = { notify: 65e3, recipient: 1e4 };
|
|
219
|
+
function resolve(opts) {
|
|
220
|
+
const env = opts.env ?? process.env;
|
|
221
|
+
const apiKey = opts.apiKey ?? env.PI_NOTIFY_KEY;
|
|
222
|
+
if (!apiKey) throw new MailError("notify(): PI_NOTIFY_KEY is required", { kind: "auth" });
|
|
223
|
+
const base = (opts.url ?? env.PI_NOTIFY_URL ?? DEFAULT_HUB_URL).replace(/\/$/, "");
|
|
224
|
+
return { apiKey, base, doFetch: opts.fetch ?? globalThis.fetch };
|
|
225
|
+
}
|
|
226
|
+
function hasNotifyKey(opts = {}) {
|
|
227
|
+
return Boolean(opts.apiKey ?? (opts.env ?? process.env).PI_NOTIFY_KEY);
|
|
228
|
+
}
|
|
229
|
+
async function hubRequest(opts, path, init) {
|
|
230
|
+
const { apiKey, base, doFetch } = resolve(opts);
|
|
231
|
+
const ms = opts.timeoutMs ?? init.timeoutMs;
|
|
232
|
+
const ac = new AbortController();
|
|
233
|
+
const timer = setTimeout(() => ac.abort(), ms);
|
|
234
|
+
const timedOut = () => new MailError(`Pi Notify timeout after ${ms}ms`, { kind: "network", provider: "hub" });
|
|
235
|
+
let res;
|
|
236
|
+
let data;
|
|
237
|
+
try {
|
|
238
|
+
res = await doFetch(`${base}${path}`, {
|
|
239
|
+
method: init.method ?? "GET",
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${apiKey}`,
|
|
242
|
+
...init.headers,
|
|
243
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
244
|
+
},
|
|
245
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
246
|
+
signal: ac.signal
|
|
247
|
+
});
|
|
248
|
+
data = await res.json().catch(() => ({}));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (ac.signal.aborted) throw timedOut();
|
|
251
|
+
throw err;
|
|
252
|
+
} finally {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
}
|
|
255
|
+
if (ac.signal.aborted) throw timedOut();
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
const err = data?.error;
|
|
258
|
+
const kind = err?.kind ?? (res.status === 401 || res.status === 403 ? "auth" : res.status === 429 ? "rate_limit" : res.status === 400 || res.status === 422 ? "invalid" : "server");
|
|
259
|
+
throw new MailError(`Pi Notify ${res.status}: ${err?.message ?? res.statusText}`, {
|
|
260
|
+
kind,
|
|
261
|
+
provider: "hub",
|
|
262
|
+
status: res.status,
|
|
263
|
+
cause: data,
|
|
264
|
+
retryAfterSec: retryAfterOf(res, data)
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return data;
|
|
268
|
+
}
|
|
269
|
+
function absoluteLink(link, opts = {}) {
|
|
270
|
+
if (!link.startsWith("/") || link.startsWith("//")) return link;
|
|
271
|
+
const env = opts.env ?? process.env;
|
|
272
|
+
const base = opts.appUrl ?? env.APP_URL ?? env.NEXT_PUBLIC_APP_URL;
|
|
273
|
+
if (!base)
|
|
274
|
+
throw new MailError(
|
|
275
|
+
`link "${link}" \u0E40\u0E1B\u0E47\u0E19\u0E25\u0E34\u0E07\u0E01\u0E4C relative \u2014 \u0E15\u0E31\u0E49\u0E07 APP_URL (\u0E2B\u0E23\u0E37\u0E2D NEXT_PUBLIC_APP_URL / opts.appUrl) \u0E2B\u0E23\u0E37\u0E2D\u0E2A\u0E48\u0E07 URL \u0E40\u0E15\u0E47\u0E21`,
|
|
276
|
+
{ kind: "invalid", provider: "hub" }
|
|
277
|
+
);
|
|
278
|
+
return new URL(link, base.endsWith("/") ? base : `${base}/`).toString();
|
|
279
|
+
}
|
|
280
|
+
async function postNotify(body, idempotencyKey, opts) {
|
|
281
|
+
if (typeof body.link === "string") body = { ...body, link: absoluteLink(body.link, opts) };
|
|
282
|
+
const data = await hubRequest(opts, "/api/v1/notify", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body,
|
|
285
|
+
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
286
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.notify
|
|
287
|
+
});
|
|
288
|
+
return { results: data.results, ...data.summary };
|
|
289
|
+
}
|
|
290
|
+
async function notify(input, opts = {}) {
|
|
291
|
+
const { idempotencyKey, ...body } = input;
|
|
292
|
+
return postNotify({ ...body, to: toEmails(input.to) }, idempotencyKey, opts);
|
|
293
|
+
}
|
|
294
|
+
async function notifyTopic(input, opts = {}) {
|
|
295
|
+
const { idempotencyKey, ...body } = input;
|
|
296
|
+
return postNotify(body, idempotencyKey, opts);
|
|
297
|
+
}
|
|
298
|
+
async function getRequestStatus(idempotencyKey, opts = {}) {
|
|
299
|
+
try {
|
|
300
|
+
return await hubRequest(
|
|
301
|
+
opts,
|
|
302
|
+
`/api/v1/requests/${encodeURIComponent(idempotencyKey)}`,
|
|
303
|
+
{ timeoutMs: NOTIFY_TIMEOUT_MS.recipient }
|
|
304
|
+
);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err instanceof MailError && err.status === 404) return null;
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function getRecipient(email, opts = {}) {
|
|
311
|
+
return hubRequest(
|
|
312
|
+
opts,
|
|
313
|
+
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}`,
|
|
314
|
+
{
|
|
315
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
async function createLink(email, channel = "telegram", opts = {}) {
|
|
320
|
+
const { piUserId, ...rest } = opts;
|
|
321
|
+
const link = await hubRequest(rest, `/api/v1/recipients/${encodeURIComponent(toEmail(email))}/link`, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
body: { channel, ...piUserId ? { piUserId } : {} },
|
|
324
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
325
|
+
});
|
|
326
|
+
const code = link.code ?? safeStartParam(link.url);
|
|
327
|
+
return { ...link, code };
|
|
328
|
+
}
|
|
329
|
+
function safeStartParam(url) {
|
|
330
|
+
try {
|
|
331
|
+
return new URL(url).searchParams.get("start") ?? "";
|
|
332
|
+
} catch {
|
|
333
|
+
return "";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function pauseChannel(email, channel, opts = {}) {
|
|
337
|
+
return hubRequest(
|
|
338
|
+
opts,
|
|
339
|
+
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}/pause`,
|
|
340
|
+
{ method: "POST", body: { channel }, timeoutMs: NOTIFY_TIMEOUT_MS.recipient }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
async function getChannelStatus(email, channel = "telegram", opts = {}) {
|
|
344
|
+
const recipient2 = await getRecipient(email, opts);
|
|
345
|
+
const found = recipient2.channels.find((c) => c.type === channel && c.enabled);
|
|
346
|
+
const paused = !found && recipient2.channels.some((c) => c.type === channel && !c.enabled);
|
|
347
|
+
return {
|
|
348
|
+
linked: Boolean(found),
|
|
349
|
+
...paused ? { paused: true } : {},
|
|
350
|
+
label: found?.label ?? null,
|
|
351
|
+
linkedAt: found?.linkedAt ?? null,
|
|
352
|
+
manageUrl: recipient2.linkUrl
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function getPushConfig(opts = {}) {
|
|
356
|
+
return hubRequest(opts, "/api/v1/push/config", { timeoutMs: NOTIFY_TIMEOUT_MS.recipient });
|
|
357
|
+
}
|
|
358
|
+
async function savePushSubscription(email, subscription, opts = {}) {
|
|
359
|
+
const { label, piUserId, ...rest } = opts;
|
|
360
|
+
return hubRequest(rest, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
body: { subscription, ...label ? { label } : {}, ...piUserId ? { piUserId } : {} },
|
|
363
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async function deletePushSubscription(email, endpoint, opts = {}) {
|
|
367
|
+
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
368
|
+
method: "DELETE",
|
|
369
|
+
body: { endpoint },
|
|
370
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async function listPushDevices(email, opts = {}) {
|
|
374
|
+
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
375
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
39
379
|
// src/providers/smtp.ts
|
|
40
380
|
import nodemailer from "nodemailer";
|
|
41
381
|
function createSmtpProvider(opts) {
|
|
@@ -218,77 +558,6 @@ function providersFromEnv(env = process.env) {
|
|
|
218
558
|
return listed.some((p) => p.type === "hub") ? listed : [...hub, ...listed];
|
|
219
559
|
}
|
|
220
560
|
|
|
221
|
-
// src/errors.ts
|
|
222
|
-
var MailError = class extends Error {
|
|
223
|
-
kind;
|
|
224
|
-
provider;
|
|
225
|
-
status;
|
|
226
|
-
/** เวลาที่ปลายทางขอให้รอก่อนลองใหม่ (วินาที จาก Retry-After) — mailer ใช้เป็นเวลาพักแทน backoff */
|
|
227
|
-
retryAfterSec;
|
|
228
|
-
constructor(message, opts = {}) {
|
|
229
|
-
super(message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
230
|
-
this.name = "MailError";
|
|
231
|
-
this.kind = opts.kind ?? "unknown";
|
|
232
|
-
this.provider = opts.provider;
|
|
233
|
-
this.status = opts.status;
|
|
234
|
-
this.retryAfterSec = opts.retryAfterSec;
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
function retryAfterOf(res, body) {
|
|
238
|
-
if (res.status !== 429 && res.status !== 503) return void 0;
|
|
239
|
-
const fromHeader = Number(res.headers?.get?.("Retry-After")?.trim() ?? "");
|
|
240
|
-
if (Number.isFinite(fromHeader) && fromHeader > 0) return Math.ceil(fromHeader);
|
|
241
|
-
const fromBody = body?.error?.retryAfterSec;
|
|
242
|
-
return typeof fromBody === "number" && fromBody > 0 ? Math.ceil(fromBody) : void 0;
|
|
243
|
-
}
|
|
244
|
-
var AllProvidersFailedError = class extends MailError {
|
|
245
|
-
attempts;
|
|
246
|
-
constructor(attempts) {
|
|
247
|
-
const tried = attempts.filter((a) => !a.skipped);
|
|
248
|
-
const summary = attempts.length === 0 ? "no providers configured" : attempts.map(
|
|
249
|
-
(a) => a.skipped ? `${a.provider}: skipped (${a.skipped})` : `${a.provider}: ${a.error}`
|
|
250
|
-
).join(" | ");
|
|
251
|
-
super(`All mail providers failed (${tried.length}/${attempts.length} tried): ${summary}`);
|
|
252
|
-
this.name = "AllProvidersFailedError";
|
|
253
|
-
this.attempts = attempts;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
var QUOTA_RE = /quota|limit exceeded|exceeded.*(limit|quota)|daily.*limit|monthly.*limit|sending limit|too many messages/i;
|
|
257
|
-
var RATE_RE = /rate.?limit|too many requests|throttl|slow down/i;
|
|
258
|
-
var AUTH_RE = /authenticat|credential|password|invalid.*(key|token)|api key|unauthori[sz]ed|forbidden|not verified|verify.*domain|domain.*verif/i;
|
|
259
|
-
var NET_CODE_RE = /^(ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|ESOCKET|EDNS|ECONNECTION)/i;
|
|
260
|
-
var NET_RE = /ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket|timed? ?out|network|fetch failed/i;
|
|
261
|
-
var INVALID_RE = /invalid.*(email|address|recipient)|no recipients|recipient.*(rejected|invalid)|malformed|missing.*(to|subject|from)/i;
|
|
262
|
-
function classifyError(err) {
|
|
263
|
-
if (err instanceof MailError)
|
|
264
|
-
return {
|
|
265
|
-
kind: err.kind,
|
|
266
|
-
message: err.message,
|
|
267
|
-
status: err.status,
|
|
268
|
-
retryAfterSec: err.retryAfterSec
|
|
269
|
-
};
|
|
270
|
-
const e = typeof err === "object" && err !== null ? err : {};
|
|
271
|
-
const message = String(e.message ?? e.response ?? err ?? "unknown error");
|
|
272
|
-
const code = typeof e.code === "string" ? e.code : void 0;
|
|
273
|
-
const status = [e.responseCode, e.status, e.statusCode].find(
|
|
274
|
-
(v) => typeof v === "number"
|
|
275
|
-
);
|
|
276
|
-
const text = `${code ?? ""} ${message} ${typeof e.response === "string" ? e.response : ""}`;
|
|
277
|
-
if (code && NET_CODE_RE.test(code)) return { kind: "network", message, status };
|
|
278
|
-
if (status === 552 || QUOTA_RE.test(text)) return { kind: "quota", message, status };
|
|
279
|
-
if (status === 429 || RATE_RE.test(text)) return { kind: "rate_limit", message, status };
|
|
280
|
-
if (status === 401 || status === 403 || status === 534 || status === 535 || AUTH_RE.test(text))
|
|
281
|
-
return { kind: "auth", message, status };
|
|
282
|
-
if (INVALID_RE.test(text)) return { kind: "invalid", message, status };
|
|
283
|
-
if (status !== void 0) {
|
|
284
|
-
if ([400, 422, 550, 553, 554].includes(status)) return { kind: "invalid", message, status };
|
|
285
|
-
if (status >= 420 && status < 460) return { kind: "server", message, status };
|
|
286
|
-
if (status >= 500) return { kind: "server", message, status };
|
|
287
|
-
}
|
|
288
|
-
if (NET_RE.test(text)) return { kind: "network", message, status };
|
|
289
|
-
return { kind: "unknown", message, status };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
561
|
// src/health.ts
|
|
293
562
|
var DEFAULT_COOLDOWN = {
|
|
294
563
|
network: 60,
|
|
@@ -405,150 +674,66 @@ var Health = class {
|
|
|
405
674
|
}
|
|
406
675
|
async status(p, quota) {
|
|
407
676
|
const cd = await this.cooldownOf(p.name);
|
|
408
|
-
const usage = await this.usage(p, quota);
|
|
409
|
-
const failures = Number(await this.store.get(`fail:${p.name}`) ?? 0);
|
|
410
|
-
return {
|
|
411
|
-
name: p.name,
|
|
412
|
-
type: p.type,
|
|
413
|
-
available: !cd && usage.every((u) => u.used < u.limit),
|
|
414
|
-
cooldownUntil: cd?.until,
|
|
415
|
-
cooldownKind: cd?.kind,
|
|
416
|
-
failures,
|
|
417
|
-
quota: usage
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
/** cooldown ที่ยังไม่หมด (null = ใช้ได้) */
|
|
421
|
-
async cooldownOf(name) {
|
|
422
|
-
const raw = await this.store.get(`cd:${name}`);
|
|
423
|
-
if (!raw) return null;
|
|
424
|
-
try {
|
|
425
|
-
const cd = JSON.parse(raw);
|
|
426
|
-
return cd.until > Date.now() ? cd : null;
|
|
427
|
-
} catch {
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async usage(p, quota) {
|
|
432
|
-
const out = [];
|
|
433
|
-
for (const w of WINDOWS) {
|
|
434
|
-
const limit = quota?.[w.key];
|
|
435
|
-
if (limit === void 0) continue;
|
|
436
|
-
const used = Number(
|
|
437
|
-
await this.store.get(`q:${p.name}:${w.window}:${windowId(w.window)}`) ?? 0
|
|
438
|
-
);
|
|
439
|
-
out.push({ window: w.window, used, limit });
|
|
440
|
-
}
|
|
441
|
-
return out;
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
// src/providers/console.ts
|
|
446
|
-
function createConsoleProvider(opts = {}) {
|
|
447
|
-
const name = opts.name ?? "console";
|
|
448
|
-
const log = opts.log ?? ((l) => console.log(l));
|
|
449
|
-
let seq = 0;
|
|
450
|
-
return {
|
|
451
|
-
name,
|
|
452
|
-
type: "console",
|
|
453
|
-
from: opts.from,
|
|
454
|
-
async send(msg) {
|
|
455
|
-
const id = `console-${Date.now()}-${++seq}`;
|
|
456
|
-
const lines = [
|
|
457
|
-
`[mailer:${name}] ${id}`,
|
|
458
|
-
` from: ${msg.from ? formatAddress(msg.from) : "(hub default)"}`,
|
|
459
|
-
` to: ${msg.to.map(formatAddress).join(", ")}`,
|
|
460
|
-
...msg.cc.length ? [` cc: ${msg.cc.map(formatAddress).join(", ")}`] : [],
|
|
461
|
-
...msg.bcc.length ? [` bcc: ${msg.bcc.map(formatAddress).join(", ")}`] : [],
|
|
462
|
-
` subject: ${msg.subject}`,
|
|
463
|
-
...msg.attachments.length ? [` files: ${msg.attachments.map((a) => a.filename).join(", ")}`] : [],
|
|
464
|
-
...opts.body ? [` ---`, msg.text ?? msg.html ?? "(empty)"] : []
|
|
465
|
-
];
|
|
466
|
-
log(lines.join("\n"));
|
|
467
|
-
return { id };
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// src/providers/hub.ts
|
|
473
|
-
var DEFAULT_HUB_URL = "https://notify.suphark.com";
|
|
474
|
-
function createHubProvider(opts) {
|
|
475
|
-
const name = opts.name ?? "hub";
|
|
476
|
-
const url = `${(opts.url ?? DEFAULT_HUB_URL).replace(/\/$/, "")}/api/v1/send`;
|
|
477
|
-
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
478
|
-
return {
|
|
479
|
-
name,
|
|
480
|
-
type: "hub",
|
|
481
|
-
from: opts.from,
|
|
482
|
-
optionalFrom: true,
|
|
483
|
-
async send(msg) {
|
|
484
|
-
const body = {
|
|
485
|
-
from: msg.from,
|
|
486
|
-
to: msg.to,
|
|
487
|
-
cc: msg.cc.length ? msg.cc : void 0,
|
|
488
|
-
bcc: msg.bcc.length ? msg.bcc : void 0,
|
|
489
|
-
replyTo: msg.replyTo.length ? msg.replyTo : void 0,
|
|
490
|
-
subject: msg.subject || (msg.template ? void 0 : msg.subject),
|
|
491
|
-
html: msg.html,
|
|
492
|
-
text: msg.text,
|
|
493
|
-
attachments: msg.attachments.length ? msg.attachments : void 0,
|
|
494
|
-
headers: Object.keys(msg.headers).length ? msg.headers : void 0,
|
|
495
|
-
tags: Object.keys(msg.tags).length ? msg.tags : void 0,
|
|
496
|
-
prefer: msg.prefer,
|
|
497
|
-
deliverWithin: msg.deliverWithin,
|
|
498
|
-
template: msg.template,
|
|
499
|
-
data: msg.template ? msg.data ?? {} : void 0
|
|
500
|
-
};
|
|
501
|
-
const res = await doFetch(url, {
|
|
502
|
-
method: "POST",
|
|
503
|
-
headers: {
|
|
504
|
-
Authorization: `Bearer ${opts.apiKey}`,
|
|
505
|
-
"Content-Type": "application/json",
|
|
506
|
-
...msg.idempotencyKey ? { "Idempotency-Key": msg.idempotencyKey } : {}
|
|
507
|
-
},
|
|
508
|
-
body: JSON.stringify(body),
|
|
509
|
-
// mailer หมดเวลา → abort คำขอนี้ทิ้ง (Pi Notify อาจส่งไปแล้ว — แอปที่มีตัวสำรองในเครื่องอาจได้เมลซ้ำ ยอมรับได้)
|
|
510
|
-
signal: msg.signal
|
|
511
|
-
});
|
|
512
|
-
const data = await res.json().catch(() => ({}));
|
|
513
|
-
if (!res.ok) {
|
|
514
|
-
const err = data.error;
|
|
515
|
-
const kind = err?.kind ?? (res.status === 401 || res.status === 403 ? "auth" : res.status === 429 ? "rate_limit" : res.status === 400 || res.status === 422 ? "invalid" : "server");
|
|
516
|
-
throw new MailError(`Pi Notify ${res.status}: ${err?.message ?? res.statusText}`, {
|
|
517
|
-
kind,
|
|
518
|
-
provider: name,
|
|
519
|
-
status: res.status,
|
|
520
|
-
cause: data,
|
|
521
|
-
retryAfterSec: retryAfterOf(res, data)
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
const ok = data;
|
|
525
|
-
if (ok.status === "queued") return { id: ok.deliveryId, queued: true };
|
|
526
|
-
return { id: ok.id };
|
|
527
|
-
},
|
|
528
|
-
async listProviders() {
|
|
529
|
-
const res = await doFetch(url.replace(/\/send$/, "/providers"), {
|
|
530
|
-
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
531
|
-
});
|
|
532
|
-
if (!res.ok)
|
|
533
|
-
throw new MailError(`Pi Notify ${res.status}: ${res.statusText}`, {
|
|
534
|
-
kind: res.status === 401 ? "auth" : "server",
|
|
535
|
-
provider: name,
|
|
536
|
-
status: res.status
|
|
537
|
-
});
|
|
538
|
-
const data = await res.json();
|
|
539
|
-
return data.providers.map((p) => ({ ...p, via: name }));
|
|
540
|
-
},
|
|
541
|
-
async verify() {
|
|
542
|
-
const res = await doFetch(url.replace(/\/send$/, "/status"), {
|
|
543
|
-
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
544
|
-
});
|
|
545
|
-
if (!res.ok) {
|
|
546
|
-
throw new MailError(`Pi Notify ${res.status}: ${res.statusText}`, {
|
|
547
|
-
kind: res.status === 401 ? "auth" : "server",
|
|
548
|
-
provider: name,
|
|
549
|
-
status: res.status
|
|
550
|
-
});
|
|
551
|
-
}
|
|
677
|
+
const usage = await this.usage(p, quota);
|
|
678
|
+
const failures = Number(await this.store.get(`fail:${p.name}`) ?? 0);
|
|
679
|
+
return {
|
|
680
|
+
name: p.name,
|
|
681
|
+
type: p.type,
|
|
682
|
+
available: !cd && usage.every((u) => u.used < u.limit),
|
|
683
|
+
cooldownUntil: cd?.until,
|
|
684
|
+
cooldownKind: cd?.kind,
|
|
685
|
+
failures,
|
|
686
|
+
quota: usage
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/** cooldown ที่ยังไม่หมด (null = ใช้ได้) */
|
|
690
|
+
async cooldownOf(name) {
|
|
691
|
+
const raw = await this.store.get(`cd:${name}`);
|
|
692
|
+
if (!raw) return null;
|
|
693
|
+
try {
|
|
694
|
+
const cd = JSON.parse(raw);
|
|
695
|
+
return cd.until > Date.now() ? cd : null;
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async usage(p, quota) {
|
|
701
|
+
const out = [];
|
|
702
|
+
for (const w of WINDOWS) {
|
|
703
|
+
const limit = quota?.[w.key];
|
|
704
|
+
if (limit === void 0) continue;
|
|
705
|
+
const used = Number(
|
|
706
|
+
await this.store.get(`q:${p.name}:${w.window}:${windowId(w.window)}`) ?? 0
|
|
707
|
+
);
|
|
708
|
+
out.push({ window: w.window, used, limit });
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/providers/console.ts
|
|
715
|
+
function createConsoleProvider(opts = {}) {
|
|
716
|
+
const name = opts.name ?? "console";
|
|
717
|
+
const log = opts.log ?? ((l) => console.log(l));
|
|
718
|
+
let seq = 0;
|
|
719
|
+
return {
|
|
720
|
+
name,
|
|
721
|
+
type: "console",
|
|
722
|
+
from: opts.from,
|
|
723
|
+
async send(msg) {
|
|
724
|
+
const id = `console-${Date.now()}-${++seq}`;
|
|
725
|
+
const lines = [
|
|
726
|
+
`[mailer:${name}] ${id}`,
|
|
727
|
+
` from: ${msg.from ? formatAddress(msg.from) : "(hub default)"}`,
|
|
728
|
+
` to: ${msg.to.map(formatAddress).join(", ")}`,
|
|
729
|
+
...msg.cc.length ? [` cc: ${msg.cc.map(formatAddress).join(", ")}`] : [],
|
|
730
|
+
...msg.bcc.length ? [` bcc: ${msg.bcc.map(formatAddress).join(", ")}`] : [],
|
|
731
|
+
` subject: ${msg.subject}`,
|
|
732
|
+
...msg.attachments.length ? [` files: ${msg.attachments.map((a) => a.filename).join(", ")}`] : [],
|
|
733
|
+
...opts.body ? [` ---`, msg.text ?? msg.html ?? "(empty)"] : []
|
|
734
|
+
];
|
|
735
|
+
log(lines.join("\n"));
|
|
736
|
+
return { id };
|
|
552
737
|
}
|
|
553
738
|
};
|
|
554
739
|
}
|
|
@@ -921,7 +1106,17 @@ function createMailer(options = {}) {
|
|
|
921
1106
|
}
|
|
922
1107
|
async function send(message) {
|
|
923
1108
|
if (!message?.template) return sendVia(message, "all");
|
|
924
|
-
const
|
|
1109
|
+
const fromName = (() => {
|
|
1110
|
+
try {
|
|
1111
|
+
return (message.from !== void 0 ? parseAddress(message.from) : defaultFrom)?.name;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
})();
|
|
1116
|
+
const fallback = message.fallback ?? builtinFallback(message.template, {
|
|
1117
|
+
...fromName ? { appName: fromName } : {},
|
|
1118
|
+
...message.data
|
|
1119
|
+
});
|
|
925
1120
|
const hasHub = entries.some((e) => e.provider.type === "hub");
|
|
926
1121
|
const hasLocal = entries.some((e) => e.provider.type !== "hub" && !e.disabled);
|
|
927
1122
|
const asFallback = (fb) => ({
|
|
@@ -946,7 +1141,8 @@ function createMailer(options = {}) {
|
|
|
946
1141
|
} catch (err) {
|
|
947
1142
|
const attempts = err instanceof AllProvidersFailedError ? err.attempts : [];
|
|
948
1143
|
const invalid = err instanceof MailError && err.kind === "invalid" && attempts.length === 0 || attempts.some((a) => a.kind === "invalid");
|
|
949
|
-
|
|
1144
|
+
const ambiguous = attempts.some((a) => a.error?.includes("send timed out after"));
|
|
1145
|
+
if (!fallback || !hasLocal || invalid || ambiguous) throw err;
|
|
950
1146
|
emit({ type: "fallback", template: message.template });
|
|
951
1147
|
const r = await sendVia(asFallback(fallback), "local");
|
|
952
1148
|
return { ...r, failedOver: true, fallback: true, attempts: [...attempts, ...r.attempts] };
|
|
@@ -1111,174 +1307,6 @@ function resetMailer() {
|
|
|
1111
1307
|
pending = void 0;
|
|
1112
1308
|
}
|
|
1113
1309
|
|
|
1114
|
-
// src/recipients.ts
|
|
1115
|
-
var emailOf = (r) => {
|
|
1116
|
-
const raw = typeof r === "string" ? r : r?.email;
|
|
1117
|
-
const email = raw?.trim().toLowerCase();
|
|
1118
|
-
return email ? email : null;
|
|
1119
|
-
};
|
|
1120
|
-
function toEmails(to) {
|
|
1121
|
-
if (Array.isArray(to)) {
|
|
1122
|
-
const emails = [...new Set(to.map(emailOf).filter((e) => e !== null))];
|
|
1123
|
-
if (emails.length === 0) throw invalidRecipient("\u0E44\u0E21\u0E48\u0E21\u0E35\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E17\u0E35\u0E48\u0E21\u0E35\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E40\u0E25\u0E22");
|
|
1124
|
-
return emails;
|
|
1125
|
-
}
|
|
1126
|
-
const email = emailOf(to);
|
|
1127
|
-
if (!email) throw invalidRecipient("\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2D\u0E35\u0E40\u0E21\u0E25");
|
|
1128
|
-
return email;
|
|
1129
|
-
}
|
|
1130
|
-
var invalidRecipient = (why) => new MailError(`notify(): ${why}`, { kind: "invalid", provider: "hub" });
|
|
1131
|
-
function toEmail(to) {
|
|
1132
|
-
const email = toEmails(to);
|
|
1133
|
-
if (Array.isArray(email)) throw invalidRecipient("\u0E15\u0E49\u0E2D\u0E07\u0E40\u0E1B\u0E47\u0E19\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E04\u0E19\u0E40\u0E14\u0E35\u0E22\u0E27");
|
|
1134
|
-
return email;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// src/notify.ts
|
|
1138
|
-
var NOTIFY_TIMEOUT_MS = { notify: 65e3, recipient: 1e4 };
|
|
1139
|
-
function resolve(opts) {
|
|
1140
|
-
const env = opts.env ?? process.env;
|
|
1141
|
-
const apiKey = opts.apiKey ?? env.PI_NOTIFY_KEY;
|
|
1142
|
-
if (!apiKey) throw new MailError("notify(): PI_NOTIFY_KEY is required", { kind: "auth" });
|
|
1143
|
-
const base = (opts.url ?? env.PI_NOTIFY_URL ?? DEFAULT_HUB_URL).replace(/\/$/, "");
|
|
1144
|
-
return { apiKey, base, doFetch: opts.fetch ?? globalThis.fetch };
|
|
1145
|
-
}
|
|
1146
|
-
function hasNotifyKey(opts = {}) {
|
|
1147
|
-
return Boolean(opts.apiKey ?? (opts.env ?? process.env).PI_NOTIFY_KEY);
|
|
1148
|
-
}
|
|
1149
|
-
async function hubRequest(opts, path, init) {
|
|
1150
|
-
const { apiKey, base, doFetch } = resolve(opts);
|
|
1151
|
-
const ms = opts.timeoutMs ?? init.timeoutMs;
|
|
1152
|
-
const ac = new AbortController();
|
|
1153
|
-
const timer = setTimeout(() => ac.abort(), ms);
|
|
1154
|
-
const timedOut = () => new MailError(`Pi Notify timeout after ${ms}ms`, { kind: "network", provider: "hub" });
|
|
1155
|
-
let res;
|
|
1156
|
-
let data;
|
|
1157
|
-
try {
|
|
1158
|
-
res = await doFetch(`${base}${path}`, {
|
|
1159
|
-
method: init.method ?? "GET",
|
|
1160
|
-
headers: {
|
|
1161
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1162
|
-
...init.headers,
|
|
1163
|
-
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1164
|
-
},
|
|
1165
|
-
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
1166
|
-
signal: ac.signal
|
|
1167
|
-
});
|
|
1168
|
-
data = await res.json().catch(() => ({}));
|
|
1169
|
-
} catch (err) {
|
|
1170
|
-
if (ac.signal.aborted) throw timedOut();
|
|
1171
|
-
throw err;
|
|
1172
|
-
} finally {
|
|
1173
|
-
clearTimeout(timer);
|
|
1174
|
-
}
|
|
1175
|
-
if (ac.signal.aborted) throw timedOut();
|
|
1176
|
-
if (!res.ok) {
|
|
1177
|
-
const err = data?.error;
|
|
1178
|
-
const kind = err?.kind ?? (res.status === 401 || res.status === 403 ? "auth" : res.status === 429 ? "rate_limit" : res.status === 400 || res.status === 422 ? "invalid" : "server");
|
|
1179
|
-
throw new MailError(`Pi Notify ${res.status}: ${err?.message ?? res.statusText}`, {
|
|
1180
|
-
kind,
|
|
1181
|
-
provider: "hub",
|
|
1182
|
-
status: res.status,
|
|
1183
|
-
cause: data,
|
|
1184
|
-
retryAfterSec: retryAfterOf(res, data)
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
return data;
|
|
1188
|
-
}
|
|
1189
|
-
async function postNotify(body, idempotencyKey, opts) {
|
|
1190
|
-
const data = await hubRequest(opts, "/api/v1/notify", {
|
|
1191
|
-
method: "POST",
|
|
1192
|
-
body,
|
|
1193
|
-
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
1194
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.notify
|
|
1195
|
-
});
|
|
1196
|
-
return { results: data.results, ...data.summary };
|
|
1197
|
-
}
|
|
1198
|
-
async function notify(input, opts = {}) {
|
|
1199
|
-
const { idempotencyKey, ...body } = input;
|
|
1200
|
-
return postNotify({ ...body, to: toEmails(input.to) }, idempotencyKey, opts);
|
|
1201
|
-
}
|
|
1202
|
-
async function notifyTopic(input, opts = {}) {
|
|
1203
|
-
const { idempotencyKey, ...body } = input;
|
|
1204
|
-
return postNotify(body, idempotencyKey, opts);
|
|
1205
|
-
}
|
|
1206
|
-
async function getRequestStatus(idempotencyKey, opts = {}) {
|
|
1207
|
-
try {
|
|
1208
|
-
return await hubRequest(
|
|
1209
|
-
opts,
|
|
1210
|
-
`/api/v1/requests/${encodeURIComponent(idempotencyKey)}`,
|
|
1211
|
-
{ timeoutMs: NOTIFY_TIMEOUT_MS.recipient }
|
|
1212
|
-
);
|
|
1213
|
-
} catch (err) {
|
|
1214
|
-
if (err instanceof MailError && err.status === 404) return null;
|
|
1215
|
-
throw err;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
async function getRecipient(email, opts = {}) {
|
|
1219
|
-
return hubRequest(
|
|
1220
|
-
opts,
|
|
1221
|
-
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}`,
|
|
1222
|
-
{
|
|
1223
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1224
|
-
}
|
|
1225
|
-
);
|
|
1226
|
-
}
|
|
1227
|
-
async function createLink(email, channel = "telegram", opts = {}) {
|
|
1228
|
-
const { piUserId, ...rest } = opts;
|
|
1229
|
-
const link = await hubRequest(
|
|
1230
|
-
rest,
|
|
1231
|
-
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}/link`,
|
|
1232
|
-
{
|
|
1233
|
-
method: "POST",
|
|
1234
|
-
body: { channel, ...piUserId ? { piUserId } : {} },
|
|
1235
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1236
|
-
}
|
|
1237
|
-
);
|
|
1238
|
-
const code = link.code ?? safeStartParam(link.url);
|
|
1239
|
-
return { ...link, code };
|
|
1240
|
-
}
|
|
1241
|
-
function safeStartParam(url) {
|
|
1242
|
-
try {
|
|
1243
|
-
return new URL(url).searchParams.get("start") ?? "";
|
|
1244
|
-
} catch {
|
|
1245
|
-
return "";
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
async function getChannelStatus(email, channel = "telegram", opts = {}) {
|
|
1249
|
-
const recipient2 = await getRecipient(email, opts);
|
|
1250
|
-
const found = recipient2.channels.find((c) => c.type === channel && c.enabled);
|
|
1251
|
-
return {
|
|
1252
|
-
linked: Boolean(found),
|
|
1253
|
-
label: found?.label ?? null,
|
|
1254
|
-
linkedAt: found?.linkedAt ?? null,
|
|
1255
|
-
manageUrl: recipient2.linkUrl
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
async function getPushConfig(opts = {}) {
|
|
1259
|
-
return hubRequest(opts, "/api/v1/push/config", { timeoutMs: NOTIFY_TIMEOUT_MS.recipient });
|
|
1260
|
-
}
|
|
1261
|
-
async function savePushSubscription(email, subscription, opts = {}) {
|
|
1262
|
-
const { label, piUserId, ...rest } = opts;
|
|
1263
|
-
return hubRequest(rest, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1264
|
-
method: "POST",
|
|
1265
|
-
body: { subscription, ...label ? { label } : {}, ...piUserId ? { piUserId } : {} },
|
|
1266
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
async function deletePushSubscription(email, endpoint, opts = {}) {
|
|
1270
|
-
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1271
|
-
method: "DELETE",
|
|
1272
|
-
body: { endpoint },
|
|
1273
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
async function listPushDevices(email, opts = {}) {
|
|
1277
|
-
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1278
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
1310
|
// src/safe.ts
|
|
1283
1311
|
var warnedDisabled = false;
|
|
1284
1312
|
function warnDisabled(what, env) {
|
|
@@ -1339,40 +1367,42 @@ export {
|
|
|
1339
1367
|
requireFrom,
|
|
1340
1368
|
parseAddressList,
|
|
1341
1369
|
formatAddress,
|
|
1342
|
-
createSmtpProvider,
|
|
1343
|
-
SMTP_PRESETS,
|
|
1344
|
-
createPresetSmtpProvider,
|
|
1345
|
-
providersFromEnv,
|
|
1346
1370
|
MailError,
|
|
1347
1371
|
AllProvidersFailedError,
|
|
1348
1372
|
classifyError,
|
|
1349
|
-
DEFAULT_COOLDOWN,
|
|
1350
|
-
createConsoleProvider,
|
|
1351
1373
|
DEFAULT_HUB_URL,
|
|
1352
1374
|
createHubProvider,
|
|
1353
|
-
createResendProvider,
|
|
1354
|
-
createZeptoMailProvider,
|
|
1355
|
-
createMemoryStore,
|
|
1356
|
-
createMailer,
|
|
1357
|
-
configureMailer,
|
|
1358
|
-
getMailer,
|
|
1359
|
-
sendMail,
|
|
1360
|
-
listProviders,
|
|
1361
|
-
resetMailer,
|
|
1362
1375
|
toEmails,
|
|
1363
1376
|
NOTIFY_TIMEOUT_MS,
|
|
1364
1377
|
hasNotifyKey,
|
|
1365
1378
|
hubRequest,
|
|
1379
|
+
absoluteLink,
|
|
1366
1380
|
notify,
|
|
1367
1381
|
notifyTopic,
|
|
1368
1382
|
getRequestStatus,
|
|
1369
1383
|
getRecipient,
|
|
1370
1384
|
createLink,
|
|
1385
|
+
pauseChannel,
|
|
1371
1386
|
getChannelStatus,
|
|
1372
1387
|
getPushConfig,
|
|
1373
1388
|
savePushSubscription,
|
|
1374
1389
|
deletePushSubscription,
|
|
1375
1390
|
listPushDevices,
|
|
1391
|
+
createSmtpProvider,
|
|
1392
|
+
SMTP_PRESETS,
|
|
1393
|
+
createPresetSmtpProvider,
|
|
1394
|
+
providersFromEnv,
|
|
1395
|
+
DEFAULT_COOLDOWN,
|
|
1396
|
+
createConsoleProvider,
|
|
1397
|
+
createResendProvider,
|
|
1398
|
+
createZeptoMailProvider,
|
|
1399
|
+
createMemoryStore,
|
|
1400
|
+
createMailer,
|
|
1401
|
+
configureMailer,
|
|
1402
|
+
getMailer,
|
|
1403
|
+
sendMail,
|
|
1404
|
+
listProviders,
|
|
1405
|
+
resetMailer,
|
|
1376
1406
|
resetSafeWarnings,
|
|
1377
1407
|
notifySafe,
|
|
1378
1408
|
notifyTopicSafe,
|