@softure-ai/mailing 0.1.11 → 0.1.13
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 +42 -0
- package/README.md +182 -9
- package/dist/cli/run.d.ts +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +179 -25
- package/dist/cli/run.js.map +1 -1
- package/dist/contract.d.ts +5 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +3 -1
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/index.js.map +1 -1
- package/dist/next/pages.d.ts +25 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +33 -10
- package/dist/next/pages.js.map +1 -1
- package/dist/next/preview-mail.d.ts +5 -0
- package/dist/next/preview-mail.d.ts.map +1 -0
- package/dist/next/preview-mail.js +8 -0
- package/dist/next/preview-mail.js.map +1 -0
- package/dist/next/run-deliveries.d.ts +4 -0
- package/dist/next/run-deliveries.d.ts.map +1 -0
- package/dist/next/run-deliveries.js +7 -0
- package/dist/next/run-deliveries.js.map +1 -0
- package/dist/options.d.ts +3 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +16 -1
- package/dist/options.js.map +1 -1
- package/dist/server/campaign-file.d.ts +7 -2
- package/dist/server/campaign-file.d.ts.map +1 -1
- package/dist/server/campaign-file.js +35 -15
- package/dist/server/campaign-file.js.map +1 -1
- package/dist/server/campaigns.d.ts +13 -8
- package/dist/server/campaigns.d.ts.map +1 -1
- package/dist/server/campaigns.js +15 -14
- package/dist/server/campaigns.js.map +1 -1
- package/dist/server/deliveries.d.ts +32 -0
- package/dist/server/deliveries.d.ts.map +1 -1
- package/dist/server/deliveries.js +36 -0
- package/dist/server/deliveries.js.map +1 -1
- package/dist/server/dns.d.ts +15 -1
- package/dist/server/dns.d.ts.map +1 -1
- package/dist/server/dns.js +45 -4
- package/dist/server/dns.js.map +1 -1
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +6 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/operator-output.d.ts +5 -0
- package/dist/server/operator-output.d.ts.map +1 -0
- package/dist/server/operator-output.js +19 -0
- package/dist/server/operator-output.js.map +1 -0
- package/dist/server/options.d.ts +2 -0
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +5 -0
- package/dist/server/options.js.map +1 -1
- package/dist/server/run-deliveries.d.ts +62 -0
- package/dist/server/run-deliveries.d.ts.map +1 -0
- package/dist/server/run-deliveries.js +133 -0
- package/dist/server/run-deliveries.js.map +1 -0
- package/dist/server/send-mail.d.ts +21 -2
- package/dist/server/send-mail.d.ts.map +1 -1
- package/dist/server/send-mail.js +50 -30
- package/dist/server/send-mail.js.map +1 -1
- package/dist/server/suppressions.d.ts +5 -0
- package/dist/server/suppressions.d.ts.map +1 -1
- package/dist/server/suppressions.js +27 -1
- package/dist/server/suppressions.js.map +1 -1
- package/migrations/0005_add_sql_surface.sql +44 -0
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/cli/run.ts +184 -27
- package/src/contract.ts +9 -3
- package/src/index.ts +1 -1
- package/src/next/index.ts +3 -1
- package/src/next/pages.tsx +69 -14
- package/src/next/preview-mail.ts +9 -0
- package/src/next/run-deliveries.ts +10 -0
- package/src/options.ts +18 -1
- package/src/server/campaign-file.ts +36 -2
- package/src/server/campaigns.ts +25 -21
- package/src/server/deliveries.ts +53 -0
- package/src/server/dns.ts +60 -4
- package/src/server/index.ts +9 -3
- package/src/server/operator-output.ts +20 -0
- package/src/server/options.ts +6 -0
- package/src/server/run-deliveries.ts +193 -0
- package/src/server/send-mail.ts +63 -37
- package/src/server/suppressions.ts +28 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type MailingErrorCode } from "../contract.js";
|
|
2
|
+
import { type DeliverOptions, type Delivery, type DeliveryContext, type DeliveryOutcome, type RunHalt } from "./deliveries.js";
|
|
3
|
+
export interface DeliveryRunInput<Recipient> {
|
|
4
|
+
readonly recipients: Iterable<Recipient> | AsyncIterable<Recipient>;
|
|
5
|
+
/** The delivery for one recipient (its scope and mail), or `null` to skip them. A throw stops the run. */
|
|
6
|
+
readonly build: (recipient: Recipient) => Delivery | null | Promise<Delivery | null>;
|
|
7
|
+
}
|
|
8
|
+
export interface RunDeliveriesOptions extends DeliverOptions {
|
|
9
|
+
/** Pause after every mail that reached the provider, for its rate limit. Default 0. */
|
|
10
|
+
readonly pauseMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Most mails this run hands to the provider (sent, rejected by it, or not taken now). Once reached, the run reads
|
|
13
|
+
* the rest only to count `remaining`. A whole number of at least 1. Default: no limit.
|
|
14
|
+
*/
|
|
15
|
+
readonly limit?: number;
|
|
16
|
+
/** Reads the ledger and the suppression list and sends and writes nothing; `sent` then counts what would go out. */
|
|
17
|
+
readonly dryRun?: boolean;
|
|
18
|
+
/** Called after every delivery of a real run, e.g. for progress output. Never receives the address. */
|
|
19
|
+
readonly onDelivery?: (outcome: DeliveryOutcome) => void;
|
|
20
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* End the run at the first `retry-later` (the provider is down, timed out or rate limited), reported in `halted` as
|
|
23
|
+
* `mailing.unavailable`. Going on would spend an attempt of every following recipient on the same answer. Default
|
|
24
|
+
* `true`.
|
|
25
|
+
*/
|
|
26
|
+
readonly stopOnRetryLater?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface DeliveryRunSummary {
|
|
29
|
+
readonly dryRun: boolean;
|
|
30
|
+
/** Recipients read (past the limit too, but not past a halt). */
|
|
31
|
+
readonly recipients: number;
|
|
32
|
+
/** `build` returned `null`; nothing stored. */
|
|
33
|
+
readonly skipped: number;
|
|
34
|
+
/** Sent in this run; in a dry run, the mails that would reach the provider. */
|
|
35
|
+
readonly sent: number;
|
|
36
|
+
/** Rejected in this run, by reason; suppressed list mail is under `mailing.suppressed`. */
|
|
37
|
+
readonly rejected: Readonly<Record<MailingErrorCode, number>>;
|
|
38
|
+
/** Closed by an earlier run (or, in a dry run, by a delivery earlier in this one); nothing sent. */
|
|
39
|
+
readonly done: number;
|
|
40
|
+
/** Claimed by another run that is still sending. */
|
|
41
|
+
readonly inFlight: number;
|
|
42
|
+
/** The provider was unavailable, or the run halted on it; the next run tries again. */
|
|
43
|
+
readonly retryLater: number;
|
|
44
|
+
/** Claimed more than `uncertainClaimMs` ago and never closed: their mail may have gone out. */
|
|
45
|
+
readonly uncertain: number;
|
|
46
|
+
/**
|
|
47
|
+
* Set when the run stopped early: at a failure about the sending account (run again once the account can send) or,
|
|
48
|
+
* with `stopOnRetryLater`, at the first mail the provider could not take now (`mailing.unavailable`; run again later).
|
|
49
|
+
*/
|
|
50
|
+
readonly halted: RunHalt | null;
|
|
51
|
+
/** Deliveries past the `limit` cut that the next run would send; 0 without a cut, `null` after a halt. */
|
|
52
|
+
readonly remaining: number | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds and delivers one mail per recipient, in order, each at most once per scope and recipient. Stops at the first
|
|
56
|
+
* `halted` delivery and, unless `stopOnRetryLater` is `false`, at the first `retry-later`. Throws on a
|
|
57
|
+
* database failure, when `build` throws, like `deliverOnce` for a malformed delivery, at the first list mail when
|
|
58
|
+
* `MAILING_UNSUBSCRIBE_SECRET` is not set (each send would fail and use up an attempt), and (a `RangeError`) for a
|
|
59
|
+
* `limit` that is not a whole number of at least 1.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runDeliveries<Recipient>(ctx: DeliveryContext, input: DeliveryRunInput<Recipient>, options?: RunDeliveriesOptions): Promise<DeliveryRunSummary>;
|
|
62
|
+
//# sourceMappingURL=run-deliveries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-deliveries.d.ts","sourceRoot":"","sources":["../../src/server/run-deliveries.ts"],"names":[],"mappings":"AAKA,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAA6C,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAyB,KAAK,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAMjM,MAAM,WAAW,gBAAgB,CAAC,SAAS;IACzC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IACpE,0GAA0G;IAC1G,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,oHAAoH;IACpH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,uGAAuG;IACvG,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,0GAA0G;IAC1G,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAID;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAyCxK"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// A run of lifecycle mail: one delivery per recipient through the ledger (`deliverOnce`), e.g. every subscription
|
|
2
|
+
// whose trial ends this week. The app lists the recipients and builds each mail; the run keeps the ledger's
|
|
3
|
+
// at-most-once rule, stops at the first failure about the sending account (refused key, spent quota), at the first
|
|
4
|
+
// mail the provider could not take now (unless told otherwise), and after `limit` mails reached the provider, and
|
|
5
|
+
// counts what it did. A dry run reads the same state and writes nothing.
|
|
6
|
+
import { TRANSACTIONAL_KIND } from "../contract.js";
|
|
7
|
+
import { deliverOnce, forecastDelivery, getRunHalt } from "./deliveries.js";
|
|
8
|
+
import { getRecipientKey, MIN_UNSUBSCRIBE_SECRET_LENGTH, readUnsubscribeSecrets, UNSUBSCRIBE_SECRET_ENV } from "./unsubscribe-link.js";
|
|
9
|
+
/** Rejections the provider answered (the others never left the process). */
|
|
10
|
+
const PROVIDER_REASONS = new Set(["mailing.rejected", "mailing.unavailable"]);
|
|
11
|
+
/**
|
|
12
|
+
* Builds and delivers one mail per recipient, in order, each at most once per scope and recipient. Stops at the first
|
|
13
|
+
* `halted` delivery and, unless `stopOnRetryLater` is `false`, at the first `retry-later`. Throws on a
|
|
14
|
+
* database failure, when `build` throws, like `deliverOnce` for a malformed delivery, at the first list mail when
|
|
15
|
+
* `MAILING_UNSUBSCRIBE_SECRET` is not set (each send would fail and use up an attempt), and (a `RangeError`) for a
|
|
16
|
+
* `limit` that is not a whole number of at least 1.
|
|
17
|
+
*/
|
|
18
|
+
export async function runDeliveries(ctx, input, options = {}) {
|
|
19
|
+
const { pauseMs = 0, limit, dryRun = false, onDelivery, sleep = wait, stopOnRetryLater = true, ...deliverOptions } = options;
|
|
20
|
+
if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1)) {
|
|
21
|
+
throw new RangeError(`@softure-ai/mailing: runDeliveries limit must be a whole number of at least 1, got ${String(limit)}`);
|
|
22
|
+
}
|
|
23
|
+
const counts = { recipients: 0, skipped: 0, sent: 0, done: 0, inFlight: 0, retryLater: 0, uncertain: 0, rejected: emptyRejections() };
|
|
24
|
+
// Deliveries this run already sent or would send, so a dry run (and the count past the limit) sees its own effect.
|
|
25
|
+
const reached = new Set();
|
|
26
|
+
let handedToProvider = 0;
|
|
27
|
+
let remaining = 0;
|
|
28
|
+
for await (const recipient of input.recipients) {
|
|
29
|
+
counts.recipients += 1;
|
|
30
|
+
const delivery = await input.build(recipient);
|
|
31
|
+
if (delivery === null) {
|
|
32
|
+
counts.skipped += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
assertListMailCanBeSigned(delivery);
|
|
36
|
+
const isPastLimit = limit !== undefined && handedToProvider >= limit;
|
|
37
|
+
if (dryRun || isPastLimit) {
|
|
38
|
+
const forecast = await forecastOnce(ctx, delivery, { options: deliverOptions, reached });
|
|
39
|
+
if (isPastLimit) {
|
|
40
|
+
if (forecast.status === "send")
|
|
41
|
+
remaining += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
countForecast(counts, forecast);
|
|
45
|
+
if (forecast.status === "send")
|
|
46
|
+
handedToProvider += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const outcome = await deliverOnce(ctx, delivery, deliverOptions);
|
|
50
|
+
countOutcome(counts, outcome);
|
|
51
|
+
onDelivery?.(outcome);
|
|
52
|
+
const halt = getRunHalt(outcome, stopOnRetryLater);
|
|
53
|
+
if (halt !== null)
|
|
54
|
+
return { dryRun, ...counts, halted: halt, remaining: null };
|
|
55
|
+
const reachedProvider = outcome.status === "sent" || outcome.status === "retry-later" || (outcome.status === "rejected" && PROVIDER_REASONS.has(outcome.reason));
|
|
56
|
+
if (outcome.status === "sent")
|
|
57
|
+
reached.add(getDeliveryKey(delivery));
|
|
58
|
+
if (reachedProvider)
|
|
59
|
+
handedToProvider += 1;
|
|
60
|
+
if (reachedProvider && pauseMs > 0)
|
|
61
|
+
await sleep(pauseMs);
|
|
62
|
+
}
|
|
63
|
+
return { dryRun, ...counts, halted: null, remaining };
|
|
64
|
+
}
|
|
65
|
+
function getDeliveryKey(delivery) {
|
|
66
|
+
return `${delivery.scope}\n${getRecipientKey(delivery.mail.to)}`;
|
|
67
|
+
}
|
|
68
|
+
/** List mail without the signing secret is a configuration bug: stop before any claim is taken. */
|
|
69
|
+
function assertListMailCanBeSigned(delivery) {
|
|
70
|
+
if ((delivery.mail.kind ?? TRANSACTIONAL_KIND) === TRANSACTIONAL_KIND || readUnsubscribeSecrets().current !== null)
|
|
71
|
+
return;
|
|
72
|
+
throw new Error(`@softure-ai/mailing: runDeliveries sends list mail, which needs ${UNSUBSCRIBE_SECRET_ENV} (at least ${String(MIN_UNSUBSCRIBE_SECRET_LENGTH)} characters)`);
|
|
73
|
+
}
|
|
74
|
+
/** The forecast, with a delivery this run already sent (or would send) counted as done. */
|
|
75
|
+
async function forecastOnce(ctx, delivery, { options, reached }) {
|
|
76
|
+
const key = getDeliveryKey(delivery);
|
|
77
|
+
const forecast = await forecastDelivery(ctx, delivery, options);
|
|
78
|
+
if (forecast.status !== "send")
|
|
79
|
+
return forecast;
|
|
80
|
+
if (reached.has(key))
|
|
81
|
+
return { status: "done" };
|
|
82
|
+
reached.add(key);
|
|
83
|
+
return forecast;
|
|
84
|
+
}
|
|
85
|
+
function countForecast(counts, forecast) {
|
|
86
|
+
switch (forecast.status) {
|
|
87
|
+
case "send":
|
|
88
|
+
counts.sent += 1;
|
|
89
|
+
return;
|
|
90
|
+
case "rejected":
|
|
91
|
+
counts.rejected[forecast.reason] += 1;
|
|
92
|
+
return;
|
|
93
|
+
case "done":
|
|
94
|
+
counts.done += 1;
|
|
95
|
+
return;
|
|
96
|
+
case "in-flight":
|
|
97
|
+
counts.inFlight += 1;
|
|
98
|
+
return;
|
|
99
|
+
case "uncertain":
|
|
100
|
+
counts.uncertain += 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function countOutcome(counts, outcome) {
|
|
105
|
+
switch (outcome.status) {
|
|
106
|
+
case "sent":
|
|
107
|
+
counts.sent += 1;
|
|
108
|
+
return;
|
|
109
|
+
case "rejected":
|
|
110
|
+
counts.rejected[outcome.reason] += 1;
|
|
111
|
+
return;
|
|
112
|
+
case "done":
|
|
113
|
+
counts.done += 1;
|
|
114
|
+
return;
|
|
115
|
+
case "in-flight":
|
|
116
|
+
counts.inFlight += 1;
|
|
117
|
+
return;
|
|
118
|
+
case "retry-later":
|
|
119
|
+
case "halted":
|
|
120
|
+
counts.retryLater += 1;
|
|
121
|
+
return;
|
|
122
|
+
case "uncertain":
|
|
123
|
+
counts.uncertain += 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function emptyRejections() {
|
|
128
|
+
return { "mailing.invalid_input": 0, "mailing.rejected": 0, "mailing.unavailable": 0, "mailing.suppressed": 0, "mailing.provider_refused": 0, "mailing.quota_exceeded": 0 };
|
|
129
|
+
}
|
|
130
|
+
function wait(ms) {
|
|
131
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=run-deliveries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-deliveries.js","sourceRoot":"","sources":["../../src/server/run-deliveries.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type SoftureConfig } from "@softure-ai/core";
|
|
1
|
+
import { type Err, type Ok, type SoftureConfig } from "@softure-ai/core";
|
|
2
2
|
import type { Queryable } from "@softure-ai/db";
|
|
3
|
-
import { type OutgoingMail, type SendMailOptions, type SendMailResult } from "../contract.js";
|
|
3
|
+
import { type OutgoingMail, type ProviderMessage, type SendMailOptions, type SendMailResult } from "../contract.js";
|
|
4
|
+
import { type Env } from "./unsubscribe-link.js";
|
|
4
5
|
/**
|
|
5
6
|
* What `sendMail` needs: the app's configuration and, for list mail, the database handle (the
|
|
6
7
|
* suppression check reads it). Transactional mail needs no `db`.
|
|
@@ -18,4 +19,22 @@ export interface MailContext {
|
|
|
18
19
|
* Throws only when the module is not enabled, or for list mail without `context.db`.
|
|
19
20
|
*/
|
|
20
21
|
export declare function sendMail(context: MailContext, mail: OutgoingMail, options?: SendMailOptions): Promise<SendMailResult>;
|
|
22
|
+
/** Why `previewMail` cannot render a mail: invalid input (with the fields), or list mail without the secret. */
|
|
23
|
+
export type MailPreviewFailure = (Err<"mailing.invalid_input"> & {
|
|
24
|
+
readonly fields: readonly string[];
|
|
25
|
+
}) | (Err<"mailing.unavailable"> & {
|
|
26
|
+
readonly cause: "no_unsubscribe_secret";
|
|
27
|
+
});
|
|
28
|
+
export type MailPreviewResult = Ok<ProviderMessage> | MailPreviewFailure;
|
|
29
|
+
export interface PreviewMailOptions extends SendMailOptions {
|
|
30
|
+
/** Where the unsubscribe secret is read. Default: `process.env`. */
|
|
31
|
+
readonly env?: Env;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Exactly what `sendMail` would hand the provider for `mail`: the sender, the reply-to, and for list mail the
|
|
35
|
+
* footer with a link signed for `mail.to` and the RFC 8058 headers. Sends nothing, logs nothing and needs no
|
|
36
|
+
* database: it does not check the suppression list, so it renders a mail `sendMail` would refuse as suppressed.
|
|
37
|
+
* The links in it are credentials for `mail.to`. Throws only when the module is not enabled.
|
|
38
|
+
*/
|
|
39
|
+
export declare function previewMail(config: SoftureConfig, mail: OutgoingMail, options?: PreviewMailOptions): MailPreviewResult;
|
|
21
40
|
//# sourceMappingURL=send-mail.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-mail.d.ts","sourceRoot":"","sources":["../../src/server/send-mail.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"send-mail.d.ts","sourceRoot":"","sources":["../../src/server/send-mail.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAIL,KAAK,YAAY,EAEjB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAAwG,KAAK,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAGvJ;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyB/H;AAED,gHAAgH;AAChH,MAAM,MAAM,kBAAkB,GAC1B,CAAC,GAAG,CAAC,uBAAuB,CAAC,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,GACvE,CAAC,GAAG,CAAC,qBAAqB,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAA;CAAE,CAAC,CAAC;AAE/E,MAAM,MAAM,iBAAiB,GAAG,EAAE,CAAC,eAAe,CAAC,GAAG,kBAAkB,CAAC;AAEzE,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,oEAAoE;IACpE,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAO1H"}
|
package/dist/server/send-mail.js
CHANGED
|
@@ -19,23 +19,22 @@ import { validateMail } from "./validate-mail.js";
|
|
|
19
19
|
* Throws only when the module is not enabled, or for list mail without `context.db`.
|
|
20
20
|
*/
|
|
21
21
|
export async function sendMail(context, mail, options = {}) {
|
|
22
|
-
const {
|
|
22
|
+
const { provider, timeoutMs } = getMailingOptions(context.config);
|
|
23
23
|
const validation = validateMail(mail, options);
|
|
24
24
|
if (!validation.ok) {
|
|
25
25
|
logFailure(provider, "mailing.invalid_input", { fields: validation.fields });
|
|
26
26
|
return err("mailing.invalid_input");
|
|
27
27
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (kind !== TRANSACTIONAL_KIND) {
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
logFailure(provider,
|
|
34
|
-
return err(
|
|
28
|
+
const valid = validation.value;
|
|
29
|
+
const secret = readUnsubscribeSecrets().current;
|
|
30
|
+
if (valid.kind !== TRANSACTIONAL_KIND) {
|
|
31
|
+
const refusal = await checkListMail(context, valid, secret);
|
|
32
|
+
if (refusal !== null) {
|
|
33
|
+
logFailure(provider, refusal.error, { cause: refusal.cause });
|
|
34
|
+
return err(refusal.error);
|
|
35
35
|
}
|
|
36
|
-
message = listMail.value;
|
|
37
36
|
}
|
|
38
|
-
const outcome = await callProvider(provider,
|
|
37
|
+
const outcome = await callProvider(provider, composeMessage(context.config, valid, secret), timeoutMs);
|
|
39
38
|
if (outcome.status === "sent") {
|
|
40
39
|
return ok({ id: outcome.id, provider: provider.name });
|
|
41
40
|
}
|
|
@@ -43,6 +42,22 @@ export async function sendMail(context, mail, options = {}) {
|
|
|
43
42
|
logFailure(provider, code, { status: outcome.httpStatus });
|
|
44
43
|
return outcome.httpStatus === undefined ? err(code) : { ...err(code), httpStatus: outcome.httpStatus };
|
|
45
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Exactly what `sendMail` would hand the provider for `mail`: the sender, the reply-to, and for list mail the
|
|
47
|
+
* footer with a link signed for `mail.to` and the RFC 8058 headers. Sends nothing, logs nothing and needs no
|
|
48
|
+
* database: it does not check the suppression list, so it renders a mail `sendMail` would refuse as suppressed.
|
|
49
|
+
* The links in it are credentials for `mail.to`. Throws only when the module is not enabled.
|
|
50
|
+
*/
|
|
51
|
+
export function previewMail(config, mail, options = {}) {
|
|
52
|
+
const { env, ...sendOptions } = options;
|
|
53
|
+
const validation = validateMail(mail, sendOptions);
|
|
54
|
+
if (!validation.ok)
|
|
55
|
+
return { ...err("mailing.invalid_input"), fields: validation.fields };
|
|
56
|
+
const secret = readUnsubscribeSecrets(env).current;
|
|
57
|
+
if (validation.value.kind !== TRANSACTIONAL_KIND && secret === null)
|
|
58
|
+
return { ...err("mailing.unavailable"), cause: "no_unsubscribe_secret" };
|
|
59
|
+
return ok(composeMessage(config, validation.value, secret));
|
|
60
|
+
}
|
|
46
61
|
const FAILURE_CODES = {
|
|
47
62
|
rejected: "mailing.rejected",
|
|
48
63
|
unavailable: "mailing.unavailable",
|
|
@@ -50,40 +65,45 @@ const FAILURE_CODES = {
|
|
|
50
65
|
quota_exceeded: "mailing.quota_exceeded",
|
|
51
66
|
};
|
|
52
67
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
68
|
+
* Why a list mail must not go out, or `null` when it may. Fails closed: without the secret, or when the suppression
|
|
69
|
+
* list cannot be read, nothing is sent.
|
|
55
70
|
*/
|
|
56
|
-
async function
|
|
71
|
+
async function checkListMail(context, mail, secret) {
|
|
57
72
|
if (context.db === undefined) {
|
|
58
73
|
throw new Error("@softure-ai/mailing: list mail needs the database handle; call sendMail({ config, db }, ...)");
|
|
59
74
|
}
|
|
60
|
-
const secret = readUnsubscribeSecrets().current;
|
|
61
75
|
if (secret === null) {
|
|
62
76
|
console.error(`mailing: list mail needs ${UNSUBSCRIBE_SECRET_ENV} (at least ${String(MIN_UNSUBSCRIBE_SECRET_LENGTH)} characters) to sign unsubscribe links`);
|
|
63
|
-
return {
|
|
77
|
+
return { error: "mailing.unavailable", cause: "no_unsubscribe_secret" };
|
|
64
78
|
}
|
|
65
|
-
let suppressed;
|
|
66
79
|
try {
|
|
67
|
-
|
|
80
|
+
return (await isSuppressed({ db: context.db }, mail.to)) ? { error: "mailing.suppressed", cause: "unsubscribed" } : null;
|
|
68
81
|
}
|
|
69
82
|
catch {
|
|
70
83
|
// The error text can carry the query parameters, the recipient key among them.
|
|
71
|
-
return {
|
|
84
|
+
return { error: "mailing.unavailable", cause: "suppressions_unreadable" };
|
|
72
85
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The message the provider receives: the configured sender, the mail's or the configured reply-to, and for list
|
|
89
|
+
* mail the footer and headers signed with `secret` (which the caller checked is set).
|
|
90
|
+
*/
|
|
91
|
+
function composeMessage(config, mail, secret) {
|
|
92
|
+
const { from, replyTo } = getMailingOptions(config);
|
|
93
|
+
const { idempotencyKey, kind, replyTo: mailReplyTo, ...content } = mail;
|
|
94
|
+
const envelope = { from, replyTo: mailReplyTo ?? replyTo ?? null, idempotencyKey };
|
|
95
|
+
if (kind === TRANSACTIONAL_KIND || secret === null)
|
|
96
|
+
return { ...content, ...envelope };
|
|
97
|
+
const links = buildUnsubscribeLinks(config, mail.to, secret);
|
|
76
98
|
// The module factory merged the dictionaries; their shape is the module's own.
|
|
77
|
-
const copy = getMailingModule(
|
|
99
|
+
const copy = getMailingModule(config).messages[config.locale].footer;
|
|
78
100
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
headers: { ...mail.headers, ...getListUnsubscribeHeaders(links) },
|
|
86
|
-
},
|
|
101
|
+
...envelope,
|
|
102
|
+
to: mail.to,
|
|
103
|
+
subject: mail.subject,
|
|
104
|
+
text: addTextFooter(mail.text, links, copy),
|
|
105
|
+
html: mail.html === null ? null : addHtmlFooter(mail.html, links, copy),
|
|
106
|
+
headers: { ...mail.headers, ...getListUnsubscribeHeaders(links) },
|
|
87
107
|
};
|
|
88
108
|
}
|
|
89
109
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-mail.js","sourceRoot":"","sources":["../../src/server/send-mail.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,oGAAoG;AACpG,mGAAmG;AACnG,mHAAmH;AACnH,8FAA8F;AAC9F,OAAO,EAAE,GAAG,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"send-mail.js","sourceRoot":"","sources":["../../src/server/send-mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,11 @@ import { type Env, type UnsubscribeLink, type UnsubscribeToken } from "./unsubsc
|
|
|
5
5
|
export type SuppressionContext = ModuleContext<Queryable>;
|
|
6
6
|
/** Whether `address` unsubscribed. Throws on a database failure: callers decide how to fail. */
|
|
7
7
|
export declare function isSuppressed(ctx: Pick<SuppressionContext, "db">, address: string): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Which of `addresses` unsubscribed, each returned as it was given; e.g. a report that splits a list into
|
|
10
|
+
* active and suppressed recipients. Throws on a database failure: callers decide how to fail.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findSuppressedAddresses(ctx: Pick<SuppressionContext, "db">, addresses: readonly string[]): Promise<Set<string>>;
|
|
8
13
|
/**
|
|
9
14
|
* Unsubscribes `address` from list mail, for an operator's script or another module (a bounce or
|
|
10
15
|
* complaint handler). Throws on a database failure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suppressions.d.ts","sourceRoot":"","sources":["../../src/server/suppressions.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAA2B,MAAM,gBAAgB,CAAC;AAGvG,OAAO,EAAmE,KAAK,GAAG,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE/J,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAE1D,gGAAgG;AAChG,wBAAsB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzG;AAOD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,iBAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvI;AAKD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5G;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,kBAAkB,EACvB,IAAI,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,EAC/C,MAAM,EAAE,OAAO,CAAC,iBAAiB,EAAE,UAAU,CAAC,EAC9C,GAAG,GAAE,GAAiB,GACrB,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC,CAWlD"}
|
|
1
|
+
{"version":3,"file":"suppressions.d.ts","sourceRoot":"","sources":["../../src/server/suppressions.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAA2B,MAAM,gBAAgB,CAAC;AAGvG,OAAO,EAAmE,KAAK,GAAG,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE/J,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAE1D,gGAAgG;AAChG,wBAAsB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzG;AAKD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAkBrI;AAOD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,iBAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvI;AAKD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5G;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,kBAAkB,EACvB,IAAI,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,EAC/C,MAAM,EAAE,OAAO,CAAC,iBAAiB,EAAE,UAAU,CAAC,EAC9C,GAAG,GAAE,GAAiB,GACrB,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC,CAWlD"}
|
|
@@ -17,6 +17,32 @@ export async function isSuppressed(ctx, address) {
|
|
|
17
17
|
.limit(1);
|
|
18
18
|
return rows.length > 0;
|
|
19
19
|
}
|
|
20
|
+
/** How many recipient keys one query asks about, so a long list never builds an oversized statement. */
|
|
21
|
+
const SUPPRESSION_LOOKUP_BATCH = 500;
|
|
22
|
+
/**
|
|
23
|
+
* Which of `addresses` unsubscribed, each returned as it was given; e.g. a report that splits a list into
|
|
24
|
+
* active and suppressed recipients. Throws on a database failure: callers decide how to fail.
|
|
25
|
+
*/
|
|
26
|
+
export async function findSuppressedAddresses(ctx, addresses) {
|
|
27
|
+
const byKey = new Map();
|
|
28
|
+
for (const address of addresses) {
|
|
29
|
+
const key = getRecipientKey(address);
|
|
30
|
+
byKey.set(key, [...(byKey.get(key) ?? []), address]);
|
|
31
|
+
}
|
|
32
|
+
const keys = [...byKey.keys()];
|
|
33
|
+
const suppressed = new Set();
|
|
34
|
+
for (let start = 0; start < keys.length; start += SUPPRESSION_LOOKUP_BATCH) {
|
|
35
|
+
const rows = await ctx.db
|
|
36
|
+
.select({ recipientKey: suppressions.recipientKey })
|
|
37
|
+
.from(suppressions)
|
|
38
|
+
.where(inArray(suppressions.recipientKey, keys.slice(start, start + SUPPRESSION_LOOKUP_BATCH)));
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
for (const address of byKey.get(row.recipientKey) ?? [])
|
|
41
|
+
suppressed.add(address);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return suppressed;
|
|
45
|
+
}
|
|
20
46
|
/** Records an opt-out by recipient key. Throws on a database failure. */
|
|
21
47
|
async function recordSuppression(ctx, recipientKey, source) {
|
|
22
48
|
await ctx.db.insert(suppressions).values({ recipientKey, source, createdAt: ctx.clock.now() }).onConflictDoNothing();
|
|
@@ -75,7 +101,7 @@ async function verifyLink(ctx, link, env) {
|
|
|
75
101
|
const legacy = getMailingOptions(ctx.config).legacyUnsubscribe;
|
|
76
102
|
if (legacy === undefined)
|
|
77
103
|
return null;
|
|
78
|
-
const address = await legacy.verify(link.values, ctx);
|
|
104
|
+
const address = await legacy.verify(link.values, ctx, env);
|
|
79
105
|
if (typeof address !== "string" || address.trim() === "")
|
|
80
106
|
return null;
|
|
81
107
|
return { recipientKey: getRecipientKey(address), link: { scheme: "legacy", values: link.values } };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suppressions.js","sourceRoot":"","sources":["../../src/server/suppressions.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"suppressions.js","sourceRoot":"","sources":["../../src/server/suppressions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- A stable surface for an app's own SQL (reports, recipient selection), so it never re-implements the recipient key
|
|
2
|
+
-- or reads the tables' internals: a change to the tables keeps these names and columns. Every name is qualified,
|
|
3
|
+
-- because a function runs with its caller's search_path.
|
|
4
|
+
-- Rollback: DROP VIEW mailing.suppressed_recipients; DROP VIEW mailing.delivery_outcomes;
|
|
5
|
+
-- DROP FUNCTION mailing.was_delivered(text, text); DROP FUNCTION mailing.is_suppressed(text);
|
|
6
|
+
-- DROP FUNCTION mailing.recipient_key(text);
|
|
7
|
+
-- DELETE FROM softure.migrations WHERE module = 'mailing' AND version = 5;
|
|
8
|
+
|
|
9
|
+
-- getRecipientKey in SQL: base64url SHA-256 (unpadded) of the address, lowercased and trimmed of the whitespace
|
|
10
|
+
-- JavaScript's trim() removes. lower() follows the database's ctype: under the C locale only ASCII letters are
|
|
11
|
+
-- lowercased, so an address with non-ASCII capitals keys differently there than in JavaScript.
|
|
12
|
+
CREATE FUNCTION recipient_key(address text) RETURNS text
|
|
13
|
+
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$
|
|
14
|
+
SELECT rtrim(translate(encode(sha256(convert_to(lower(btrim(address,
|
|
15
|
+
chr(9) || chr(10) || chr(11) || chr(12) || chr(13) || chr(32) || chr(160) || chr(5760)
|
|
16
|
+
|| chr(8192) || chr(8193) || chr(8194) || chr(8195) || chr(8196) || chr(8197) || chr(8198) || chr(8199)
|
|
17
|
+
|| chr(8200) || chr(8201) || chr(8202) || chr(8232) || chr(8233) || chr(8239) || chr(8287) || chr(12288)
|
|
18
|
+
|| chr(65279))), 'UTF8')), 'base64'), '+/', '-_'), '=')
|
|
19
|
+
$$;
|
|
20
|
+
|
|
21
|
+
-- Whether the address unsubscribed from list mail.
|
|
22
|
+
CREATE FUNCTION is_suppressed(address text) RETURNS boolean
|
|
23
|
+
LANGUAGE sql STABLE STRICT PARALLEL SAFE AS $$
|
|
24
|
+
SELECT EXISTS (SELECT 1 FROM mailing.suppressions WHERE recipient_key = mailing.recipient_key(address))
|
|
25
|
+
$$;
|
|
26
|
+
|
|
27
|
+
-- Whether a mail of this scope was sent to the address (a `sent` delivery, made by the module or imported).
|
|
28
|
+
CREATE FUNCTION was_delivered(scope text, address text) RETURNS boolean
|
|
29
|
+
LANGUAGE sql STABLE STRICT PARALLEL SAFE AS $$
|
|
30
|
+
SELECT EXISTS (
|
|
31
|
+
SELECT 1 FROM mailing.deliveries d
|
|
32
|
+
WHERE d.scope = was_delivered.scope AND d.recipient_key = mailing.recipient_key(address) AND d.status = 'sent'
|
|
33
|
+
)
|
|
34
|
+
$$;
|
|
35
|
+
|
|
36
|
+
-- The delivery ledger as app SQL may read it. status: pending, claimed, sent or rejected.
|
|
37
|
+
CREATE VIEW delivery_outcomes AS
|
|
38
|
+
SELECT scope, recipient_key, kind, campaign_id, status, attempts, claimed_at, finished_at, imported_at
|
|
39
|
+
FROM mailing.deliveries;
|
|
40
|
+
|
|
41
|
+
-- The suppression list as app SQL may read it. source: one-click, page or operator.
|
|
42
|
+
CREATE VIEW suppressed_recipients AS
|
|
43
|
+
SELECT recipient_key, source, created_at
|
|
44
|
+
FROM mailing.suppressions;
|
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/mailing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "SOFTURE AI mailing module: sendMail over a provider adapter (Resend first) with plain-text and HTML bodies, sender and reply-to from config, idempotency keys, a timeout and a typed result; signed one-click unsubscribe (RFC 8058) with a suppression list for list mail; an exactly-once delivery ledger, campaigns sent from a content file by the softure-mail command, and a sender DNS check; a fake provider for tests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|