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