@voltro/plugin-mail 0.84.0 → 0.86.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/dist/index.js CHANGED
@@ -1,951 +1,22 @@
1
- import { Context as e, Duration as t, Effect as n, Fiber as r, Layer as i, ParseResult as a, Schedule as o, Schema as s } from "effect";
2
- import { definePlugin as c, pluginInstanceName as l } from "@voltro/protocol";
3
- import { pluginEnv as u } from "@voltro/env";
4
- import { createLogger as d } from "@voltro/logger";
5
- import { and as f, dropped as p, eq as m, id as h, inSet as g, queryFor as _, registerRetention as ee, retentionTtlMsFromEnv as v, table as y, text as b, timestamp as x } from "@voltro/database";
6
- import { FetchHttpClient as te, HttpClient as S, HttpClientRequest as C } from "@effect/platform";
7
- //#region src/types.ts
8
- var w = class extends s.TaggedError()("MailError", {
9
- provider: s.String,
10
- message: s.String,
11
- transient: s.Boolean,
12
- status: s.optional(s.Number)
13
- }) {}, T = class extends s.TaggedError()("MailWebhookError", {
14
- provider: s.String,
15
- message: s.String
16
- }) {}, ne = (e) => ({
17
- name: e.name,
18
- props: e.props,
19
- subject: e.subject,
20
- render: e.render,
21
- ...e.locale === void 0 ? {} : { locale: e.locale },
22
- ...e.preview === void 0 ? {} : { preview: e.preview }
23
- }), E = class extends e.Tag("@voltro/plugin-mail/MailService")() {}, D = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, re = (e) => {
24
- try {
25
- let t = a.ArrayFormatter.formatErrorSync(e);
26
- return t.length === 0 ? String(e) : t.map((e) => `${e.path.length > 0 ? e.path.join(".") : "(root)"}: ${e.message}`).join("; ");
27
- } catch {
28
- return String(e);
29
- }
30
- }, O = null, ie = async () => {
31
- if (!O) {
32
- let e = await import("@react-email/render");
33
- if (O = e.render ?? e.default?.render ?? null, !O) throw Error("@react-email/render: `render` export not found (install @react-email/render + react + react-dom)");
34
- }
35
- return O;
36
- }, k = (e, t) => n.gen(function* () {
37
- let r = e, i = yield* s.decodeUnknown(r.props)(t).pipe(n.mapError((t) => new w({
38
- provider: "template",
39
- message: `invalid props for '${e.name}': ${re(t)}`.slice(0, 500),
40
- transient: !1
41
- }))), a = r.subject(i), o = r.render(i);
42
- return {
43
- subject: a,
44
- html: yield* n.tryPromise({
45
- try: async () => (await ie())(o, { plainText: !1 }),
46
- catch: (t) => new w({
47
- provider: "template",
48
- message: `render failed for '${e.name}': ${String(t)}`.slice(0, 500),
49
- transient: !1
50
- })
51
- }),
52
- text: yield* n.tryPromise({
53
- try: async () => (await ie())(o, { plainText: !0 }),
54
- catch: (t) => new w({
55
- provider: "template",
56
- message: `render(text) failed for '${e.name}': ${String(t)}`.slice(0, 500),
57
- transient: !1
58
- })
59
- })
60
- };
61
- }), A = /* @__PURE__ */ new Map(), ae = (e) => e.locale ? `${e.name}:${e.locale}` : e.name, oe = (e) => {
62
- for (let t of e) A.set(ae(t), t);
63
- }, j = (e, t) => (t ? A.get(`${e}:${t}`) : void 0) ?? A.get(e), se = () => A.size, ce = () => [...A.values()].map((e) => e.locale ? {
64
- name: e.name,
65
- locale: e.locale
66
- } : { name: e.name }), le = () => A.clear(), ue = (e) => {
67
- if (typeof e != "object" || !e) return !1;
68
- let t = e;
69
- return typeof t.name == "string" && typeof t.subject == "function" && typeof t.render == "function" && t.props !== void 0;
70
- }, M = "_voltro_mail_suppression", N = "_voltro_mail_idempotency", de = y(M, {
71
- id: h({ scheme: "numeric" }),
72
- tenant: b().maxLength(191),
73
- email: b().maxLength(320),
74
- reason: b().nullable(),
75
- createdAt: x().default("now"),
76
- created_at: p()
77
- }).unique("mailSuppressionAddress", ["tenant", "email"]).nonReactive(), fe = y(N, {
78
- id: h({ scheme: "numeric" }),
79
- tenant: b().maxLength(191),
80
- key: b().maxLength(255),
81
- resultId: b().nullable().renamedFrom("result_id"),
82
- resultProvider: b().nullable().renamedFrom("result_provider"),
83
- createdAt: x().default("now"),
84
- created_at: p()
85
- }).unique("mailIdempotencyKey", ["tenant", "key"]).nonReactive(), P = (e) => n.tryPromise({
86
- try: e,
87
- catch: (e) => e
88
- }), F = (e) => {
89
- let t = [];
90
- for (let n = e, r = 0; n != null && r < 4; n = n.cause, r++) t.push(n instanceof Error ? n.message : String(n));
91
- return t.join(" ← ");
92
- }, pe = d({ scope: "@voltro/plugin-mail" }), I = (e) => e.trim().toLowerCase(), me = "*", L = (e) => e ?? me, R = () => {
93
- let e = /* @__PURE__ */ new Set(), t = (e, t) => `${L(e)}\u0000${I(t)}`;
94
- return {
95
- isSuppressed: (r, i) => n.sync(() => e.has(t(r, i)) || e.has(t(null, i))),
96
- suppress: (r, i) => n.sync(() => void e.add(t(r, i))),
97
- unsuppress: (r, i) => n.sync(() => void e.delete(t(r, i)))
98
- };
99
- }, he = () => {
100
- let e = null, t = (e, t) => ({
101
- tenant: L(e),
102
- email: I(t)
103
- });
104
- return {
105
- isSuppressed: (t, r) => {
106
- let i = e;
107
- return i ? P(() => i.query(_(de).where(f(m("email", I(r)), g("tenant", [L(t), me]))).limit(1).descriptor)).pipe(n.map((e) => e.length > 0), n.catchAll(() => n.succeed(!1))) : n.succeed(!1);
108
- },
109
- suppress: (r, i, a) => {
110
- let o = e;
111
- return o ? P(() => o.upsert(M, {
112
- ...t(r, i),
113
- reason: a,
114
- createdAt: /* @__PURE__ */ new Date()
115
- }, { conflictColumns: ["tenant", "email"] })).pipe(n.asVoid, n.catchAll((e) => n.sync(() => pe.warn("suppress write failed", {
116
- email: I(i),
117
- message: F(e)
118
- })))) : n.void;
119
- },
120
- unsuppress: (r, i) => {
121
- let a = e;
122
- if (!a) return n.void;
123
- let { tenant: o, email: s } = t(r, i);
124
- return P(() => a.deleteMany(M, { where: f(m("tenant", o), m("email", s)) })).pipe(n.asVoid, n.catchAll(() => n.void));
125
- },
126
- bind: async (t) => {
127
- e = t;
128
- }
129
- };
130
- }, ge = d({ scope: "@voltro/plugin-mail" }), z = (e) => e ?? "*", B = () => {
131
- let e = /* @__PURE__ */ new Map(), t = (e, t) => `${z(e)} ${t}`;
132
- return {
133
- reserve: (r, i) => n.sync(() => {
134
- let n = t(r, i);
135
- if (e.has(n)) {
136
- let t = e.get(n);
137
- return t ? {
138
- won: !1,
139
- prior: t
140
- } : { won: !1 };
141
- }
142
- return e.set(n, null), { won: !0 };
143
- }),
144
- record: (r, i, a) => n.sync(() => void e.set(t(r, i), a))
145
- };
146
- }, _e = () => {
147
- let e = null;
148
- return {
149
- reserve: (t, r) => {
150
- let i = e;
151
- return i ? P(() => i.insertIgnoreWithOutcome(N, {
152
- tenant: z(t),
153
- key: r,
154
- createdAt: /* @__PURE__ */ new Date()
155
- }, { conflictColumns: ["tenant", "key"] })).pipe(n.map(({ row: e, outcome: t }) => {
156
- if (t === "inserted") return { won: !0 };
157
- let n = e.resultId, r = e.resultProvider;
158
- return typeof n == "string" && typeof r == "string" ? {
159
- won: !1,
160
- prior: {
161
- id: n,
162
- provider: r
163
- }
164
- } : { won: !1 };
165
- }), n.catchAll((e) => n.sync(() => (ge.warn("idempotency reserve failed — skipping send to avoid a double", {
166
- key: r,
167
- message: F(e)
168
- }), { won: !1 })))) : n.succeed({ won: !0 });
169
- },
170
- record: (t, r, i) => {
171
- let a = e;
172
- return a ? P(() => a.updateMany(N, {
173
- resultId: i.id,
174
- resultProvider: i.provider
175
- }, { where: f(m("tenant", z(t)), m("key", r)) })).pipe(n.asVoid, n.catchAll((e) => n.sync(() => ge.warn("idempotency record failed", {
176
- key: r,
177
- message: F(e)
178
- })))) : n.void;
179
- },
180
- bind: async (t) => {
181
- e = t;
182
- }
183
- };
184
- }, ve = d({ scope: "@voltro/plugin-mail" }), V = () => {
185
- let e = /* @__PURE__ */ new Map();
186
- return {
187
- hold: (t) => n.sync(() => void e.set(t.id, t)),
188
- claimDue: (t) => n.sync(() => {
189
- let n = [];
190
- for (let [r, i] of e) i.dueAt <= t && (n.push(i), e.delete(r));
191
- return n;
192
- }),
193
- size: () => n.sync(() => e.size)
194
- };
195
- }, ye = 0, be = (e) => e.idempotencyKey ?? `held-${ye += 1}`, xe = (e) => ({
196
- shouldHold: (t) => !e.providerSchedules && t !== void 0 && t.getTime() > Date.now(),
197
- hold: (t) => {
198
- let { scheduledAt: n, ...r } = t;
199
- return e.store.hold({
200
- id: be(t),
201
- dueAt: t.scheduledAt?.getTime() ?? Date.now(),
202
- message: r
203
- });
204
- },
205
- tick: (t = Date.now()) => n.gen(function* () {
206
- let r = yield* e.store.claimDue(t);
207
- for (let t of r) yield* e.flush(t.message).pipe(n.catchAll((e) => n.sync(() => ve.warn("scheduled flush failed", {
208
- id: t.id,
209
- message: String(e)
210
- }))));
211
- return r.length;
212
- }),
213
- pending: () => e.store.size()
214
- }), Se = 0, Ce = (e) => `${e}-${Se += 1}`, we = (e) => e === void 0 || e === 408 || e === 429 || e >= 500, H = (e, t) => {
215
- let n = t.status;
216
- return new w({
217
- provider: e,
218
- message: (t.message ?? "send failed").slice(0, 500),
219
- transient: e === "smtp" ? n === void 0 || n >= 400 && n < 500 : we(n),
220
- ...n === void 0 ? {} : { status: n }
221
- });
222
- }, U = (e, t, r, i, a) => n.tryPromise({
223
- try: async () => {
224
- let e = await fetch(t, {
225
- method: "POST",
226
- headers: {
227
- "content-type": "application/json",
228
- ...r
229
- },
230
- body: JSON.stringify(i)
231
- });
232
- if (!e.ok) {
233
- let t = await e.text().catch(() => "");
234
- throw {
235
- status: e.status,
236
- message: t || `HTTP ${e.status}`
237
- };
238
- }
239
- return await a(e);
240
- },
241
- catch: (t) => {
242
- let n = t;
243
- return H(e, {
244
- ...typeof n.status == "number" ? { status: n.status } : {},
245
- message: n.message ?? t?.message ?? String(t)
246
- });
247
- }
248
- }), W = 500, Te = (e, t) => {
249
- let n = e.stream === "broadcast" ? t?.broadcast ?? "broadcast" : t?.transactional;
250
- return {
251
- ...n === void 0 ? {} : { route: n },
252
- from: e.from,
253
- to: D(e.to),
254
- ...e.cc ? { cc: D(e.cc) } : {},
255
- ...e.bcc ? { bcc: D(e.bcc) } : {},
256
- ...e.replyTo ? { reply_to: [e.replyTo] } : {},
257
- subject: e.subject,
258
- ...e.html === void 0 ? {} : { html: e.html },
259
- ...e.text === void 0 ? {} : { text: e.text },
260
- ...e.headers ? { headers: e.headers } : {},
261
- ...e.metadata ? { metadata: e.metadata } : {},
262
- ...e.tags && e.tags.length > 0 ? { tags: e.tags.map((e) => ({
263
- name: e,
264
- value: "1"
265
- })) } : {},
266
- ...e.scheduledAt ? { scheduled_at: e.scheduledAt.toISOString() } : {},
267
- ...e.attachments ? { attachments: e.attachments.map((e) => ({
268
- filename: e.filename,
269
- content: e.content,
270
- ...e.contentType ? { content_type: e.contentType } : {}
271
- })) } : {}
272
- };
273
- }, Ee = (e, t, r, i = Math.floor(Date.now() / 1e3), a = 300) => n.tryPromise({
274
- try: async () => {
275
- let n = (e) => {
276
- throw new T({
277
- provider: "lettermint",
278
- message: e
279
- });
280
- }, o = (t) => Object.entries(e).find(([e]) => e.toLowerCase() === t)?.[1];
281
- r === "" && n("no webhook secret configured"), t === "" && n("empty body");
282
- let s = o("x-lettermint-signature") ?? n("missing x-lettermint-signature"), c = o("x-lettermint-delivery") ?? n("missing x-lettermint-delivery"), l, u = [];
283
- for (let e of s.split(",")) {
284
- let [t, n] = e.trim().split("=", 2);
285
- t === "t" && n !== void 0 && /^\d+$/.test(n) ? l = n : t === "v1" && n !== void 0 && /^[a-fA-F0-9]{64}$/.test(n) && u.push(n.toLowerCase());
286
- }
287
- (l === void 0 || u.length === 0) && n("malformed signature header"), c !== l && n("signature and delivery timestamps differ"), Math.abs(i - Number(l)) > a && n("signature timestamp outside the tolerance");
288
- let { createHmac: d, timingSafeEqual: f } = await import("node:crypto"), p = d("sha256", r).update(`${l}.`).update(t).digest();
289
- u.some((e) => f(Buffer.from(e, "hex"), p)) || n("signature does not match");
290
- try {
291
- return JSON.parse(t);
292
- } catch {
293
- return n("body is not JSON");
294
- }
295
- },
296
- catch: (e) => e instanceof T ? e : new T({
297
- provider: "lettermint",
298
- message: e?.message ?? String(e)
299
- })
300
- }), De = (e) => {
301
- let t = (e.baseUrl ?? "https://api.lettermint.co/v1").replace(/\/+$/, ""), r = {
302
- "x-lettermint-token": e.projectToken,
303
- accept: "application/json"
304
- };
305
- return {
306
- name: "lettermint",
307
- supportsScheduling: !0,
308
- managesUnsubscribe: !0,
309
- send: (n) => U("lettermint", `${t}/send`, {
310
- ...r,
311
- ...n.idempotencyKey === void 0 ? {} : { "idempotency-key": n.idempotencyKey }
312
- }, Te(n, e.routes), async (e) => ({
313
- id: (await e.json()).message_id ?? "lettermint",
314
- provider: "lettermint"
315
- })),
316
- sendBatch: (i) => n.forEach(Array.from({ length: Math.ceil(i.length / W) }, (e, t) => i.slice(t * W, (t + 1) * W)), (n) => U("lettermint", `${t}/send/batch`, r, n.map((t) => Te(t, e.routes)), async (e) => {
317
- let t = await e.json();
318
- return n.map((e, n) => ({
319
- id: t[n]?.message_id ?? "lettermint",
320
- provider: "lettermint"
321
- }));
322
- })).pipe(n.map((e) => e.flat())),
323
- webhook: { verify: Ee }
324
- };
325
- }, Oe = (e) => ({
326
- from: e.from,
327
- to: D(e.to),
328
- subject: e.subject,
329
- html: e.html,
330
- text: e.text,
331
- reply_to: e.replyTo,
332
- cc: e.cc ? D(e.cc) : void 0,
333
- bcc: e.bcc ? D(e.bcc) : void 0,
334
- headers: e.headers,
335
- tags: e.tags?.map((e) => ({
336
- name: e,
337
- value: "1"
338
- })),
339
- ...e.scheduledAt ? { scheduled_at: e.scheduledAt.toISOString() } : {},
340
- attachments: e.attachments?.map((e) => ({
341
- filename: e.filename,
342
- content: e.content,
343
- ...e.contentType ? { content_type: e.contentType } : {}
344
- }))
345
- }), ke = (e) => ({
346
- name: "resend",
347
- supportsScheduling: !0,
348
- send: (t) => U("resend", "https://api.resend.com/emails", { authorization: `Bearer ${e.apiKey}` }, Oe(t), async (e) => ({
349
- id: (await e.json()).id ?? "resend",
350
- provider: "resend"
351
- })),
352
- sendBatch: (t) => U("resend", "https://api.resend.com/emails/batch", { authorization: `Bearer ${e.apiKey}` }, t.map(Oe), async (e) => {
353
- let n = (await e.json()).data ?? [];
354
- return t.map((e, t) => ({
355
- id: n[t]?.id ?? "resend",
356
- provider: "resend"
357
- }));
358
- })
359
- }), Ae = (e) => ({
360
- name: "postmark",
361
- send: (t) => U("postmark", "https://api.postmarkapp.com/email", {
362
- "x-postmark-server-token": e.serverToken,
363
- accept: "application/json"
364
- }, {
365
- From: t.from,
366
- To: D(t.to).join(","),
367
- Subject: t.subject,
368
- HtmlBody: t.html,
369
- TextBody: t.text,
370
- ReplyTo: t.replyTo,
371
- Cc: t.cc ? D(t.cc).join(",") : void 0,
372
- Bcc: t.bcc ? D(t.bcc).join(",") : void 0,
373
- MessageStream: t.stream === "broadcast" ? e.streams?.broadcast ?? "broadcast" : e.streams?.transactional ?? "outbound",
374
- Headers: t.headers ? Object.entries(t.headers).map(([e, t]) => ({
375
- Name: e,
376
- Value: t
377
- })) : void 0,
378
- Metadata: t.metadata,
379
- Attachments: t.attachments?.map((e) => ({
380
- Name: e.filename,
381
- Content: e.content,
382
- ContentType: e.contentType ?? "application/octet-stream"
383
- }))
384
- }, async (e) => ({
385
- id: (await e.json()).MessageID ?? "postmark",
386
- provider: "postmark"
387
- }))
388
- }), je = (e) => [...e.text ? [{
389
- type: "text/plain",
390
- value: e.text
391
- }] : [], ...e.html ? [{
392
- type: "text/html",
393
- value: e.html
394
- }] : []], Me = (e) => ({
395
- to: D(e.to).map((e) => ({ email: e })),
396
- cc: e.cc ? D(e.cc).map((e) => ({ email: e })) : void 0,
397
- bcc: e.bcc ? D(e.bcc).map((e) => ({ email: e })) : void 0,
398
- subject: e.subject,
399
- ...e.scheduledAt ? { send_at: Math.floor(e.scheduledAt.getTime() / 1e3) } : {}
400
- }), Ne = (e) => e.attachments?.map((e) => ({
401
- filename: e.filename,
402
- content: e.content,
403
- type: e.contentType ?? "application/octet-stream",
404
- disposition: "attachment"
405
- })), Pe = (e) => {
406
- let t = (e) => ({
407
- personalizations: [Me(e)],
408
- from: { email: e.from },
409
- reply_to: e.replyTo ? { email: e.replyTo } : void 0,
410
- subject: e.subject,
411
- content: je(e),
412
- attachments: Ne(e)
413
- });
414
- return {
415
- name: "sendgrid",
416
- supportsScheduling: !0,
417
- send: (n) => U("sendgrid", "https://api.sendgrid.com/v3/mail/send", { authorization: `Bearer ${e.apiKey}` }, t(n), async (e) => ({
418
- id: e.headers.get("x-message-id") ?? "sendgrid",
419
- provider: "sendgrid"
420
- })),
421
- sendBatch: (t) => U("sendgrid", "https://api.sendgrid.com/v3/mail/send", { authorization: `Bearer ${e.apiKey}` }, {
422
- personalizations: t.map(Me),
423
- from: { email: t[0]?.from },
424
- subject: t[0]?.subject,
425
- content: je(t[0] ?? {})
426
- }, async (e) => {
427
- let n = e.headers.get("x-message-id") ?? "sendgrid";
428
- return t.map((e, t) => ({
429
- id: `${n}-${t}`,
430
- provider: "sendgrid"
431
- }));
432
- })
433
- };
434
- }, Fe = (e, t) => {
435
- let n = t, r = n.response?.status;
436
- return H(e, {
437
- ...typeof r == "number" ? { status: r } : {},
438
- message: n.message ?? String(t)
439
- });
440
- }, Ie = (e, t, r) => n.gen(function* () {
441
- let i = yield* (yield* S.HttpClient).execute(t);
442
- if (i.status < 200 || i.status >= 300) {
443
- let t = yield* i.text.pipe(n.catchAll(() => n.succeed("")));
444
- return yield* n.fail(H(e, {
445
- status: i.status,
446
- message: t || `HTTP ${i.status}`
447
- }));
448
- }
449
- return yield* r(i);
450
- }).pipe(n.catchAll((t) => t instanceof w ? n.fail(t) : n.fail(Fe(e, t))), n.provide(te.layer)), Le = (e) => ({ sign: async (t, n, r) => {
451
- let { createHash: i, createHmac: a } = await import("node:crypto"), o = (e) => i("sha256").update(e).digest("hex"), s = (e, t) => a("sha256", e).update(t).digest(), c = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, ""), l = c.slice(0, 8), u = `host:${new URL(t).host}\nx-amz-date:${c}\n` + (e.sessionToken ? `x-amz-security-token:${e.sessionToken}\n` : ""), d = e.sessionToken ? "host;x-amz-date;x-amz-security-token" : "host;x-amz-date", f = `POST\n${n}\n\n${u}\n${d}\n${o(r)}`, p = `${l}/${e.region}/ses/aws4_request`, m = `AWS4-HMAC-SHA256\n${c}\n${p}\n${o(f)}`, h = a("sha256", s(s(s(s(`AWS4${e.secretAccessKey}`, l), e.region), "ses"), "aws4_request")).update(m).digest("hex");
452
- return {
453
- authorization: `AWS4-HMAC-SHA256 Credential=${e.accessKeyId}/${p}, SignedHeaders=${d}, Signature=${h}`,
454
- "x-amz-date": c,
455
- ...e.sessionToken ? { "x-amz-security-token": e.sessionToken } : {}
456
- };
457
- } }), Re = (e) => JSON.stringify({
458
- FromEmailAddress: e.from,
459
- Destination: {
460
- ToAddresses: D(e.to),
461
- ...e.cc ? { CcAddresses: D(e.cc) } : {},
462
- ...e.bcc ? { BccAddresses: D(e.bcc) } : {}
463
- },
464
- ...e.replyTo ? { ReplyToAddresses: [e.replyTo] } : {},
465
- Content: { Simple: {
466
- Subject: {
467
- Data: e.subject,
468
- Charset: "UTF-8"
469
- },
470
- Body: {
471
- ...e.html ? { Html: {
472
- Data: e.html,
473
- Charset: "UTF-8"
474
- } } : {},
475
- ...e.text ? { Text: {
476
- Data: e.text,
477
- Charset: "UTF-8"
478
- } } : {}
479
- }
480
- } }
481
- }), ze = (e) => {
482
- let t = Le(e), r = `https://email.${e.region}.amazonaws.com`, i = "/v2/email/outbound-emails";
483
- return {
484
- name: "ses",
485
- send: (e) => n.gen(function* () {
486
- let a = Re(e), o = yield* n.tryPromise({
487
- try: () => t.sign(r, i, a),
488
- catch: (e) => H("ses", { message: e?.message ?? String(e) })
489
- });
490
- return yield* Ie("ses", C.post(`${r}${i}`).pipe(C.setHeaders({
491
- "content-type": "application/json",
492
- ...o
493
- }), C.bodyUnsafeJson(JSON.parse(a))), (e) => e.json.pipe(n.map((e) => ({
494
- id: e.MessageId ?? "ses",
495
- provider: "ses"
496
- }))));
497
- })
498
- };
499
- }, Be = (e) => {
500
- let t = [["from", e.from], ["subject", e.subject]];
501
- for (let n of D(e.to)) t.push(["to", n]);
502
- if (e.cc) for (let n of D(e.cc)) t.push(["cc", n]);
503
- if (e.bcc) for (let n of D(e.bcc)) t.push(["bcc", n]);
504
- if (e.replyTo && t.push(["h:Reply-To", e.replyTo]), e.html && t.push(["html", e.html]), e.text && t.push(["text", e.text]), e.headers) for (let [n, r] of Object.entries(e.headers)) t.push([`h:${n}`, r]);
505
- if (e.tags) for (let n of e.tags) t.push(["o:tag", n]);
506
- return e.scheduledAt && t.push(["o:deliverytime", e.scheduledAt.toUTCString()]), t;
507
- }, Ve = (e) => {
508
- let t = `${e.baseUrl ?? "https://api.mailgun.net"}/v3/${e.domain}/messages`;
509
- return {
510
- name: "mailgun",
511
- supportsScheduling: !0,
512
- send: (r) => {
513
- let i = new URLSearchParams();
514
- for (let [e, t] of Be(r)) i.append(e, t);
515
- return Ie("mailgun", C.post(t).pipe(C.setHeader("authorization", `Basic ${Buffer.from(`api:${e.apiKey}`).toString("base64")}`), C.bodyText(i.toString(), "application/x-www-form-urlencoded")), (e) => e.json.pipe(n.map((e) => ({
516
- id: e.id ?? "mailgun",
517
- provider: "mailgun"
518
- }))));
519
- }
520
- };
521
- }, G = (e) => {
522
- let t = null;
523
- return {
524
- name: "smtp",
525
- send: (r) => n.tryPromise({
526
- try: async () => {
527
- if (!t) {
528
- let n = await import("nodemailer"), r = n.createTransport ?? n.default?.createTransport;
529
- if (!r) throw Error("nodemailer: createTransport export not found");
530
- t = r(e.url ?? {
531
- host: e.host,
532
- port: e.port,
533
- secure: e.secure,
534
- ...e.auth ? { auth: e.auth } : {}
535
- });
536
- }
537
- return {
538
- id: (await t.sendMail({
539
- from: r.from,
540
- to: D(r.to),
541
- cc: r.cc ? D(r.cc) : void 0,
542
- bcc: r.bcc ? D(r.bcc) : void 0,
543
- replyTo: r.replyTo,
544
- subject: r.subject,
545
- html: r.html,
546
- text: r.text,
547
- headers: r.headers,
548
- attachments: r.attachments?.map((e) => ({
549
- filename: e.filename,
550
- content: e.content,
551
- encoding: "base64",
552
- ...e.contentType ? { contentType: e.contentType } : {}
553
- }))
554
- }))?.messageId ?? "smtp",
555
- provider: "smtp"
556
- };
557
- },
558
- catch: (e) => {
559
- let t = e;
560
- return H("smtp", {
561
- ...typeof t.responseCode == "number" ? { status: t.responseCode } : {},
562
- message: t.message ?? String(e)
563
- });
564
- }
565
- })
566
- };
567
- }, He = (e) => ({
568
- name: "console",
569
- send: (t) => n.sync(() => {
570
- let n = `✉ to=${D(t.to).join(",")} subject=${JSON.stringify(t.subject)}`;
571
- return e ? e(n, { from: t.from }) : process.stdout.write(`[mail:console] ${n}\n`), {
572
- id: Ce("console"),
573
- provider: "console"
574
- };
575
- })
576
- }), K = [], Ue = () => [...K], We = () => {
577
- K.length = 0;
578
- }, Ge = () => ({
579
- name: "memory",
580
- send: (e) => n.sync(() => (K.push(e), {
581
- id: Ce("mem"),
582
- provider: "memory"
583
- }))
584
- }), q = d({ scope: "@voltro/plugin-mail" }), Ke = (e) => {
585
- if (e.provider !== void 0 && typeof e.provider != "string") return e.provider;
586
- let t = e.env ?? {}, n = e.provider ?? t.MAIL_PROVIDER ?? "console", r = (e, t) => {
587
- if (!t) throw Error(`@voltro/plugin-mail: provider '${n}' needs apiKey or ${e}`);
588
- return t;
589
- };
590
- switch (n) {
591
- case "lettermint": return De({ projectToken: r("LETTERMINT_PROJECT_TOKEN", e.apiKey ?? t.LETTERMINT_PROJECT_TOKEN) });
592
- case "resend": return ke({ apiKey: r("RESEND_API_KEY", e.apiKey ?? t.RESEND_API_KEY) });
593
- case "postmark": return Ae({ serverToken: r("POSTMARK_SERVER_TOKEN", e.apiKey ?? t.POSTMARK_SERVER_TOKEN) });
594
- case "sendgrid": return Pe({ apiKey: r("SENDGRID_API_KEY", e.apiKey ?? t.SENDGRID_API_KEY) });
595
- case "ses": return ze({
596
- region: r("AWS_REGION", t.AWS_REGION),
597
- accessKeyId: r("AWS_ACCESS_KEY_ID", t.AWS_ACCESS_KEY_ID),
598
- secretAccessKey: r("AWS_SECRET_ACCESS_KEY", t.AWS_SECRET_ACCESS_KEY),
599
- ...t.AWS_SESSION_TOKEN ? { sessionToken: t.AWS_SESSION_TOKEN } : {}
600
- });
601
- case "mailgun": return Ve({
602
- apiKey: r("MAILGUN_API_KEY", e.apiKey ?? t.MAILGUN_API_KEY),
603
- domain: r("MAILGUN_DOMAIN", t.MAILGUN_DOMAIN),
604
- ...t.MAILGUN_BASE_URL ? { baseUrl: t.MAILGUN_BASE_URL } : {}
605
- });
606
- case "smtp": return t.SMTP_URL ? G({ url: t.SMTP_URL }) : G({
607
- host: r("SMTP_URL/SMTP_HOST", t.SMTP_HOST),
608
- ...t.SMTP_PORT ? { port: Number(t.SMTP_PORT) } : {},
609
- secure: t.SMTP_SECURE === "true",
610
- ...t.SMTP_USER && t.SMTP_PASS ? { auth: {
611
- user: t.SMTP_USER,
612
- pass: t.SMTP_PASS
613
- } } : {}
614
- });
615
- case "memory": return Ge();
616
- default: return He((e, t) => q.info(e, t));
617
- }
618
- }, J = (e) => {
619
- let t = e.attempts ?? 3, r = e.suppression ?? R(), i = e.allowlist, a = e.production !== !0 && i !== void 0 && i.length > 0, s = (e) => i.some((t) => t.toLowerCase() === e.toLowerCase()), c = o.recurs(t).pipe(o.whileInput((e) => e.transient)), l = /* @__PURE__ */ new Map();
620
- for (let t of e.templates ?? []) l.set(t.locale ? `${t.name}:${t.locale}` : t.name, t);
621
- let u = (e, t) => (t ? l.get(`${e}:${t}`) : void 0) ?? l.get(e) ?? j(e, t), d = /* @__PURE__ */ new Map(), f = (e, t) => `${e.name}|${e.locale ?? ""}|${JSON.stringify(t ?? null)}`, p = (e, t) => {
622
- if (d.set(e, t), d.size > 500) {
623
- let e = d.keys().next().value;
624
- e !== void 0 && d.delete(e);
625
- }
626
- }, m = e.idempotency ?? B(), h = xe({
627
- store: e.schedule ?? V(),
628
- providerSchedules: e.provider.supportsScheduling === !0,
629
- flush: (e) => v(e)
630
- }), g = (t) => {
631
- if (t.stream !== "broadcast" || e.provider.managesUnsubscribe === !0) return n.succeed(void 0);
632
- let r = t.unsubscribe;
633
- if (r === void 0 || !r.oneClickUrl.startsWith("https://")) return n.fail(new w({
634
- provider: e.provider.name,
635
- message: `a broadcast needs \`unsubscribe: { oneClickUrl }\` (an https URL) — '${e.provider.name}' does not unsubscribe recipients itself`,
636
- transient: !1
637
- }));
638
- let i = [`<${r.oneClickUrl}>`, ...r.mailto === void 0 ? [] : [`<mailto:${r.mailto.replace(/^mailto:/, "")}>`]];
639
- return n.succeed({
640
- "List-Unsubscribe": i.join(", "),
641
- "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
642
- });
643
- }, _ = (t) => n.gen(function* () {
644
- let i = t.from ?? e.from;
645
- if (!i) return yield* n.fail(new w({
646
- provider: e.provider.name,
647
- message: "no `from` — set mailPlugin({ from }) or pass message.from",
648
- transient: !1
649
- }));
650
- let o = D(t.to);
651
- if (a) {
652
- let n = o.filter(s);
653
- if (n.length === 0) return q.warn("mail skipped — no recipient in allowlist (non-production)", {
654
- to: o.join(","),
655
- subject: t.subject
656
- }), e.onSent?.({
657
- id: "skipped-allowlist",
658
- to: o,
659
- from: i,
660
- subject: t.subject,
661
- provider: "skipped",
662
- ok: !0,
663
- ts: Date.now()
664
- }), {
665
- kind: "skip",
666
- result: {
667
- id: "skipped-allowlist",
668
- provider: "skipped"
669
- }
670
- };
671
- o = n;
672
- }
673
- let c = t.tenantId ?? null, l = [];
674
- for (let e of o) (yield* r.isSuppressed(c, e)) || l.push(e);
675
- if (l.length === 0) return q.warn("mail skipped — all recipients suppressed", {
676
- to: o.join(","),
677
- subject: t.subject
678
- }), e.onSent?.({
679
- id: "skipped-suppressed",
680
- to: o,
681
- from: i,
682
- subject: t.subject,
683
- provider: "suppressed",
684
- ok: !0,
685
- ts: Date.now()
686
- }), {
687
- kind: "skip",
688
- result: {
689
- id: "skipped-suppressed",
690
- provider: "suppressed"
691
- }
692
- };
693
- o = l;
694
- let u = t.replyTo ?? e.replyTo, d = yield* g(t), f = d === void 0 ? t.headers : {
695
- ...t.headers,
696
- ...d
697
- };
698
- return {
699
- kind: "send",
700
- message: {
701
- ...t,
702
- from: i,
703
- to: o,
704
- ...u === void 0 ? {} : { replyTo: u },
705
- ...f === void 0 ? {} : { headers: f }
706
- }
707
- };
708
- }), ee = (t) => {
709
- let r = D(t.to);
710
- return e.provider.send(t).pipe(n.retry(c), n.tap((i) => n.sync(() => {
711
- q.info("mail sent", {
712
- provider: i.provider,
713
- id: i.id,
714
- to: r.join(",")
715
- }), e.onSent?.({
716
- id: i.id,
717
- to: r,
718
- from: t.from,
719
- subject: t.subject,
720
- provider: i.provider,
721
- ok: !0,
722
- ts: Date.now()
723
- });
724
- })), n.tapError((i) => n.sync(() => {
725
- q.warn("mail failed", {
726
- provider: i.provider,
727
- status: i.status,
728
- message: i.message
729
- }), e.onSent?.({
730
- id: `err-${Date.now()}`,
731
- to: r,
732
- from: t.from,
733
- subject: t.subject,
734
- provider: i.provider,
735
- ok: !1,
736
- error: i.message,
737
- ts: Date.now()
738
- });
739
- })));
740
- }, v = (t) => n.gen(function* () {
741
- let n = yield* _(t);
742
- return n.kind === "skip" ? n.result : h.shouldHold(t.scheduledAt) ? (yield* h.hold(n.message), q.info("mail scheduled (held for later flush)", {
743
- subject: n.message.subject,
744
- dueAt: t.scheduledAt?.toISOString()
745
- }), e.onSent?.({
746
- id: "scheduled",
747
- to: D(n.message.to),
748
- from: n.message.from,
749
- subject: n.message.subject,
750
- provider: "scheduled",
751
- ok: !0,
752
- ts: Date.now()
753
- }), {
754
- id: "scheduled",
755
- provider: "scheduled"
756
- }) : yield* ee(n.message);
757
- }), y = (e, t, r) => t === void 0 ? r : n.gen(function* () {
758
- let n = yield* m.reserve(e, t);
759
- if (!n.won) return q.info("mail idempotent replay — skipped", { key: t }), n.prior ?? {
760
- id: "idempotent-replay",
761
- provider: "idempotent"
762
- };
763
- let i = yield* r;
764
- return yield* m.record(e, t, i), i;
765
- }), b = (e) => n.suspend(() => {
766
- let t = u(e.template, e.locale);
767
- if (!t) return n.fail(new w({
768
- provider: "template",
769
- message: `unknown template '${e.template}'${e.locale ? ` (locale '${e.locale}')` : ""}`,
770
- transient: !1
771
- }));
772
- let r = f(t, e.props), i = d.get(r);
773
- return (i ? n.succeed(i) : k(t, e.props).pipe(n.tap((e) => n.sync(() => p(r, e))))).pipe(n.map((t) => ({
774
- to: e.to,
775
- subject: t.subject,
776
- html: t.html,
777
- text: t.text,
778
- ...e.from === void 0 ? {} : { from: e.from },
779
- ...e.replyTo === void 0 ? {} : { replyTo: e.replyTo },
780
- ...e.cc === void 0 ? {} : { cc: e.cc },
781
- ...e.bcc === void 0 ? {} : { bcc: e.bcc },
782
- ...e.headers === void 0 ? {} : { headers: e.headers },
783
- ...e.tags === void 0 ? {} : { tags: e.tags },
784
- ...e.attachments === void 0 ? {} : { attachments: e.attachments },
785
- ...e.tenantId === void 0 ? {} : { tenantId: e.tenantId },
786
- ...e.scheduledAt === void 0 ? {} : { scheduledAt: e.scheduledAt },
787
- ...e.idempotencyKey === void 0 ? {} : { idempotencyKey: e.idempotencyKey },
788
- ...e.stream === void 0 ? {} : { stream: e.stream },
789
- ...e.unsubscribe === void 0 ? {} : { unsubscribe: e.unsubscribe },
790
- ...e.metadata === void 0 ? {} : { metadata: e.metadata }
791
- })));
792
- }), x = (e) => "template" in e ? b(e) : n.succeed(e);
793
- return {
794
- service: {
795
- send: (e) => n.gen(function* () {
796
- let t = yield* x(e);
797
- return yield* y(t.tenantId ?? null, t.idempotencyKey, v(t));
798
- }),
799
- sendBatch: (t) => n.gen(function* () {
800
- let r = yield* n.forEach(t, x), i = r.some((e) => e.idempotencyKey !== void 0 || h.shouldHold(e.scheduledAt));
801
- if (e.provider.sendBatch && !i) {
802
- let t = [], i = [];
803
- for (let e of r) {
804
- let n = yield* _(e);
805
- n.kind === "skip" ? i.push(n.result) : (i.push(t.length), t.push(n.message));
806
- }
807
- let a = t.length > 0 ? yield* e.provider.sendBatch(t).pipe(n.retry(c), n.tap((r) => n.sync(() => {
808
- t.forEach((t, n) => {
809
- let i = r[n];
810
- i && (q.info("mail sent (batch)", {
811
- provider: i.provider,
812
- id: i.id,
813
- to: D(t.to).join(",")
814
- }), e.onSent?.({
815
- id: i.id,
816
- to: D(t.to),
817
- from: t.from,
818
- subject: t.subject,
819
- provider: i.provider,
820
- ok: !0,
821
- ts: Date.now()
822
- }));
823
- });
824
- })), n.tapError((e) => n.sync(() => q.warn("mail batch failed", {
825
- provider: e.provider,
826
- status: e.status,
827
- message: e.message
828
- })))) : [];
829
- return i.map((t) => typeof t == "number" ? a[t] ?? {
830
- id: "batch",
831
- provider: e.provider.name
832
- } : t);
833
- }
834
- return yield* n.forEach(r, (e) => y(e.tenantId ?? null, e.idempotencyKey, v(e)));
835
- }),
836
- suppress: r.suppress,
837
- unsuppress: r.unsuppress,
838
- isSuppressed: r.isSuppressed,
839
- provider: {
840
- name: e.provider.name,
841
- managesUnsubscribe: e.provider.managesUnsubscribe === !0
842
- }
843
- },
844
- scheduler: h
845
- };
846
- }, qe = (e) => J(e).service, Je = (e) => i.succeed(E, qe(e)), Y = (e, t, n, r, i, a = {}) => {
847
- if (!t) return null;
848
- let o = a.metadata !== null && typeof a.metadata == "object" ? Object.fromEntries(Object.entries(a.metadata).filter(([, e]) => typeof e == "string")) : void 0;
849
- return {
850
- type: e,
851
- email: t,
852
- provider: n,
853
- raw: i,
854
- ...r === void 0 ? {} : { messageId: r },
855
- ...a.eventId === void 0 ? {} : { eventId: a.eventId },
856
- ...o === void 0 ? {} : { metadata: o }
857
- };
858
- }, Ye = (e) => e === "message.hard_bounced" ? "bounced" : e === "message.spam_complaint" ? "complained" : e === "message.unsubscribed" ? "unsubscribed" : e === "message.delivered" ? "delivered" : e === "message.opened" ? "opened" : e === "message.clicked" ? "clicked" : "unknown", Xe = (e) => e === "hard_bounce" ? "bounced" : e === "spam_complaint" ? "complained" : e === "unsubscribe" ? "unsubscribed" : "unknown", Ze = (e) => e === "email.bounced" ? "bounced" : e === "email.complained" ? "complained" : e === "email.delivered" ? "delivered" : e === "email.opened" ? "opened" : e === "email.clicked" ? "clicked" : "unknown", Qe = (e) => e === "Bounce" ? "bounced" : e === "SpamComplaint" ? "complained" : e === "Delivery" ? "delivered" : e === "Open" ? "opened" : e === "Click" ? "clicked" : "unknown", $e = (e) => e === "bounce" || e === "dropped" ? "bounced" : e === "spamreport" ? "complained" : e === "delivered" ? "delivered" : e === "open" ? "opened" : e === "click" ? "clicked" : "unknown", X = (e, t) => {
859
- let n = [], r = (e) => {
860
- e && n.push(e);
861
- };
862
- if (e === "sendgrid" && Array.isArray(t)) {
863
- for (let e of t) r(Y($e(String(e.event)), e.email, "sendgrid", e.sg_message_id, e));
864
- return n;
865
- }
866
- let i = t ?? {};
867
- if (e === "lettermint") {
868
- let e = String(i.event), a = i.data ?? {}, o = typeof i.id == "string" ? i.id : void 0;
869
- return e === "suppression.added" ? (a.type === "email" && r(Y(Xe(String(a.reason)), a.value, "lettermint", void 0, t, { eventId: o })), n) : e === "suppression.removed" ? (a.type === "email" && r(Y("unsuppressed", a.value, "lettermint", void 0, t, { eventId: o })), n) : (e.startsWith("message.") && r(Y(Ye(e), a.recipient, "lettermint", a.message_id, t, {
870
- eventId: o,
871
- metadata: a.metadata
872
- })), n);
873
- }
874
- if (e === "resend") {
875
- let e = i.data ?? {}, a = Array.isArray(e.to) ? e.to[0] : e.to ?? e.email;
876
- return r(Y(Ze(String(i.type)), a, "resend", e.email_id, t)), n;
877
- }
878
- if (e === "postmark") {
879
- let e = i.Email ?? i.Recipient;
880
- return r(Y(Qe(String(i.RecordType)), e, "postmark", i.MessageID, t, { metadata: i.Metadata })), n;
881
- }
882
- return n;
883
- }, et = (e, t, r, i = null) => n.gen(function* () {
884
- let n = X(e, t);
885
- for (let e of n) e.type === "bounced" || e.type === "complained" ? yield* r.suppress(i, e.email, e.type) : e.type === "unsuppressed" && (yield* r.unsuppress(i, e.email));
886
- return n;
887
- }), tt = (e = 200) => {
888
- let t = [];
889
- return {
890
- record: (n) => {
891
- t.push(n), t.length > e && t.splice(0, t.length - e);
892
- },
893
- list: () => [...t].reverse(),
894
- clear: () => {
895
- t.length = 0;
896
- }
897
- };
898
- }, nt = {
899
- lettermint: "api.lettermint.co",
900
- resend: "api.resend.com",
901
- postmark: "api.postmarkapp.com",
902
- sendgrid: "api.sendgrid.com"
903
- }, rt = (e) => {
904
- try {
905
- return new URL(e).host || void 0;
906
- } catch {
907
- return;
908
- }
909
- }, it = (e) => {
910
- let t = e.env ?? {}, n = (e) => [{
911
- destination: e,
912
- purpose: "outgoing email: recipients, subject and body",
913
- data: ["personal", "content"],
914
- disable: "MAIL_PROVIDER=console (or memory) logs mail instead of sending it — none leaves"
915
- }];
916
- if (e.provider !== void 0 && typeof e.provider != "string") {
917
- let t = e.provider.name;
918
- return t === "console" || t === "memory" ? [] : n(`runtime:the '${t}' mail provider`);
919
- }
920
- let r = e.provider ?? t.MAIL_PROVIDER ?? "console", i = nt[r];
921
- if (i !== void 0) return n(i);
922
- switch (r) {
923
- case "ses": return n(t.AWS_REGION ? `email.${t.AWS_REGION}.amazonaws.com` : "runtime:email.<AWS_REGION>.amazonaws.com");
924
- case "mailgun": return n((t.MAILGUN_BASE_URL ? rt(t.MAILGUN_BASE_URL) : void 0) ?? "api.mailgun.net");
925
- case "smtp": return n((t.SMTP_URL ? rt(t.SMTP_URL) : void 0) ?? (t.SMTP_HOST ? `${t.SMTP_HOST}${t.SMTP_PORT ? `:${t.SMTP_PORT}` : ""}` : void 0) ?? "runtime:SMTP_URL or SMTP_HOST");
926
- default: return [];
927
- }
928
- }, Z = (e, t) => {
929
- if (t > 6) return null;
930
- switch (e._tag) {
931
- case "StringKeyword": return "";
932
- case "NumberKeyword": return 0;
933
- case "BooleanKeyword": return !1;
934
- case "Literal": return e.literal;
935
- case "Enums": return e.enums.length > 0 ? e.enums[0][1] : null;
936
- case "TypeLiteral": {
937
- let n = {};
938
- for (let r of e.propertySignatures) typeof r.name == "string" && (n[r.name] = Z(r.type, t + 1));
939
- return n;
940
- }
941
- case "TupleType": return [];
942
- case "Union": return e.types.length > 0 ? Z(e.types[0], t + 1) : null;
943
- case "Refinement": return Z(e.from, t + 1);
944
- case "Transformation": return Z(e.from, t + 1);
945
- case "Suspend": return Z(e.f(), t + 1);
946
- default: return null;
947
- }
948
- }, at = (e) => Z(e.ast, 0), Q = u([
1
+ import { MailError as e, MailService as t, MailWebhookError as n, defineEmail as r } from "./types.js";
2
+ import { renderTemplate as i } from "./render.js";
3
+ import { clearGlobalTemplates as a, globalTemplateCount as o, isEmailTemplate as s, listGlobalTemplates as c, lookupGlobalTemplate as l, registerEmailTemplates as u } from "./registry.js";
4
+ import { MAIL_IDEMPOTENCY_TABLE as ee, mailIdempotencyTable as d, mailSuppressionTable as f } from "./storeTables.js";
5
+ import { memorySuppressionStore as p, postgresSuppressionStore as m } from "./suppression.js";
6
+ import { memoryIdempotencyStore as h, postgresIdempotencyStore as g } from "./idempotency.js";
7
+ import { heldId as _, makeScheduler as v, memoryScheduleStore as y } from "./scheduler.js";
8
+ import { clearMailBuffer as b, consoleProvider as x, lettermintProvider as S, mailgunProvider as C, memoryProvider as w, postmarkProvider as T, readMailBuffer as E, resendProvider as D, sendgridProvider as O, sesProvider as k, smtpProvider as A, verifyLettermintSignature as j } from "./providers.js";
9
+ import { buildMailService as M, buildMailServiceWithScheduler as N, mailServiceLayer as P, resolveProvider as F } from "./service.js";
10
+ import { handleMailEvents as I, parseMailEvent as L } from "./events.js";
11
+ import { createOutbox as R } from "./outbox.js";
12
+ import { mailEgressOf as z } from "./egress.js";
13
+ import { schemaSkeleton as te } from "./skeleton.js";
14
+ import { Duration as B, Effect as V, Fiber as H, Layer as U, Result as W, Schedule as G } from "effect";
15
+ import { definePlugin as K, pluginInstanceName as q } from "@voltro/protocol";
16
+ import { pluginEnv as J } from "@voltro/env";
17
+ import { registerRetention as Y, retentionTtlMsFromEnv as X } from "@voltro/database";
18
+ //#region src/index.ts
19
+ var Z = J([
949
20
  {
950
21
  name: "MAIL_PROVIDER",
951
22
  required: !1,
@@ -1066,28 +137,28 @@ var w = class extends s.TaggedError()("MailError", {
1066
137
  secret: !1,
1067
138
  description: "Comma-separated recipient allowlist; in dev, mail to addresses outside it is skipped."
1068
139
  }
1069
- ]), ot = "@voltro/plugin-mail", $ = (e, t) => ({
140
+ ]), Q = "@voltro/plugin-mail", $ = (e, t) => ({
1070
141
  status: e,
1071
142
  contentType: "application/json",
1072
143
  body: JSON.stringify(t)
1073
- }), st = (e, t, r, i) => {
1074
- let a = e.webhook;
1075
- if (a === void 0) throw Error(`@voltro/plugin-mail: provider '${e.name}' has no webhook verifier — mailPlugin({ webhook }) needs one (lettermint)`);
1076
- let o = r.secret ?? Q.read("MAIL_WEBHOOK_SECRET");
1077
- if (!o && i) throw Error("@voltro/plugin-mail: mailPlugin({ webhook }) needs a signing secret in production — set MAIL_WEBHOOK_SECRET");
144
+ }), ne = (e, t, n, r) => {
145
+ let i = e.webhook;
146
+ if (i === void 0) throw Error(`@voltro/plugin-mail: provider '${e.name}' has no webhook verifier — mailPlugin({ webhook }) needs one (lettermint)`);
147
+ let a = n.secret ?? Z.read("MAIL_WEBHOOK_SECRET");
148
+ if (!a && r) throw Error("@voltro/plugin-mail: mailPlugin({ webhook }) needs a signing secret in production — set MAIL_WEBHOOK_SECRET");
1078
149
  return {
1079
150
  method: "POST",
1080
- path: r.path ?? "/webhooks/mail",
1081
- handle: async (i) => {
1082
- if (!o) return $(503, { error: "mail webhook secret not configured" });
1083
- let s = await n.runPromise(n.either(a.verify(i.headers, new TextDecoder().decode(i.rawBody), o)));
1084
- if (s._tag === "Left") return $(401, { error: s.left.message });
1085
- let c = X(e.name, s.right);
151
+ path: n.path ?? "/webhooks/mail",
152
+ handle: async (r) => {
153
+ if (!a) return $(503, { error: "mail webhook secret not configured" });
154
+ let o = await V.runPromise(V.result(i.verify(r.headers, new TextDecoder().decode(r.rawBody), a)));
155
+ if (W.isFailure(o)) return $(401, { error: o.failure.message });
156
+ let s = L(e.name, o.success);
1086
157
  try {
1087
- for (let e of c) {
1088
- e.type === "bounced" || e.type === "complained" ? await n.runPromise(t.suppress(null, e.email, e.type)) : e.type === "unsuppressed" && await n.runPromise(t.unsuppress(null, e.email));
1089
- let a = r.onEvent?.(e, { store: i.store });
1090
- n.isEffect(a) ? await n.runPromise(a) : await a;
158
+ for (let e of s) {
159
+ e.type === "bounced" || e.type === "complained" ? await V.runPromise(t.suppress(null, e.email, e.type)) : e.type === "unsuppressed" && await V.runPromise(t.unsuppress(null, e.email));
160
+ let i = n.onEvent?.(e, { store: r.store });
161
+ V.isEffect(i) ? await V.runPromise(i) : await i;
1091
162
  }
1092
163
  } catch (e) {
1093
164
  return {
@@ -1095,57 +166,57 @@ var w = class extends s.TaggedError()("MailError", {
1095
166
  cause: e
1096
167
  };
1097
168
  }
1098
- return $(200, { received: c.length });
169
+ return $(200, { received: s.length });
1099
170
  }
1100
171
  };
1101
- }, ct = (e = {}) => {
1102
- let a = process.env, s = Ke({
172
+ }, re = (e = {}) => {
173
+ let n = process.env, r = F({
1103
174
  ...e.provider === void 0 ? {} : { provider: e.provider },
1104
175
  ...e.apiKey === void 0 ? {} : { apiKey: e.apiKey },
1105
- env: a
1106
- }), u = Q.read("MAIL_ALLOWLIST"), d = e.allowlist ?? (u ? u.split(",").map((e) => e.trim()).filter(Boolean) : void 0), f = a.NODE_ENV === "production", p = e.suppression === void 0 || e.suppression === "memory" ? R() : e.suppression === "postgres" ? he() : e.suppression, m = e.idempotency === void 0 || e.idempotency === "memory" ? B() : e.idempotency === "postgres" ? _e() : e.idempotency, h = e.schedule ?? V(), g = [...e.suppression === "postgres" ? [de] : [], ...e.idempotency === "postgres" ? [fe] : []];
1107
- e.idempotency === "postgres" && ee({
176
+ env: n
177
+ }), a = Z.read("MAIL_ALLOWLIST"), o = e.allowlist ?? (a ? a.split(",").map((e) => e.trim()).filter(Boolean) : void 0), s = n.NODE_ENV === "production", u = e.suppression === void 0 || e.suppression === "memory" ? p() : e.suppression === "postgres" ? m() : e.suppression, _ = e.idempotency === void 0 || e.idempotency === "memory" ? h() : e.idempotency === "postgres" ? g() : e.idempotency, v = e.schedule ?? y(), b = [...e.suppression === "postgres" ? [f] : [], ...e.idempotency === "postgres" ? [d] : []];
178
+ e.idempotency === "postgres" && Y({
1108
179
  source: "plugin",
1109
- table: N,
180
+ table: ee,
1110
181
  timeColumn: "createdAt",
1111
- ttlMs: v(process.env.VOLTRO_MAIL_IDEMPOTENCY_TTL_HOURS, 720),
182
+ ttlMs: X(process.env.VOLTRO_MAIL_IDEMPOTENCY_TTL_HOURS, 720),
1112
183
  label: "mail idempotency keys"
1113
184
  });
1114
- let _ = tt(), y = J({
1115
- provider: s,
1116
- suppression: p,
1117
- idempotency: m,
1118
- schedule: h,
1119
- onSent: _.record,
185
+ let x = R(), S = N({
186
+ provider: r,
187
+ suppression: u,
188
+ idempotency: _,
189
+ schedule: v,
190
+ onSent: x.record,
1120
191
  ...e.from === void 0 ? {} : { from: e.from },
1121
192
  ...e.replyTo === void 0 ? {} : { replyTo: e.replyTo },
1122
- ...d === void 0 ? {} : { allowlist: d },
193
+ ...o === void 0 ? {} : { allowlist: o },
1123
194
  ...e.attempts === void 0 ? {} : { attempts: e.attempts },
1124
195
  ...e.templates === void 0 ? {} : { templates: e.templates },
1125
- production: f
1126
- }), b = i.succeed(E, y.service), x = s.supportsScheduling !== !0, te = e.scheduleTickMs ?? 1e3, S, C = /* @__PURE__ */ new Map();
1127
- for (let t of e.templates ?? []) C.set(t.locale ? `${t.name}:${t.locale}` : t.name, t);
1128
- let w = (e, t) => (t ? C.get(`${e}:${t}`) : void 0) ?? C.get(e) ?? j(e, t), T = (e) => ({
196
+ production: s
197
+ }), C = U.succeed(t, S.service), w = r.supportsScheduling !== !0, T = e.scheduleTickMs ?? 1e3, E, D = /* @__PURE__ */ new Map();
198
+ for (let t of e.templates ?? []) D.set(t.locale ? `${t.name}:${t.locale}` : t.name, t);
199
+ let O = (e, t) => (t ? D.get(`${e}:${t}`) : void 0) ?? D.get(e) ?? l(e, t), k = (e) => ({
1129
200
  name: e.name,
1130
201
  ...e.locale ? { locale: e.locale } : {},
1131
- props: e.preview ?? at(e.props)
1132
- }), ne = () => {
202
+ props: e.preview ?? te(e.props)
203
+ }), A = () => {
1133
204
  let t = /* @__PURE__ */ new Map();
1134
- for (let n of e.templates ?? []) t.set(n.locale ? `${n.name}:${n.locale}` : n.name, T(n));
1135
- for (let e of ce()) {
1136
- let n = j(e.name, e.locale);
1137
- n && t.set(e.locale ? `${e.name}:${e.locale}` : e.name, T(n));
205
+ for (let n of e.templates ?? []) t.set(n.locale ? `${n.name}:${n.locale}` : n.name, k(n));
206
+ for (let e of c()) {
207
+ let n = l(e.name, e.locale);
208
+ n && t.set(e.locale ? `${e.name}:${e.locale}` : e.name, k(n));
1138
209
  }
1139
210
  return [...t.values()];
1140
- }, D = e.webhook === void 0 ? [] : [st(s, p, e.webhook, f)], re = [
211
+ }, j = e.webhook === void 0 ? [] : [ne(r, u, e.webhook, s)], M = [
1141
212
  {
1142
213
  method: "GET",
1143
214
  path: "/outbox",
1144
215
  scope: "process",
1145
216
  description: "Recent sent mail (dev capture buffer), newest first",
1146
- handler: () => n.succeed({
217
+ handler: () => V.succeed({
1147
218
  kind: "json",
1148
- data: { sends: _.list() }
219
+ data: { sends: x.list() }
1149
220
  })
1150
221
  },
1151
222
  {
@@ -1153,16 +224,16 @@ var w = class extends s.TaggedError()("MailError", {
1153
224
  path: "/templates",
1154
225
  scope: "declaration",
1155
226
  description: "Discovered + registered email templates",
1156
- handler: () => n.succeed({
227
+ handler: () => V.succeed({
1157
228
  kind: "json",
1158
- data: { templates: ne() }
229
+ data: { templates: A() }
1159
230
  })
1160
231
  },
1161
232
  {
1162
233
  method: "POST",
1163
234
  path: "/preview",
1164
235
  description: "Render a template to html + text without sending — body { template, props, locale? }",
1165
- handler: (e) => n.gen(function* () {
236
+ handler: (e) => V.gen(function* () {
1166
237
  let t = {};
1167
238
  try {
1168
239
  t = JSON.parse(e.body || "{}");
@@ -1178,11 +249,11 @@ var w = class extends s.TaggedError()("MailError", {
1178
249
  status: 400,
1179
250
  data: { error: "missing \"template\"" }
1180
251
  };
1181
- let r = w(t.template, t.locale);
1182
- return r ? yield* k(r, t.props).pipe(n.map((e) => ({
252
+ let n = O(t.template, t.locale);
253
+ return n ? yield* i(n, t.props).pipe(V.map((e) => ({
1183
254
  kind: "json",
1184
255
  data: e
1185
- })), n.catchAll((e) => n.succeed({
256
+ })), V.catch((e) => V.succeed({
1186
257
  kind: "json",
1187
258
  status: 400,
1188
259
  data: { error: e.message }
@@ -1193,55 +264,55 @@ var w = class extends s.TaggedError()("MailError", {
1193
264
  };
1194
265
  })
1195
266
  }
1196
- ], O = [
267
+ ], P = [
1197
268
  "network:outbound:*",
1198
269
  "inspect:read",
1199
270
  "inspect:write",
1200
- ...g.length > 0 ? ["store:write"] : []
271
+ ...b.length > 0 ? ["store:write"] : []
1201
272
  ];
1202
273
  return {
1203
- ...c({
1204
- name: l({
1205
- base: ot,
274
+ ...K({
275
+ name: q({
276
+ base: Q,
1206
277
  alias: e.alias,
1207
278
  instance: e.name
1208
279
  }),
1209
- baseName: ot,
280
+ baseName: Q,
1210
281
  description: "Transactional and broadcast email — MailService over Lettermint / Resend / Postmark / SendGrid / SES / Mailgun / SMTP (+ console/memory), with default-from, transient retry, suppression and a dev allowlist.",
1211
- permissions: O,
1212
- declaredEnv: Q.declared,
1213
- egress: it({
282
+ permissions: P,
283
+ declaredEnv: Z.declared,
284
+ egress: z({
1214
285
  ...e.provider === void 0 ? {} : { provider: e.provider },
1215
- env: a
286
+ env: n
1216
287
  }),
1217
- services: b,
1218
- inspectEndpoints: re,
1219
- ...D.length > 0 ? { httpRoutes: D } : {},
1220
- ...g.length > 0 ? { extendSchema: { tables: g } } : {},
288
+ services: C,
289
+ inspectEndpoints: M,
290
+ ...j.length > 0 ? { httpRoutes: j } : {},
291
+ ...b.length > 0 ? { extendSchema: { tables: b } } : {},
1221
292
  bindDataStore: async (e) => {
1222
- await p.bind?.(e), await m.bind?.(e);
293
+ await u.bind?.(e), await _.bind?.(e);
1223
294
  },
1224
- onActivate: (r) => n.gen(function* () {
1225
- if (p.activate && (yield* n.promise(() => p.activate(r.env))), m.activate && (yield* n.promise(() => m.activate(r.env))), x) {
1226
- let e = y.scheduler.tick().pipe(n.repeat(o.spaced(t.millis(Math.max(50, te)))));
1227
- S = n.runFork(e);
295
+ onActivate: (t) => V.gen(function* () {
296
+ if (u.activate && (yield* V.promise(() => u.activate(t.env))), _.activate && (yield* V.promise(() => _.activate(t.env))), w) {
297
+ let e = S.scheduler.tick().pipe(V.repeat(G.spaced(B.millis(Math.max(50, T)))));
298
+ E = V.runFork(e);
1228
299
  }
1229
- r.logger.info("mail active", {
1230
- provider: s.name,
300
+ t.logger.info("mail active", {
301
+ provider: r.name,
1231
302
  from: e.from ?? null,
1232
- allowlist: d?.length ?? 0,
303
+ allowlist: o?.length ?? 0,
1233
304
  suppression: typeof e.suppression == "string" ? e.suppression : e.suppression ? "custom" : "memory",
1234
305
  idempotency: typeof e.idempotency == "string" ? e.idempotency : e.idempotency ? "custom" : "memory",
1235
- scheduling: s.supportsScheduling === !0 ? "native" : "held",
1236
- production: f
306
+ scheduling: r.supportsScheduling === !0 ? "native" : "held",
307
+ production: s
1237
308
  });
1238
309
  }),
1239
- onDeactivate: () => n.gen(function* () {
1240
- S !== void 0 && (yield* r.interrupt(S), S = void 0), p.deactivate && (yield* n.promise(() => p.deactivate())), m.deactivate && (yield* n.promise(() => m.deactivate()));
310
+ onDeactivate: () => V.gen(function* () {
311
+ E !== void 0 && (yield* H.interrupt(E), E = void 0), u.deactivate && (yield* V.promise(() => u.deactivate())), _.deactivate && (yield* V.promise(() => _.deactivate()));
1241
312
  })
1242
313
  }),
1243
- service: y.service
314
+ service: S.service
1244
315
  };
1245
316
  };
1246
317
  //#endregion
1247
- export { w as MailError, E as MailService, T as MailWebhookError, qe as buildMailService, J as buildMailServiceWithScheduler, le as clearGlobalTemplates, We as clearMailBuffer, He as consoleProvider, tt as createOutbox, ne as defineEmail, se as globalTemplateCount, et as handleMailEvents, be as heldId, ue as isEmailTemplate, De as lettermintProvider, ce as listGlobalTemplates, j as lookupGlobalTemplate, it as mailEgressOf, ct as mailPlugin, Je as mailServiceLayer, Ve as mailgunProvider, xe as makeScheduler, B as memoryIdempotencyStore, Ge as memoryProvider, V as memoryScheduleStore, R as memorySuppressionStore, X as parseMailEvent, _e as postgresIdempotencyStore, he as postgresSuppressionStore, Ae as postmarkProvider, Ue as readMailBuffer, oe as registerEmailTemplates, k as renderTemplate, ke as resendProvider, Ke as resolveProvider, Pe as sendgridProvider, ze as sesProvider, G as smtpProvider, Ee as verifyLettermintSignature };
318
+ export { e as MailError, t as MailService, n as MailWebhookError, M as buildMailService, N as buildMailServiceWithScheduler, a as clearGlobalTemplates, b as clearMailBuffer, x as consoleProvider, R as createOutbox, r as defineEmail, o as globalTemplateCount, I as handleMailEvents, _ as heldId, s as isEmailTemplate, S as lettermintProvider, c as listGlobalTemplates, l as lookupGlobalTemplate, z as mailEgressOf, re as mailPlugin, P as mailServiceLayer, C as mailgunProvider, v as makeScheduler, h as memoryIdempotencyStore, w as memoryProvider, y as memoryScheduleStore, p as memorySuppressionStore, L as parseMailEvent, g as postgresIdempotencyStore, m as postgresSuppressionStore, T as postmarkProvider, E as readMailBuffer, u as registerEmailTemplates, i as renderTemplate, D as resendProvider, F as resolveProvider, O as sendgridProvider, k as sesProvider, A as smtpProvider, j as verifyLettermintSignature };