@suphark/notify 0.10.1 → 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 +23 -0
- package/dist/{chunk-JN7P2CVF.js → chunk-SBQOHUW4.js} +423 -403
- 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,
|
|
@@ -402,153 +671,69 @@ var Health = class {
|
|
|
402
671
|
*/
|
|
403
672
|
async allowProbe(name) {
|
|
404
673
|
return await this.store.incr(`probe:${name}`, PROBE_INTERVAL_SEC) === 1;
|
|
405
|
-
}
|
|
406
|
-
async status(p, quota) {
|
|
407
|
-
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
|
-
}
|
|
674
|
+
}
|
|
675
|
+
async status(p, quota) {
|
|
676
|
+
const cd = await this.cooldownOf(p.name);
|
|
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
|
}
|
|
@@ -956,7 +1141,8 @@ function createMailer(options = {}) {
|
|
|
956
1141
|
} catch (err) {
|
|
957
1142
|
const attempts = err instanceof AllProvidersFailedError ? err.attempts : [];
|
|
958
1143
|
const invalid = err instanceof MailError && err.kind === "invalid" && attempts.length === 0 || attempts.some((a) => a.kind === "invalid");
|
|
959
|
-
|
|
1144
|
+
const ambiguous = attempts.some((a) => a.error?.includes("send timed out after"));
|
|
1145
|
+
if (!fallback || !hasLocal || invalid || ambiguous) throw err;
|
|
960
1146
|
emit({ type: "fallback", template: message.template });
|
|
961
1147
|
const r = await sendVia(asFallback(fallback), "local");
|
|
962
1148
|
return { ...r, failedOver: true, fallback: true, attempts: [...attempts, ...r.attempts] };
|
|
@@ -1121,174 +1307,6 @@ function resetMailer() {
|
|
|
1121
1307
|
pending = void 0;
|
|
1122
1308
|
}
|
|
1123
1309
|
|
|
1124
|
-
// src/recipients.ts
|
|
1125
|
-
var emailOf = (r) => {
|
|
1126
|
-
const raw = typeof r === "string" ? r : r?.email;
|
|
1127
|
-
const email = raw?.trim().toLowerCase();
|
|
1128
|
-
return email ? email : null;
|
|
1129
|
-
};
|
|
1130
|
-
function toEmails(to) {
|
|
1131
|
-
if (Array.isArray(to)) {
|
|
1132
|
-
const emails = [...new Set(to.map(emailOf).filter((e) => e !== null))];
|
|
1133
|
-
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");
|
|
1134
|
-
return emails;
|
|
1135
|
-
}
|
|
1136
|
-
const email = emailOf(to);
|
|
1137
|
-
if (!email) throw invalidRecipient("\u0E1C\u0E39\u0E49\u0E23\u0E31\u0E1A\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2D\u0E35\u0E40\u0E21\u0E25");
|
|
1138
|
-
return email;
|
|
1139
|
-
}
|
|
1140
|
-
var invalidRecipient = (why) => new MailError(`notify(): ${why}`, { kind: "invalid", provider: "hub" });
|
|
1141
|
-
function toEmail(to) {
|
|
1142
|
-
const email = toEmails(to);
|
|
1143
|
-
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");
|
|
1144
|
-
return email;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// src/notify.ts
|
|
1148
|
-
var NOTIFY_TIMEOUT_MS = { notify: 65e3, recipient: 1e4 };
|
|
1149
|
-
function resolve(opts) {
|
|
1150
|
-
const env = opts.env ?? process.env;
|
|
1151
|
-
const apiKey = opts.apiKey ?? env.PI_NOTIFY_KEY;
|
|
1152
|
-
if (!apiKey) throw new MailError("notify(): PI_NOTIFY_KEY is required", { kind: "auth" });
|
|
1153
|
-
const base = (opts.url ?? env.PI_NOTIFY_URL ?? DEFAULT_HUB_URL).replace(/\/$/, "");
|
|
1154
|
-
return { apiKey, base, doFetch: opts.fetch ?? globalThis.fetch };
|
|
1155
|
-
}
|
|
1156
|
-
function hasNotifyKey(opts = {}) {
|
|
1157
|
-
return Boolean(opts.apiKey ?? (opts.env ?? process.env).PI_NOTIFY_KEY);
|
|
1158
|
-
}
|
|
1159
|
-
async function hubRequest(opts, path, init) {
|
|
1160
|
-
const { apiKey, base, doFetch } = resolve(opts);
|
|
1161
|
-
const ms = opts.timeoutMs ?? init.timeoutMs;
|
|
1162
|
-
const ac = new AbortController();
|
|
1163
|
-
const timer = setTimeout(() => ac.abort(), ms);
|
|
1164
|
-
const timedOut = () => new MailError(`Pi Notify timeout after ${ms}ms`, { kind: "network", provider: "hub" });
|
|
1165
|
-
let res;
|
|
1166
|
-
let data;
|
|
1167
|
-
try {
|
|
1168
|
-
res = await doFetch(`${base}${path}`, {
|
|
1169
|
-
method: init.method ?? "GET",
|
|
1170
|
-
headers: {
|
|
1171
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1172
|
-
...init.headers,
|
|
1173
|
-
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1174
|
-
},
|
|
1175
|
-
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
1176
|
-
signal: ac.signal
|
|
1177
|
-
});
|
|
1178
|
-
data = await res.json().catch(() => ({}));
|
|
1179
|
-
} catch (err) {
|
|
1180
|
-
if (ac.signal.aborted) throw timedOut();
|
|
1181
|
-
throw err;
|
|
1182
|
-
} finally {
|
|
1183
|
-
clearTimeout(timer);
|
|
1184
|
-
}
|
|
1185
|
-
if (ac.signal.aborted) throw timedOut();
|
|
1186
|
-
if (!res.ok) {
|
|
1187
|
-
const err = data?.error;
|
|
1188
|
-
const kind = err?.kind ?? (res.status === 401 || res.status === 403 ? "auth" : res.status === 429 ? "rate_limit" : res.status === 400 || res.status === 422 ? "invalid" : "server");
|
|
1189
|
-
throw new MailError(`Pi Notify ${res.status}: ${err?.message ?? res.statusText}`, {
|
|
1190
|
-
kind,
|
|
1191
|
-
provider: "hub",
|
|
1192
|
-
status: res.status,
|
|
1193
|
-
cause: data,
|
|
1194
|
-
retryAfterSec: retryAfterOf(res, data)
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
return data;
|
|
1198
|
-
}
|
|
1199
|
-
async function postNotify(body, idempotencyKey, opts) {
|
|
1200
|
-
const data = await hubRequest(opts, "/api/v1/notify", {
|
|
1201
|
-
method: "POST",
|
|
1202
|
-
body,
|
|
1203
|
-
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
1204
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.notify
|
|
1205
|
-
});
|
|
1206
|
-
return { results: data.results, ...data.summary };
|
|
1207
|
-
}
|
|
1208
|
-
async function notify(input, opts = {}) {
|
|
1209
|
-
const { idempotencyKey, ...body } = input;
|
|
1210
|
-
return postNotify({ ...body, to: toEmails(input.to) }, idempotencyKey, opts);
|
|
1211
|
-
}
|
|
1212
|
-
async function notifyTopic(input, opts = {}) {
|
|
1213
|
-
const { idempotencyKey, ...body } = input;
|
|
1214
|
-
return postNotify(body, idempotencyKey, opts);
|
|
1215
|
-
}
|
|
1216
|
-
async function getRequestStatus(idempotencyKey, opts = {}) {
|
|
1217
|
-
try {
|
|
1218
|
-
return await hubRequest(
|
|
1219
|
-
opts,
|
|
1220
|
-
`/api/v1/requests/${encodeURIComponent(idempotencyKey)}`,
|
|
1221
|
-
{ timeoutMs: NOTIFY_TIMEOUT_MS.recipient }
|
|
1222
|
-
);
|
|
1223
|
-
} catch (err) {
|
|
1224
|
-
if (err instanceof MailError && err.status === 404) return null;
|
|
1225
|
-
throw err;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
async function getRecipient(email, opts = {}) {
|
|
1229
|
-
return hubRequest(
|
|
1230
|
-
opts,
|
|
1231
|
-
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}`,
|
|
1232
|
-
{
|
|
1233
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1234
|
-
}
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
async function createLink(email, channel = "telegram", opts = {}) {
|
|
1238
|
-
const { piUserId, ...rest } = opts;
|
|
1239
|
-
const link = await hubRequest(
|
|
1240
|
-
rest,
|
|
1241
|
-
`/api/v1/recipients/${encodeURIComponent(toEmail(email))}/link`,
|
|
1242
|
-
{
|
|
1243
|
-
method: "POST",
|
|
1244
|
-
body: { channel, ...piUserId ? { piUserId } : {} },
|
|
1245
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1246
|
-
}
|
|
1247
|
-
);
|
|
1248
|
-
const code = link.code ?? safeStartParam(link.url);
|
|
1249
|
-
return { ...link, code };
|
|
1250
|
-
}
|
|
1251
|
-
function safeStartParam(url) {
|
|
1252
|
-
try {
|
|
1253
|
-
return new URL(url).searchParams.get("start") ?? "";
|
|
1254
|
-
} catch {
|
|
1255
|
-
return "";
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
async function getChannelStatus(email, channel = "telegram", opts = {}) {
|
|
1259
|
-
const recipient2 = await getRecipient(email, opts);
|
|
1260
|
-
const found = recipient2.channels.find((c) => c.type === channel && c.enabled);
|
|
1261
|
-
return {
|
|
1262
|
-
linked: Boolean(found),
|
|
1263
|
-
label: found?.label ?? null,
|
|
1264
|
-
linkedAt: found?.linkedAt ?? null,
|
|
1265
|
-
manageUrl: recipient2.linkUrl
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
async function getPushConfig(opts = {}) {
|
|
1269
|
-
return hubRequest(opts, "/api/v1/push/config", { timeoutMs: NOTIFY_TIMEOUT_MS.recipient });
|
|
1270
|
-
}
|
|
1271
|
-
async function savePushSubscription(email, subscription, opts = {}) {
|
|
1272
|
-
const { label, piUserId, ...rest } = opts;
|
|
1273
|
-
return hubRequest(rest, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1274
|
-
method: "POST",
|
|
1275
|
-
body: { subscription, ...label ? { label } : {}, ...piUserId ? { piUserId } : {} },
|
|
1276
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
async function deletePushSubscription(email, endpoint, opts = {}) {
|
|
1280
|
-
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1281
|
-
method: "DELETE",
|
|
1282
|
-
body: { endpoint },
|
|
1283
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
async function listPushDevices(email, opts = {}) {
|
|
1287
|
-
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1288
|
-
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
1310
|
// src/safe.ts
|
|
1293
1311
|
var warnedDisabled = false;
|
|
1294
1312
|
function warnDisabled(what, env) {
|
|
@@ -1349,40 +1367,42 @@ export {
|
|
|
1349
1367
|
requireFrom,
|
|
1350
1368
|
parseAddressList,
|
|
1351
1369
|
formatAddress,
|
|
1352
|
-
createSmtpProvider,
|
|
1353
|
-
SMTP_PRESETS,
|
|
1354
|
-
createPresetSmtpProvider,
|
|
1355
|
-
providersFromEnv,
|
|
1356
1370
|
MailError,
|
|
1357
1371
|
AllProvidersFailedError,
|
|
1358
1372
|
classifyError,
|
|
1359
|
-
DEFAULT_COOLDOWN,
|
|
1360
|
-
createConsoleProvider,
|
|
1361
1373
|
DEFAULT_HUB_URL,
|
|
1362
1374
|
createHubProvider,
|
|
1363
|
-
createResendProvider,
|
|
1364
|
-
createZeptoMailProvider,
|
|
1365
|
-
createMemoryStore,
|
|
1366
|
-
createMailer,
|
|
1367
|
-
configureMailer,
|
|
1368
|
-
getMailer,
|
|
1369
|
-
sendMail,
|
|
1370
|
-
listProviders,
|
|
1371
|
-
resetMailer,
|
|
1372
1375
|
toEmails,
|
|
1373
1376
|
NOTIFY_TIMEOUT_MS,
|
|
1374
1377
|
hasNotifyKey,
|
|
1375
1378
|
hubRequest,
|
|
1379
|
+
absoluteLink,
|
|
1376
1380
|
notify,
|
|
1377
1381
|
notifyTopic,
|
|
1378
1382
|
getRequestStatus,
|
|
1379
1383
|
getRecipient,
|
|
1380
1384
|
createLink,
|
|
1385
|
+
pauseChannel,
|
|
1381
1386
|
getChannelStatus,
|
|
1382
1387
|
getPushConfig,
|
|
1383
1388
|
savePushSubscription,
|
|
1384
1389
|
deletePushSubscription,
|
|
1385
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,
|
|
1386
1406
|
resetSafeWarnings,
|
|
1387
1407
|
notifySafe,
|
|
1388
1408
|
notifyTopicSafe,
|