@voltro/plugin-mail 0.85.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.
package/dist/egress.js ADDED
@@ -0,0 +1,34 @@
1
+ //#region src/egress.ts
2
+ var e = {
3
+ lettermint: "api.lettermint.co",
4
+ resend: "api.resend.com",
5
+ postmark: "api.postmarkapp.com",
6
+ sendgrid: "api.sendgrid.com"
7
+ }, t = (e) => {
8
+ try {
9
+ return new URL(e).host || void 0;
10
+ } catch {
11
+ return;
12
+ }
13
+ }, n = (n) => {
14
+ let r = n.env ?? {}, i = (e) => [{
15
+ destination: e,
16
+ purpose: "outgoing email: recipients, subject and body",
17
+ data: ["personal", "content"],
18
+ disable: "MAIL_PROVIDER=console (or memory) logs mail instead of sending it — none leaves"
19
+ }];
20
+ if (n.provider !== void 0 && typeof n.provider != "string") {
21
+ let e = n.provider.name;
22
+ return e === "console" || e === "memory" ? [] : i(`runtime:the '${e}' mail provider`);
23
+ }
24
+ let a = n.provider ?? r.MAIL_PROVIDER ?? "console", o = e[a];
25
+ if (o !== void 0) return i(o);
26
+ switch (a) {
27
+ case "ses": return i(r.AWS_REGION ? `email.${r.AWS_REGION}.amazonaws.com` : "runtime:email.<AWS_REGION>.amazonaws.com");
28
+ case "mailgun": return i((r.MAILGUN_BASE_URL ? t(r.MAILGUN_BASE_URL) : void 0) ?? "api.mailgun.net");
29
+ case "smtp": return i((r.SMTP_URL ? t(r.SMTP_URL) : void 0) ?? (r.SMTP_HOST ? `${r.SMTP_HOST}${r.SMTP_PORT ? `:${r.SMTP_PORT}` : ""}` : void 0) ?? "runtime:SMTP_URL or SMTP_HOST");
30
+ default: return [];
31
+ }
32
+ };
33
+ //#endregion
34
+ export { n as mailEgressOf };
package/dist/events.js ADDED
@@ -0,0 +1,46 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/events.ts
3
+ var t = (e, t, n, r, i, a = {}) => {
4
+ if (!t) return null;
5
+ let o = a.metadata !== null && typeof a.metadata == "object" ? Object.fromEntries(Object.entries(a.metadata).filter(([, e]) => typeof e == "string")) : void 0;
6
+ return {
7
+ type: e,
8
+ email: t,
9
+ provider: n,
10
+ raw: i,
11
+ ...r === void 0 ? {} : { messageId: r },
12
+ ...a.eventId === void 0 ? {} : { eventId: a.eventId },
13
+ ...o === void 0 ? {} : { metadata: o }
14
+ };
15
+ }, n = (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", r = (e) => e === "hard_bounce" ? "bounced" : e === "spam_complaint" ? "complained" : e === "unsubscribe" ? "unsubscribed" : "unknown", i = (e) => e === "email.bounced" ? "bounced" : e === "email.complained" ? "complained" : e === "email.delivered" ? "delivered" : e === "email.opened" ? "opened" : e === "email.clicked" ? "clicked" : "unknown", a = (e) => e === "Bounce" ? "bounced" : e === "SpamComplaint" ? "complained" : e === "Delivery" ? "delivered" : e === "Open" ? "opened" : e === "Click" ? "clicked" : "unknown", o = (e) => e === "bounce" || e === "dropped" ? "bounced" : e === "spamreport" ? "complained" : e === "delivered" ? "delivered" : e === "open" ? "opened" : e === "click" ? "clicked" : "unknown", s = (e, s) => {
16
+ let c = [], l = (e) => {
17
+ e && c.push(e);
18
+ };
19
+ if (e === "sendgrid" && Array.isArray(s)) {
20
+ for (let e of s) l(t(o(String(e.event)), e.email, "sendgrid", e.sg_message_id, e));
21
+ return c;
22
+ }
23
+ let u = s ?? {};
24
+ if (e === "lettermint") {
25
+ let e = String(u.event), i = u.data ?? {}, a = typeof u.id == "string" ? u.id : void 0;
26
+ return e === "suppression.added" ? (i.type === "email" && l(t(r(String(i.reason)), i.value, "lettermint", void 0, s, { eventId: a })), c) : e === "suppression.removed" ? (i.type === "email" && l(t("unsuppressed", i.value, "lettermint", void 0, s, { eventId: a })), c) : (e.startsWith("message.") && l(t(n(e), i.recipient, "lettermint", i.message_id, s, {
27
+ eventId: a,
28
+ metadata: i.metadata
29
+ })), c);
30
+ }
31
+ if (e === "resend") {
32
+ let e = u.data ?? {}, n = Array.isArray(e.to) ? e.to[0] : e.to ?? e.email;
33
+ return l(t(i(String(u.type)), n, "resend", e.email_id, s)), c;
34
+ }
35
+ if (e === "postmark") {
36
+ let e = u.Email ?? u.Recipient;
37
+ return l(t(a(String(u.RecordType)), e, "postmark", u.MessageID, s, { metadata: u.Metadata })), c;
38
+ }
39
+ return c;
40
+ }, c = (t, n, r, i = null) => e.gen(function* () {
41
+ let e = s(t, n);
42
+ for (let t of e) t.type === "bounced" || t.type === "complained" ? yield* r.suppress(i, t.email, t.type) : t.type === "unsuppressed" && (yield* r.unsuppress(i, t.email));
43
+ return e;
44
+ });
45
+ //#endregion
46
+ export { c as handleMailEvents, s as parseMailEvent };
@@ -0,0 +1,75 @@
1
+ import { MAIL_IDEMPOTENCY_TABLE as e, attempt as t, describeError as n } from "./storeTables.js";
2
+ import { Effect as r } from "effect";
3
+ import { createLogger as i } from "@voltro/logger";
4
+ import { and as a, eq as o } from "@voltro/database";
5
+ //#region src/idempotency.ts
6
+ var s = i({ scope: "@voltro/plugin-mail" }), c = (e) => e ?? "*", l = () => {
7
+ let e = /* @__PURE__ */ new Map(), t = (e, t) => `${c(e)} ${t}`;
8
+ return {
9
+ reserve: (n, i) => r.sync(() => {
10
+ let r = t(n, i);
11
+ if (e.has(r)) {
12
+ let t = e.get(r);
13
+ return t ? {
14
+ won: !1,
15
+ prior: t
16
+ } : { won: !1 };
17
+ }
18
+ return e.set(r, null), { won: !0 };
19
+ }),
20
+ record: (n, i, a) => r.sync(() => void e.set(t(n, i), a))
21
+ };
22
+ }, u = () => {
23
+ let i = null;
24
+ return {
25
+ reserve: (a, o) => {
26
+ let l = i;
27
+ return l ? t(() => l.insertIgnoreWithOutcome(e, {
28
+ tenant: c(a),
29
+ key: o,
30
+ createdAt: /* @__PURE__ */ new Date()
31
+ }, { conflictColumns: ["tenant", "key"] })).pipe(r.map(({ row: e, outcome: t }) => {
32
+ if (t === "inserted") return { won: !0 };
33
+ let n = e.resultId, r = e.resultProvider;
34
+ return typeof n == "string" && typeof r == "string" ? {
35
+ won: !1,
36
+ prior: {
37
+ id: n,
38
+ provider: r
39
+ }
40
+ } : { won: !1 };
41
+ }), r.catch((e) => r.sync(() => (s.warn("idempotency reserve failed — skipping send to avoid a double", {
42
+ key: o,
43
+ message: n(e)
44
+ }), { won: !1 })))) : r.succeed({ won: !1 });
45
+ },
46
+ record: (l, u, d) => {
47
+ let f = i;
48
+ return f ? t(() => f.updateMany(e, {
49
+ resultId: d.id,
50
+ resultProvider: d.provider
51
+ }, { where: a(o("tenant", c(l)), o("key", u)) })).pipe(r.asVoid, r.catch((e) => r.sync(() => s.warn("idempotency record failed", {
52
+ key: u,
53
+ message: n(e)
54
+ })))) : r.void;
55
+ },
56
+ bind: async (e) => {
57
+ i = e;
58
+ }
59
+ };
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)));
74
+ //#endregion
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`. */