@voltro/plugin-mail 0.86.0 → 0.87.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.
@@ -41,7 +41,7 @@ var s = i({ scope: "@voltro/plugin-mail" }), c = (e) => e ?? "*", l = () => {
41
41
  }), r.catch((e) => r.sync(() => (s.warn("idempotency reserve failed — skipping send to avoid a double", {
42
42
  key: o,
43
43
  message: n(e)
44
- }), { won: !1 })))) : r.succeed({ won: !0 });
44
+ }), { won: !1 })))) : r.succeed({ won: !1 });
45
45
  },
46
46
  record: (l, u, d) => {
47
47
  let f = i;
@@ -57,6 +57,19 @@ var s = i({ scope: "@voltro/plugin-mail" }), c = (e) => e ?? "*", l = () => {
57
57
  i = e;
58
58
  }
59
59
  };
60
- };
60
+ }, d = (n, i, s) => t(() => n.query({
61
+ table: e,
62
+ predicate: a(o("tenant", c(i)), o("key", s)),
63
+ projection: ["resultId", "resultProvider"],
64
+ order: [],
65
+ take: 1,
66
+ skip: void 0
67
+ })).pipe(r.map((e) => {
68
+ let t = e[0], n = t?.resultId, r = t?.resultProvider;
69
+ return typeof n == "string" && n !== "" && typeof r == "string" && r !== "" ? {
70
+ id: n,
71
+ provider: r
72
+ } : null;
73
+ }), r.catch(() => r.succeed(null)));
61
74
  //#endregion
62
- export { l as memoryIdempotencyStore, u as postgresIdempotencyStore };
75
+ export { l as memoryIdempotencyStore, u as postgresIdempotencyStore, d as readMailReceipt };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
+ import { ColumnBuilder } from '@voltro/database';
1
2
  import { Context } from 'effect';
2
3
  import { DataStore } from '@voltro/database';
3
4
  import { Effect } from 'effect';
5
+ import { FieldDefinitions } from '@voltro/database';
4
6
  import { Layer } from 'effect';
5
7
  import { PluginEgressDeclaration } from '@voltro/protocol';
6
8
  import { PluginHttpRouteRequest } from '@voltro/protocol';
7
9
  import { Schema } from 'effect';
10
+ import { Table } from '@voltro/database';
8
11
  import { VoltroPlugin } from '@voltro/protocol';
9
12
  import { YieldableError } from 'effect/Cause';
10
13
 
