@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/CHANGELOG.md +5987 -0
- package/THIRD-PARTY-NOTICES.md +865 -254
- package/dist/egress.js +34 -0
- package/dist/events.js +46 -0
- package/dist/idempotency.js +75 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +97 -1026
- package/dist/outbox.js +15 -0
- package/dist/providers.js +377 -0
- package/dist/registry.js +13 -0
- package/dist/render.js +44 -0
- package/dist/scheduler.js +36 -0
- package/dist/service.js +285 -0
- package/dist/skeleton.js +24 -0
- package/dist/storeTables.js +28 -0
- package/dist/suppression.js +46 -0
- package/dist/types.js +20 -0
- package/package.json +5 -5
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`. */
|