@@ -215,6 +218,17 @@ export declare interface MailgunOptions {
215
218
 
216
219
  export declare const mailgunProvider: (options: MailgunOptions) => MailProvider;
217
220
 
221
+ /** One row per idempotency key: the reservation, then the winning send's result. */
222
+ export declare const mailIdempotencyTable: Table<"_voltro_mail_idempotency", FieldDefinitions<{
223
+ readonly id: ColumnBuilder<number, "id", boolean>;
224
+ readonly tenant: ColumnBuilder<string, "text", boolean>;
225
+ readonly key: ColumnBuilder<string, "text", boolean>;
226
+ readonly resultId: ColumnBuilder<string | null, "text", boolean>;
227
+ readonly resultProvider: ColumnBuilder<string | null, "text", boolean>;
228
+ readonly createdAt: ColumnBuilder<Date, "timestamp", true>;
229
+ readonly created_at: ColumnBuilder<unknown, "text", boolean>;
230
+ }>, false, never>;
231
+
218
232
  /** A message to send. `from` is optional here — the service fills it
219
233
  * from the plugin's configured default before handing it to a provider. */
220
234
  export declare interface MailMessage {
@@ -245,6 +259,10 @@ export declare interface MailMessage {
245
259
  * step. The first send with a given key delivers; a replay with the same
246
260
  * key is a no-op returning the recorded result. Scoped per tenant. */
247
261
  readonly idempotencyKey?: string;
262
+ /** Override the service's transient retry count for this message. Zero means
263
+ * one provider dispatch, for callers that durably reconcile unknown outcomes.
264
+ * This is not a provider-side delivery/idempotency guarantee. */
265
+ readonly retryAttempts?: number;
248
266
  /** `transactional` (default) or `broadcast`. See `MailStream`. */
249
267
  readonly stream?: MailStream;
250
268
  /** How a recipient leaves a broadcast. See `MailUnsubscribe`. */
@@ -447,6 +465,17 @@ export declare interface MailServiceShape {
447
465
  */
448
466
  export declare type MailStream = 'transactional' | 'broadcast';
449
467
 
468
+ /** One row per suppressed (tenant, address). Kept until lifted — a bounce or a
469
+ * complaint does not expire, so this table has no retention policy. */
470
+ export declare const mailSuppressionTable: Table<"_voltro_mail_suppression", FieldDefinitions<{
471
+ readonly id: ColumnBuilder<number, "id", boolean>;
472
+ readonly tenant: ColumnBuilder<string, "text", boolean>;
473
+ readonly email: ColumnBuilder<string, "text", boolean>;
474
+ readonly reason: ColumnBuilder<string | null, "text", boolean>;
475
+ readonly createdAt: ColumnBuilder<Date, "timestamp", true>;
476
+ readonly created_at: ColumnBuilder<unknown, "text", boolean>;
477
+ }>, false, never>;
478
+
450
479
  /**
451
480
  * How a recipient leaves a broadcast. `oneClickUrl` is the HTTPS endpoint a
452
481
  * mail client POSTs `List-Unsubscribe=One-Click` to (RFC 8058); the service
@@ -543,6 +572,11 @@ export declare const postmarkProvider: (options: {
543
572
  /** Stable snapshot of everything the memory provider has "sent". */
544
573
  export declare const readMailBuffer: () => ReadonlyArray<ResolvedMessage>;
545
574
 
575
+ /** Read a recorded persistent receipt without reserving a key or dispatching.
576
+ * Missing, pending, unbound or unavailable storage is not delivery evidence.
577
+ * The server caller owns authorization for the requested tenant/key. */
578
+ export declare const readMailReceipt: (store: DataStore, tenantId: string | null, key: string) => Effect.Effect<SendResult | null>;
579
+
546
580
  /** Register CLI-discovered (or hand-collected) templates. Idempotent per key. */
547
581
  export declare const registerEmailTemplates: (templates: ReadonlyArray<AnyEmailTemplate>) => void;
548
582
 
@@ -697,6 +731,10 @@ export declare interface TemplateMessage {
697
731
  readonly scheduledAt?: Date;
698
732
  /** See `MailMessage.idempotencyKey`. */
699
733
  readonly idempotencyKey?: string;
734
+ /** Override the service's transient retry count for this message. Zero means
735
+ * one provider dispatch, for callers that durably reconcile unknown outcomes.
736
+ * This is not a provider-side delivery/idempotency guarantee. */
737
+ readonly retryAttempts?: number;
700
738
  /** See `MailMessage.stream`. */
701
739
  readonly stream?: MailStream;
702
740
  /** See `MailMessage.unsubscribe`. */
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { MailError as e, MailService as t, MailWebhookError as n, defineEmail as r } from "./types.js";
2
2
  import { renderTemplate as i } from "./render.js";
3
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";
4
+ import { MAIL_IDEMPOTENCY_TABLE as d, mailIdempotencyTable as f, mailSuppressionTable as p } from "./storeTables.js";
5
+ import { memorySuppressionStore as m, postgresSuppressionStore as h } from "./suppression.js";
6
+ import { memoryIdempotencyStore as g, postgresIdempotencyStore as _, readMailReceipt as v } from "./idempotency.js";
7
+ import { heldId as y, makeScheduler as b, memoryScheduleStore as x } from "./scheduler.js";
8
+ import { clearMailBuffer as S, consoleProvider as C, lettermintProvider as w, mailgunProvider as T, memoryProvider as E, postmarkProvider as D, readMailBuffer as O, resendProvider as k, sendgridProvider as A, sesProvider as j, smtpProvider as M, verifyLettermintSignature as N } from "./providers.js";
9
+ import { buildMailService as P, buildMailServiceWithScheduler as F, mailServiceLayer as I, resolveProvider as L } from "./service.js";
10
+ import { handleMailEvents as ee, parseMailEvent as R } from "./events.js";
11
+ import { createOutbox as z } from "./outbox.js";
12
+ import { mailEgressOf as B } from "./egress.js";
13
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";
14
+ import { Duration as ne, Effect as V, Fiber as H, Layer as U, Result as W, Schedule as G } from "effect";
15
15
  import { definePlugin as K, pluginInstanceName as q } from "@voltro/protocol";
16
16
  import { pluginEnv as J } from "@voltro/env";
17
17
  import { registerRetention as Y, retentionTtlMsFromEnv as X } from "@voltro/database";
@@ -141,7 +141,7 @@ var Z = J([
141
141
  status: e,
142
142
  contentType: "application/json",
143
143
  body: JSON.stringify(t)
144
- }), ne = (e, t, n, r) => {
144
+ }), re = (e, t, n, r) => {
145
145
  let i = e.webhook;
146
146
  if (i === void 0) throw Error(`@voltro/plugin-mail: provider '${e.name}' has no webhook verifier — mailPlugin({ webhook }) needs one (lettermint)`);
147
147
  let a = n.secret ?? Z.read("MAIL_WEBHOOK_SECRET");
@@ -153,7 +153,7 @@ var Z = J([
153
153
  if (!a) return $(503, { error: "mail webhook secret not configured" });
154
154
  let o = await V.runPromise(V.result(i.verify(r.headers, new TextDecoder().decode(r.rawBody), a)));
155
155
  if (W.isFailure(o)) return $(401, { error: o.failure.message });
156
- let s = L(e.name, o.success);
156
+ let s = R(e.name, o.success);
157
157
  try {
158
158
  for (let e of s) {
159
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));
@@ -169,46 +169,46 @@ var Z = J([
169
169
  return $(200, { received: s.length });
170
170
  }
171
171
  };
172
- }, re = (e = {}) => {
173
- let n = process.env, r = F({
172
+ }, ie = (e = {}) => {
173
+ let n = process.env, r = L({
174
174
  ...e.provider === void 0 ? {} : { provider: e.provider },
175
175
  ...e.apiKey === void 0 ? {} : { apiKey: e.apiKey },
176
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] : []];
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" ? m() : e.suppression === "postgres" ? h() : e.suppression, v = e.idempotency === void 0 || e.idempotency === "memory" ? g() : e.idempotency === "postgres" ? _() : e.idempotency, y = e.schedule ?? x(), b = [...e.suppression === "postgres" ? [p] : [], ...e.idempotency === "postgres" ? [f] : []];
178
178
  e.idempotency === "postgres" && Y({
179
179
  source: "plugin",
180
- table: ee,
180
+ table: d,
181
181
  timeColumn: "createdAt",
182
182
  ttlMs: X(process.env.VOLTRO_MAIL_IDEMPOTENCY_TTL_HOURS, 720),
183
183
  label: "mail idempotency keys"
184
184
  });
185
- let x = R(), S = N({
185
+ let S = z(), C = F({
186
186
  provider: r,
187
187
  suppression: u,
188
- idempotency: _,
189
- schedule: v,
190
- onSent: x.record,
188
+ idempotency: v,
189
+ schedule: y,
190
+ onSent: S.record,
191
191
  ...e.from === void 0 ? {} : { from: e.from },
192
192
  ...e.replyTo === void 0 ? {} : { replyTo: e.replyTo },
193
193
  ...o === void 0 ? {} : { allowlist: o },
194
194
  ...e.attempts === void 0 ? {} : { attempts: e.attempts },
195
195
  ...e.templates === void 0 ? {} : { templates: e.templates },
196
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) => ({
197
+ }), w = U.succeed(t, C.service), T = r.supportsScheduling !== !0, E = e.scheduleTickMs ?? 1e3, D, O = /* @__PURE__ */ new Map();
198
+ for (let t of e.templates ?? []) O.set(t.locale ? `${t.name}:${t.locale}` : t.name, t);
199
+ let k = (e, t) => (t ? O.get(`${e}:${t}`) : void 0) ?? O.get(e) ?? l(e, t), A = (e) => ({
200
200
  name: e.name,
201
201
  ...e.locale ? { locale: e.locale } : {},
202
202
  props: e.preview ?? te(e.props)
203
- }), A = () => {
203
+ }), j = () => {
204
204
  let t = /* @__PURE__ */ new Map();
205
- for (let n of e.templates ?? []) t.set(n.locale ? `${n.name}:${n.locale}` : n.name, k(n));
205
+ for (let n of e.templates ?? []) t.set(n.locale ? `${n.name}:${n.locale}` : n.name, A(n));
206
206
  for (let e of c()) {
207
207
  let n = l(e.name, e.locale);
208
- n && t.set(e.locale ? `${e.name}:${e.locale}` : e.name, k(n));
208
+ n && t.set(e.locale ? `${e.name}:${e.locale}` : e.name, A(n));
209
209
  }
210
210
  return [...t.values()];
211
- }, j = e.webhook === void 0 ? [] : [ne(r, u, e.webhook, s)], M = [
211
+ }, M = e.webhook === void 0 ? [] : [re(r, u, e.webhook, s)], N = [
212
212
  {
213
213
  method: "GET",
214
214
  path: "/outbox",
@@ -216,7 +216,7 @@ var Z = J([
216
216
  description: "Recent sent mail (dev capture buffer), newest first",
217
217
  handler: () => V.succeed({
218
218
  kind: "json",
219
- data: { sends: x.list() }
219
+ data: { sends: S.list() }
220
220
  })
221
221
  },
222
222
  {
@@ -226,7 +226,7 @@ var Z = J([
226
226
  description: "Discovered + registered email templates",
227
227
  handler: () => V.succeed({
228
228
  kind: "json",
229
- data: { templates: A() }
229
+ data: { templates: j() }
230
230
  })
231
231
  },
232
232
  {
@@ -249,7 +249,7 @@ var Z = J([
249
249
  status: 400,
250
250
  data: { error: "missing \"template\"" }
251
251
  };
252
- let n = O(t.template, t.locale);
252
+ let n = k(t.template, t.locale);
253
253
  return n ? yield* i(n, t.props).pipe(V.map((e) => ({
254
254
  kind: "json",
255
255
  data: e
@@ -281,21 +281,21 @@ var Z = J([
281
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.",
282
282
  permissions: P,
283
283
  declaredEnv: Z.declared,
284
- egress: z({
284
+ egress: B({
285
285
  ...e.provider === void 0 ? {} : { provider: e.provider },
286
286
  env: n
287
287
  }),
288
- services: C,
289
- inspectEndpoints: M,
290
- ...j.length > 0 ? { httpRoutes: j } : {},
288
+ services: w,
289
+ inspectEndpoints: N,
290
+ ...M.length > 0 ? { httpRoutes: M } : {},
291
291
  ...b.length > 0 ? { extendSchema: { tables: b } } : {},
292
292
  bindDataStore: async (e) => {
293
- await u.bind?.(e), await _.bind?.(e);
293
+ await u.bind?.(e), await v.bind?.(e);
294
294
  },
295
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);
296
+ if (u.activate && (yield* V.promise(() => u.activate(t.env))), v.activate && (yield* V.promise(() => v.activate(t.env))), T) {
297
+ let e = C.scheduler.tick().pipe(V.repeat(G.spaced(ne.millis(Math.max(50, E)))));
298
+ D = V.runFork(e);
299
299
  }
300
300
  t.logger.info("mail active", {
301
301
  provider: r.name,
@@ -308,11 +308,11 @@ var Z = J([
308
308
  });
309
309
  }),
310
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()));
311
+ D !== void 0 && (yield* H.interrupt(D), D = void 0), u.deactivate && (yield* V.promise(() => u.deactivate())), v.deactivate && (yield* V.promise(() => v.deactivate()));
312
312
  })
313
313
  }),
314
- service: S.service
314
+ service: C.service
315
315
  };
316
316
  };
317
317
  //#endregion
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 };
318
+ export { e as MailError, t as MailService, n as MailWebhookError, P as buildMailService, F as buildMailServiceWithScheduler, a as clearGlobalTemplates, S as clearMailBuffer, C as consoleProvider, z as createOutbox, r as defineEmail, o as globalTemplateCount, ee as handleMailEvents, y as heldId, s as isEmailTemplate, w as lettermintProvider, c as listGlobalTemplates, l as lookupGlobalTemplate, B as mailEgressOf, f as mailIdempotencyTable, ie as mailPlugin, I as mailServiceLayer, p as mailSuppressionTable, T as mailgunProvider, b as makeScheduler, g as memoryIdempotencyStore, E as memoryProvider, x as memoryScheduleStore, m as memorySuppressionStore, R as parseMailEvent, _ as postgresIdempotencyStore, h as postgresSuppressionStore, D as postmarkProvider, O as readMailBuffer, v as readMailReceipt, u as registerEmailTemplates, i as renderTemplate, k as resendProvider, L as resolveProvider, A as sendgridProvider, j as sesProvider, M as smtpProvider, N as verifyLettermintSignature };
package/dist/service.js CHANGED
@@ -47,18 +47,18 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
47
47
  default: return l((e, t) => S.info(e, t));
48
48
  }
49
49
  }, w = (t) => {
50
- let l = t.attempts ?? 3, u = t.suppression ?? a(), d = t.allowlist, f = t.production !== !0 && d !== void 0 && d.length > 0, p = (e) => d.some((t) => t.toLowerCase() === e.toLowerCase()), m = b.recurs(l).pipe(b.while(({ input: e }) => e.transient)), h = /* @__PURE__ */ new Map();
51
- for (let e of t.templates ?? []) h.set(e.locale ? `${e.name}:${e.locale}` : e.name, e);
52
- let g = (e, t) => (t ? h.get(`${e}:${t}`) : void 0) ?? h.get(e) ?? i(e, t), _ = /* @__PURE__ */ new Map(), y = (e, t) => `${e.name}|${e.locale ?? ""}|${JSON.stringify(t ?? null)}`, x = (e, t) => {
53
- if (_.set(e, t), _.size > 500) {
54
- let e = _.keys().next().value;
55
- e !== void 0 && _.delete(e);
50
+ let l = t.attempts ?? 3, u = t.suppression ?? a(), d = t.allowlist, f = t.production !== !0 && d !== void 0 && d.length > 0, p = (e) => d.some((t) => t.toLowerCase() === e.toLowerCase()), m = (e) => b.recurs(e).pipe(b.while(({ input: e }) => e.transient)), h = m(l), g = /* @__PURE__ */ new Map();
51
+ for (let e of t.templates ?? []) g.set(e.locale ? `${e.name}:${e.locale}` : e.name, e);
52
+ let _ = (e, t) => (t ? g.get(`${e}:${t}`) : void 0) ?? g.get(e) ?? i(e, t), y = /* @__PURE__ */ new Map(), x = (e, t) => `${e.name}|${e.locale ?? ""}|${JSON.stringify(t ?? null)}`, C = (e, t) => {
53
+ if (y.set(e, t), y.size > 500) {
54
+ let e = y.keys().next().value;
55
+ e !== void 0 && y.delete(e);
56
56
  }
57
- }, C = t.idempotency ?? o(), w = s({
57
+ }, w = t.idempotency ?? o(), T = s({
58
58
  store: t.schedule ?? c(),
59
59
  providerSchedules: t.provider.supportsScheduling === !0,
60
- flush: (e) => O(e)
61
- }), T = (n) => {
60
+ flush: (e) => A(e)
61
+ }), E = (n) => {
62
62
  if (n.stream !== "broadcast" || t.provider.managesUnsubscribe === !0) return v.succeed(void 0);
63
63
  let r = n.unsubscribe;
64
64
  if (r === void 0 || !r.oneClickUrl.startsWith("https://")) return v.fail(new e({
@@ -71,7 +71,7 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
71
71
  "List-Unsubscribe": i.join(", "),
72
72
  "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
73
73
  });
74
- }, E = (r) => v.gen(function* () {
74
+ }, D = (r) => v.gen(function* () {
75
75
  let i = r.from ?? t.from;
76
76
  if (!i) return yield* v.fail(new e({
77
77
  provider: t.provider.name,
@@ -122,7 +122,7 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
122
122
  }
123
123
  };
124
124
  a = s;
125
- let c = r.replyTo ?? t.replyTo, l = yield* T(r), d = l === void 0 ? r.headers : {
125
+ let c = r.replyTo ?? t.replyTo, l = yield* E(r), d = l === void 0 ? r.headers : {
126
126
  ...r.headers,
127
127
  ...l
128
128
  };
@@ -136,9 +136,13 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
136
136
  ...d === void 0 ? {} : { headers: d }
137
137
  }
138
138
  };
139
- }), D = (e) => {
139
+ }), O = (n) => n.retryAttempts === void 0 || Number.isSafeInteger(n.retryAttempts) && n.retryAttempts >= 0 ? v.void : v.fail(new e({
140
+ provider: t.provider.name,
141
+ transient: !1,
142
+ message: "retryAttempts must be a non-negative safe integer."
143
+ })), k = (e) => {
140
144
  let r = n(e.to);
141
- return t.provider.send(e).pipe(v.retry(m), v.tap((n) => v.sync(() => {
145
+ return t.provider.send(e).pipe(v.retry(e.retryAttempts === void 0 ? h : m(e.retryAttempts)), v.tap((n) => v.sync(() => {
142
146
  S.info("mail sent", {
143
147
  provider: n.provider,
144
148
  id: n.id,
@@ -156,7 +160,7 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
156
160
  S.warn("mail failed", {
157
161
  provider: n.provider,
158
162
  status: n.status,
159
- message: n.message
163
+ message: "Mail provider request failed."
160
164
  }), t.onSent?.({
161
165
  id: `err-${Date.now()}`,
162
166
  to: r,
@@ -164,13 +168,14 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
164
168
  subject: e.subject,
165
169
  provider: n.provider,
166
170
  ok: !1,
167
- error: n.message,
171
+ error: "Mail provider request failed.",
168
172
  ts: Date.now()
169
173
  });
170
174
  })));
171
- }, O = (e) => v.gen(function* () {
172
- let r = yield* E(e);
173
- return r.kind === "skip" ? r.result : w.shouldHold(e.scheduledAt) ? (yield* w.hold(r.message), S.info("mail scheduled (held for later flush)", {
175
+ }, A = (e) => v.gen(function* () {
176
+ yield* O(e);
177
+ let r = yield* D(e);
178
+ return r.kind === "skip" ? r.result : T.shouldHold(e.scheduledAt) ? (yield* T.hold(r.message), S.info("mail scheduled (held for later flush)", {
174
179
  subject: r.message.subject,
175
180
  dueAt: e.scheduledAt?.toISOString()
176
181
  }), t.onSent?.({
@@ -184,24 +189,24 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
184
189
  }), {
185
190
  id: "scheduled",
186
191
  provider: "scheduled"
187
- }) : yield* D(r.message);
188
- }), k = (e, t, n) => t === void 0 ? n : v.gen(function* () {
189
- let r = yield* C.reserve(e, t);
192
+ }) : yield* k(r.message);
193
+ }), j = (e, t, n) => t === void 0 ? n : v.gen(function* () {
194
+ let r = yield* w.reserve(e, t);
190
195
  if (!r.won) return S.info("mail idempotent replay — skipped", { key: t }), r.prior ?? {
191
196
  id: "idempotent-replay",
192
197
  provider: "idempotent"
193
198
  };
194
199
  let i = yield* n;
195
- return yield* C.record(e, t, i), i;
196
- }), A = (t) => v.suspend(() => {
197
- let n = g(t.template, t.locale);
200
+ return yield* w.record(e, t, i), i;
201
+ }), M = (t) => v.suspend(() => {
202
+ let n = _(t.template, t.locale);
198
203
  if (!n) return v.fail(new e({
199
204
  provider: "template",
200
205
  message: `unknown template '${t.template}'${t.locale ? ` (locale '${t.locale}')` : ""}`,
201
206
  transient: !1
202
207
  }));
203
- let i = y(n, t.props), a = _.get(i);
204
- return (a ? v.succeed(a) : r(n, t.props).pipe(v.tap((e) => v.sync(() => x(i, e))))).pipe(v.map((e) => ({
208
+ let i = x(n, t.props), a = y.get(i);
209
+ return (a ? v.succeed(a) : r(n, t.props).pipe(v.tap((e) => v.sync(() => C(i, e))))).pipe(v.map((e) => ({
205
210
  to: t.to,
206
211
  subject: e.subject,
207
212
  html: e.html,
@@ -216,26 +221,27 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
216
221
  ...t.tenantId === void 0 ? {} : { tenantId: t.tenantId },
217
222
  ...t.scheduledAt === void 0 ? {} : { scheduledAt: t.scheduledAt },
218
223
  ...t.idempotencyKey === void 0 ? {} : { idempotencyKey: t.idempotencyKey },
224
+ ...t.retryAttempts === void 0 ? {} : { retryAttempts: t.retryAttempts },
219
225
  ...t.stream === void 0 ? {} : { stream: t.stream },
220
226
  ...t.unsubscribe === void 0 ? {} : { unsubscribe: t.unsubscribe },
221
227
  ...t.metadata === void 0 ? {} : { metadata: t.metadata }
222
228
  })));
223
- }), j = (e) => "template" in e ? A(e) : v.succeed(e);
229
+ }), N = (e) => ("template" in e ? M(e) : v.succeed(e)).pipe(v.tap(O));
224
230
  return {
225
231
  service: {
226
232
  send: (e) => v.gen(function* () {
227
- let t = yield* j(e);
228
- return yield* k(t.tenantId ?? null, t.idempotencyKey, O(t));
233
+ let t = yield* N(e);
234
+ return yield* j(t.tenantId ?? null, t.idempotencyKey, A(t));
229
235
  }),
230
236
  sendBatch: (e) => v.gen(function* () {
231
- let r = yield* v.forEach(e, j), i = r.some((e) => e.idempotencyKey !== void 0 || w.shouldHold(e.scheduledAt));
237
+ let r = yield* v.forEach(e, N), i = r.some((e) => e.idempotencyKey !== void 0 || e.retryAttempts !== void 0 || T.shouldHold(e.scheduledAt));
232
238
  if (t.provider.sendBatch && !i) {
233
239
  let e = [], i = [];
234
240
  for (let t of r) {
235
- let n = yield* E(t);
241
+ let n = yield* D(t);
236
242
  n.kind === "skip" ? i.push(n.result) : (i.push(e.length), e.push(n.message));
237
243
  }
238
- let a = e.length > 0 ? yield* t.provider.sendBatch(e).pipe(v.retry(m), v.tap((r) => v.sync(() => {
244
+ let a = e.length > 0 ? yield* t.provider.sendBatch(e).pipe(v.retry(h), v.tap((r) => v.sync(() => {
239
245
  e.forEach((e, i) => {
240
246
  let a = r[i];
241
247
  a && (S.info("mail sent (batch)", {
@@ -255,14 +261,14 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
255
261
  })), v.tapError((e) => v.sync(() => S.warn("mail batch failed", {
256
262
  provider: e.provider,
257
263
  status: e.status,
258
- message: e.message
264
+ message: "Mail provider request failed."
259
265
  })))) : [];
260
266
  return i.map((e) => typeof e == "number" ? a[e] ?? {
261
267
  id: "batch",
262
268
  provider: t.provider.name
263
269
  } : e);
264
270
  }
265
- return yield* v.forEach(r, (e) => k(e.tenantId ?? null, e.idempotencyKey, O(e)));
271
+ return yield* v.forEach(r, (e) => j(e.tenantId ?? null, e.idempotencyKey, A(e)));
266
272
  }),
267
273
  suppress: u.suppress,
268
274
  unsuppress: u.unsuppress,
@@ -272,7 +278,7 @@ var S = x({ scope: "@voltro/plugin-mail" }), C = (e) => {
272
278
  managesUnsubscribe: t.provider.managesUnsubscribe === !0
273
279
  }
274
280
  },
275
- scheduler: w
281
+ scheduler: T
276
282
  };
277
283
  }, T = (e) => w(e).service, E = (e) => y.succeed(t, T(e));
278
284
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-mail",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Transactional and broadcast email — one MailProvider contract, multiple adapters (Lettermint / Resend / Postmark / SendGrid / SES / Mailgun / SMTP + console/memory for dev/test). Exposes a MailService via the plugin services layer with transient retry, suppression, a mounted delivery-event webhook and a dev allowlist.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -33,10 +33,10 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@voltro/database": "0.86.0",
37
- "@voltro/env": "0.86.0",
38
- "@voltro/logger": "0.86.0",
39
- "@voltro/protocol": "0.86.0"
36
+ "@voltro/database": "0.87.0",
37
+ "@voltro/env": "0.87.0",
38
+ "@voltro/logger": "0.87.0",
39
+ "@voltro/protocol": "0.87.0"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "nodemailer": "^10.0.10"