@softure-ai/mailing 0.1.5 → 0.1.7
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 +28 -0
- package/README.md +85 -23
- package/dist/cli/run.d.ts +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +56 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/contract.d.ts +48 -8
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +2 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +2 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +4 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +2 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +6 -6
- package/dist/next/actions.js.map +1 -1
- package/dist/next/context.js +2 -2
- package/dist/next/context.js.map +1 -1
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +8 -6
- package/dist/next/pages.js.map +1 -1
- package/dist/next/route.d.ts +2 -1
- package/dist/next/route.d.ts.map +1 -1
- package/dist/next/route.js +6 -4
- package/dist/next/route.js.map +1 -1
- package/dist/options.d.ts +15 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +32 -1
- package/dist/options.js.map +1 -1
- package/dist/providers/resend.d.ts.map +1 -1
- package/dist/providers/resend.js +12 -6
- package/dist/providers/resend.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -1
- package/dist/server/campaigns.d.ts +30 -4
- package/dist/server/campaigns.d.ts.map +1 -1
- package/dist/server/campaigns.js +67 -12
- package/dist/server/campaigns.js.map +1 -1
- package/dist/server/deliveries.d.ts +30 -2
- package/dist/server/deliveries.d.ts.map +1 -1
- package/dist/server/deliveries.js +58 -23
- package/dist/server/deliveries.js.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/send-mail.d.ts +3 -1
- package/dist/server/send-mail.d.ts.map +1 -1
- package/dist/server/send-mail.js +18 -6
- package/dist/server/send-mail.js.map +1 -1
- package/dist/server/suppressions.d.ts +8 -6
- package/dist/server/suppressions.d.ts.map +1 -1
- package/dist/server/suppressions.js +23 -7
- package/dist/server/suppressions.js.map +1 -1
- package/dist/server/unsubscribe-link.d.ts +23 -0
- package/dist/server/unsubscribe-link.d.ts.map +1 -1
- package/dist/server/unsubscribe-link.js +30 -1
- package/dist/server/unsubscribe-link.js.map +1 -1
- package/migrations/0003_add_delivery_provider_status.sql +13 -0
- package/module.json +1 -1
- package/package.json +6 -2
- package/src/cli/run.ts +72 -27
- package/src/contract.ts +49 -6
- package/src/index.ts +8 -3
- package/src/messages/en.ts +2 -0
- package/src/messages/pl.ts +2 -0
- package/src/next/actions.ts +5 -6
- package/src/next/context.ts +2 -2
- package/src/next/pages.tsx +10 -7
- package/src/next/route.ts +6 -4
- package/src/options.ts +36 -2
- package/src/providers/resend.ts +11 -7
- package/src/schema.ts +2 -0
- package/src/server/campaigns.ts +95 -17
- package/src/server/deliveries.ts +89 -29
- package/src/server/index.ts +8 -0
- package/src/server/send-mail.ts +30 -6
- package/src/server/suppressions.ts +23 -8
- package/src/server/unsubscribe-link.ts +41 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type Result } from "@softure-ai/core";
|
|
2
2
|
import type { MailingErrorCode } from "../contract.js";
|
|
3
3
|
import { type CampaignContent } from "./campaign-file.js";
|
|
4
|
-
import { type DeliverOptions, type DeliveryContext, type DeliveryOutcome } from "./deliveries.js";
|
|
4
|
+
import { type DeliverOptions, type DeliveryContext, type DeliveryOutcome, type HaltingErrorCode } from "./deliveries.js";
|
|
5
|
+
/** Why a run stopped before the end of the list. */
|
|
6
|
+
export interface CampaignHalt {
|
|
7
|
+
readonly reason: HaltingErrorCode;
|
|
8
|
+
readonly httpStatus?: number;
|
|
9
|
+
}
|
|
5
10
|
/** The campaign id is taken by other content. Give the new content a new id. */
|
|
6
11
|
export type CampaignErrorCode = "mailing.campaign_changed";
|
|
7
12
|
export interface CampaignSummary {
|
|
@@ -16,6 +21,15 @@ export interface CampaignSummary {
|
|
|
16
21
|
readonly inFlight: number;
|
|
17
22
|
/** The provider was unavailable; the next run tries them again. */
|
|
18
23
|
readonly retryLater: number;
|
|
24
|
+
/** Skipped by `filterCampaignRecipient`; nothing stored, so a later run sends to them once they qualify. */
|
|
25
|
+
readonly filtered: number;
|
|
26
|
+
/** Claimed more than `uncertainClaimMs` ago and never closed: their mail may have gone out. */
|
|
27
|
+
readonly uncertain: number;
|
|
28
|
+
/**
|
|
29
|
+
* Set when the run stopped at a failure about the sending account. The recipient it happened to and every one
|
|
30
|
+
* after it were left as they were; run again once the account can send.
|
|
31
|
+
*/
|
|
32
|
+
readonly halted: CampaignHalt | null;
|
|
19
33
|
}
|
|
20
34
|
export interface SendCampaignOptions extends DeliverOptions {
|
|
21
35
|
/** Pause after every mail that reached the provider, for its rate limit. Default 0. */
|
|
@@ -33,7 +47,9 @@ export declare function getCampaignContentHash(content: CampaignContent): string
|
|
|
33
47
|
export declare function registerCampaign(ctx: DeliveryContext, content: CampaignContent): Promise<Result<undefined, CampaignErrorCode>>;
|
|
34
48
|
/**
|
|
35
49
|
* Sends `campaign` to every recipient that has no outcome yet, one at a time. Recipients who
|
|
36
|
-
* unsubscribed are rejected (`mailing.suppressed`) and never retried
|
|
50
|
+
* unsubscribed are rejected (`mailing.suppressed`) and never retried; recipients the module's
|
|
51
|
+
* `filterCampaignRecipient` refuses are skipped. Stops at the first `halted` delivery. Throws on a
|
|
52
|
+
* database failure and when the filter throws.
|
|
37
53
|
*/
|
|
38
54
|
export declare function sendCampaign(ctx: DeliveryContext, input: {
|
|
39
55
|
readonly campaign: CampaignContent;
|
|
@@ -45,14 +61,24 @@ export interface CampaignPlan {
|
|
|
45
61
|
readonly done: number;
|
|
46
62
|
/** Not closed yet, but unsubscribed: they will be rejected without a send. */
|
|
47
63
|
readonly suppressed: number;
|
|
64
|
+
/** Not closed yet, but `filterCampaignRecipient` refuses them: they will be skipped. */
|
|
65
|
+
readonly filtered: number;
|
|
66
|
+
/**
|
|
67
|
+
* Claimed more than `uncertainClaimMs` ago and never closed: skipped unless the run retakes uncertain claims
|
|
68
|
+
* (then they are sent again and counted in `toSend` too).
|
|
69
|
+
*/
|
|
70
|
+
readonly uncertain: number;
|
|
48
71
|
/** Would be sent now. */
|
|
49
72
|
readonly toSend: number;
|
|
50
73
|
/** The campaign id is stored with other content: a real run would be refused. */
|
|
51
74
|
readonly contentChanged: boolean;
|
|
52
75
|
}
|
|
53
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* What `sendCampaign` would do, without writing or sending anything (the filter runs, and must not write). Throws
|
|
78
|
+
* on a database failure. `toSend` counts uncertain recipients only with `retakeUncertain`.
|
|
79
|
+
*/
|
|
54
80
|
export declare function planCampaign(ctx: DeliveryContext, input: {
|
|
55
81
|
readonly campaign: CampaignContent;
|
|
56
82
|
readonly recipients: Iterable<string> | AsyncIterable<string>;
|
|
57
|
-
}): Promise<CampaignPlan>;
|
|
83
|
+
}, options?: Pick<DeliverOptions, "uncertainClaimMs" | "retakeUncertain">): Promise<CampaignPlan>;
|
|
58
84
|
//# sourceMappingURL=campaigns.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"campaigns.d.ts","sourceRoot":"","sources":["../../src/server/campaigns.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"campaigns.d.ts","sourceRoot":"","sources":["../../src/server/campaigns.ts"],"names":[],"mappings":"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"}
|
package/dist/server/campaigns.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Campaigns: one list mail to many recipients through the delivery ledger. The campaign row pins
|
|
2
2
|
// the content (a hash of kind, subject and bodies), so a re-run with the same id sends the rest of
|
|
3
|
-
// the same mail and nothing twice; other content under that id is refused.
|
|
3
|
+
// the same mail and nothing twice; other content under that id is refused. A run stops at the first
|
|
4
|
+
// failure about the sending account (refused key, spent quota), leaving the rest for the next run.
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { err, ok } from "@softure-ai/core";
|
|
6
7
|
import { and, eq, inArray } from "drizzle-orm";
|
|
7
8
|
import { campaigns, deliveries } from "../schema.js";
|
|
8
9
|
import { getCampaignProblems } from "./campaign-file.js";
|
|
9
10
|
import { deliverOnce } from "./deliveries.js";
|
|
11
|
+
import { getMailingOptions } from "./options.js";
|
|
10
12
|
import { isSuppressed } from "./suppressions.js";
|
|
11
13
|
import { getRecipientKey } from "./unsubscribe-link.js";
|
|
12
14
|
/** Rejections the provider answered (the others never left the process). */
|
|
@@ -31,7 +33,9 @@ export async function registerCampaign(ctx, content) {
|
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* Sends `campaign` to every recipient that has no outcome yet, one at a time. Recipients who
|
|
34
|
-
* unsubscribed are rejected (`mailing.suppressed`) and never retried
|
|
36
|
+
* unsubscribed are rejected (`mailing.suppressed`) and never retried; recipients the module's
|
|
37
|
+
* `filterCampaignRecipient` refuses are skipped. Stops at the first `halted` delivery. Throws on a
|
|
38
|
+
* database failure and when the filter throws.
|
|
35
39
|
*/
|
|
36
40
|
export async function sendCampaign(ctx, input, options = {}) {
|
|
37
41
|
const { pauseMs = 0, onDelivery, sleep = wait, ...deliverOptions } = options;
|
|
@@ -39,26 +43,50 @@ export async function sendCampaign(ctx, input, options = {}) {
|
|
|
39
43
|
const registered = await registerCampaign(ctx, campaign);
|
|
40
44
|
if (!registered.ok)
|
|
41
45
|
return registered;
|
|
42
|
-
const counts = { sent: 0, done: 0, inFlight: 0, retryLater: 0, rejected: emptyRejections() };
|
|
46
|
+
const counts = { sent: 0, done: 0, inFlight: 0, retryLater: 0, filtered: 0, uncertain: 0, rejected: emptyRejections() };
|
|
43
47
|
const seen = new Set();
|
|
44
48
|
const scope = `campaign:${campaign.id}`;
|
|
49
|
+
const isWanted = createRecipientFilter(ctx, campaign);
|
|
45
50
|
for await (const address of input.recipients) {
|
|
46
51
|
const recipientKey = getRecipientKey(address);
|
|
47
52
|
if (seen.has(recipientKey))
|
|
48
53
|
continue;
|
|
49
54
|
seen.add(recipientKey);
|
|
55
|
+
if (!(await isWanted(address, recipientKey))) {
|
|
56
|
+
counts.filtered += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
50
59
|
const html = campaign.html === null ? {} : { html: campaign.html };
|
|
51
60
|
const outcome = await deliverOnce(ctx, { scope, campaignId: campaign.id, mail: { to: address, subject: campaign.subject, text: campaign.text, kind: campaign.kind, ...html } }, deliverOptions);
|
|
52
61
|
countOutcome(counts, outcome);
|
|
53
62
|
onDelivery?.(outcome);
|
|
63
|
+
if (outcome.status === "halted") {
|
|
64
|
+
// The rest of the list is not even counted as seen: the summary covers what this run reached.
|
|
65
|
+
const httpStatus = outcome.httpStatus === undefined ? {} : { httpStatus: outcome.httpStatus };
|
|
66
|
+
return ok({ recipients: seen.size, ...counts, halted: { reason: outcome.reason, ...httpStatus } });
|
|
67
|
+
}
|
|
54
68
|
const reachedProvider = outcome.status === "sent" || outcome.status === "retry-later" || (outcome.status === "rejected" && PROVIDER_REASONS.has(outcome.reason));
|
|
55
69
|
if (reachedProvider && pauseMs > 0)
|
|
56
70
|
await sleep(pauseMs);
|
|
57
71
|
}
|
|
58
|
-
return ok({ recipients: seen.size, ...counts });
|
|
72
|
+
return ok({ recipients: seen.size, ...counts, halted: null });
|
|
73
|
+
}
|
|
74
|
+
/** `filterCampaignRecipient` bound to the campaign, or "everyone" when the app set none. */
|
|
75
|
+
function createRecipientFilter(ctx, campaign) {
|
|
76
|
+
const filter = getMailingOptions(ctx.config).filterCampaignRecipient;
|
|
77
|
+
if (filter === undefined)
|
|
78
|
+
return () => Promise.resolve(true);
|
|
79
|
+
const target = { id: campaign.id, kind: campaign.kind };
|
|
80
|
+
return (address, recipientKey) => {
|
|
81
|
+
const recipient = { address, recipientKey, campaign: target };
|
|
82
|
+
return filter(recipient, ctx);
|
|
83
|
+
};
|
|
59
84
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
85
|
+
/**
|
|
86
|
+
* What `sendCampaign` would do, without writing or sending anything (the filter runs, and must not write). Throws
|
|
87
|
+
* on a database failure. `toSend` counts uncertain recipients only with `retakeUncertain`.
|
|
88
|
+
*/
|
|
89
|
+
export async function planCampaign(ctx, input, options = {}) {
|
|
62
90
|
const { campaign } = input;
|
|
63
91
|
assertCampaign(campaign);
|
|
64
92
|
const stored = await ctx.db.select({ contentHash: campaigns.contentHash }).from(campaigns).where(eq(campaigns.id, campaign.id)).limit(1);
|
|
@@ -69,21 +97,43 @@ export async function planCampaign(ctx, input) {
|
|
|
69
97
|
if (!addresses.has(key))
|
|
70
98
|
addresses.set(key, address);
|
|
71
99
|
}
|
|
100
|
+
const uncertainBefore = ctx.clock.now().getTime() - (options.uncertainClaimMs ?? getMailingOptions(ctx.config).uncertainClaimMs);
|
|
72
101
|
const closed = new Set();
|
|
102
|
+
const uncertainKeys = new Set();
|
|
73
103
|
const keys = [...addresses.keys()];
|
|
74
104
|
for (let start = 0; start < keys.length; start += 1_000) {
|
|
75
105
|
const rows = await ctx.db
|
|
76
|
-
.select({ recipientKey: deliveries.recipientKey })
|
|
106
|
+
.select({ recipientKey: deliveries.recipientKey, status: deliveries.status, claimedAt: deliveries.claimedAt })
|
|
77
107
|
.from(deliveries)
|
|
78
|
-
.where(and(eq(deliveries.scope, `campaign:${campaign.id}`), inArray(deliveries.status, ["sent", "rejected"]), inArray(deliveries.recipientKey, keys.slice(start, start + 1_000))));
|
|
79
|
-
|
|
108
|
+
.where(and(eq(deliveries.scope, `campaign:${campaign.id}`), inArray(deliveries.status, ["sent", "rejected", "claimed"]), inArray(deliveries.recipientKey, keys.slice(start, start + 1_000))));
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
if (row.status !== "claimed")
|
|
111
|
+
closed.add(row.recipientKey);
|
|
112
|
+
else if (row.claimedAt.getTime() < uncertainBefore)
|
|
113
|
+
uncertainKeys.add(row.recipientKey);
|
|
114
|
+
}
|
|
80
115
|
}
|
|
116
|
+
const isWanted = createRecipientFilter(ctx, campaign);
|
|
81
117
|
let suppressed = 0;
|
|
118
|
+
let filtered = 0;
|
|
119
|
+
let toSend = 0;
|
|
120
|
+
let uncertain = 0;
|
|
82
121
|
for (const [key, address] of addresses) {
|
|
83
|
-
if (
|
|
122
|
+
if (closed.has(key))
|
|
123
|
+
continue;
|
|
124
|
+
if (!(await isWanted(address, key)))
|
|
125
|
+
filtered += 1;
|
|
126
|
+
else if (await isSuppressed(ctx, address))
|
|
84
127
|
suppressed += 1;
|
|
128
|
+
else if (uncertainKeys.has(key)) {
|
|
129
|
+
uncertain += 1;
|
|
130
|
+
if (options.retakeUncertain === true)
|
|
131
|
+
toSend += 1;
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
toSend += 1;
|
|
85
135
|
}
|
|
86
|
-
return { recipients: addresses.size, done: closed.size, suppressed,
|
|
136
|
+
return { recipients: addresses.size, done: closed.size, suppressed, filtered, uncertain, toSend, contentChanged };
|
|
87
137
|
}
|
|
88
138
|
function assertCampaign(content) {
|
|
89
139
|
const problems = getCampaignProblems(content);
|
|
@@ -91,7 +141,7 @@ function assertCampaign(content) {
|
|
|
91
141
|
throw new Error(`@softure-ai/mailing: campaign "${content.id}" cannot be sent: ${problems.join("; ")}`);
|
|
92
142
|
}
|
|
93
143
|
function emptyRejections() {
|
|
94
|
-
return { "mailing.invalid_input": 0, "mailing.rejected": 0, "mailing.unavailable": 0, "mailing.suppressed": 0 };
|
|
144
|
+
return { "mailing.invalid_input": 0, "mailing.rejected": 0, "mailing.unavailable": 0, "mailing.suppressed": 0, "mailing.provider_refused": 0, "mailing.quota_exceeded": 0 };
|
|
95
145
|
}
|
|
96
146
|
function countOutcome(counts, outcome) {
|
|
97
147
|
switch (outcome.status) {
|
|
@@ -108,8 +158,13 @@ function countOutcome(counts, outcome) {
|
|
|
108
158
|
counts.inFlight += 1;
|
|
109
159
|
return;
|
|
110
160
|
case "retry-later":
|
|
161
|
+
case "halted":
|
|
162
|
+
// A halted recipient is left for the next run, like one the provider could not take now.
|
|
111
163
|
counts.retryLater += 1;
|
|
112
164
|
return;
|
|
165
|
+
case "uncertain":
|
|
166
|
+
counts.uncertain += 1;
|
|
167
|
+
return;
|
|
113
168
|
}
|
|
114
169
|
}
|
|
115
170
|
function wait(ms) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"campaigns.js","sourceRoot":"","sources":["../../src/server/campaigns.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,mGAAmG;AACnG,
|
|
1
|
+
{"version":3,"file":"campaigns.js","sourceRoot":"","sources":["../../src/server/campaigns.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type ModuleContext } from "@softure-ai/core";
|
|
2
2
|
import type { Queryable } from "@softure-ai/db";
|
|
3
3
|
import { type MailingErrorCode, type OutgoingMail } from "../contract.js";
|
|
4
|
+
import { DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS } from "../options.js";
|
|
4
5
|
export type DeliveryContext = ModuleContext<Queryable>;
|
|
5
|
-
|
|
6
|
-
export declare const DEFAULT_STALE_CLAIM_MS: number;
|
|
6
|
+
export { DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS };
|
|
7
7
|
/** Claims a delivery gets before `mailing.unavailable` becomes its final outcome. */
|
|
8
8
|
export declare const DEFAULT_MAX_ATTEMPTS = 5;
|
|
9
9
|
export interface Delivery {
|
|
@@ -19,9 +19,19 @@ export interface Delivery {
|
|
|
19
19
|
readonly campaignId?: string;
|
|
20
20
|
}
|
|
21
21
|
export interface DeliverOptions {
|
|
22
|
+
/** Default: the module's `staleClaimMs` (15 minutes). */
|
|
22
23
|
readonly staleClaimMs?: number;
|
|
24
|
+
/** Default: the module's `uncertainClaimMs` (23 hours). */
|
|
25
|
+
readonly uncertainClaimMs?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Takes over uncertain claims too (older than `uncertainClaimMs`), accepting that their mail may arrive twice.
|
|
28
|
+
* An operator's decision, e.g. `softure-mail campaign --resend-uncertain`. Default false.
|
|
29
|
+
*/
|
|
30
|
+
readonly retakeUncertain?: boolean;
|
|
23
31
|
readonly maxAttempts?: number;
|
|
24
32
|
}
|
|
33
|
+
/** The codes that stop a run: the sending account, not the mail, is the problem. */
|
|
34
|
+
export type HaltingErrorCode = "mailing.provider_refused" | "mailing.quota_exceeded";
|
|
25
35
|
export type DeliveryOutcome =
|
|
26
36
|
/** Sent now; `id` is the provider's message id. */
|
|
27
37
|
{
|
|
@@ -32,6 +42,7 @@ export type DeliveryOutcome =
|
|
|
32
42
|
| {
|
|
33
43
|
readonly status: "rejected";
|
|
34
44
|
readonly reason: MailingErrorCode;
|
|
45
|
+
readonly httpStatus?: number;
|
|
35
46
|
}
|
|
36
47
|
/** An earlier run already closed this delivery; nothing was sent. */
|
|
37
48
|
| {
|
|
@@ -42,9 +53,26 @@ export type DeliveryOutcome =
|
|
|
42
53
|
| {
|
|
43
54
|
readonly status: "in-flight";
|
|
44
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A claim older than `uncertainClaimMs` is open: its mail may have gone out. Nothing was sent; only
|
|
58
|
+
* `retakeUncertain` sends it again.
|
|
59
|
+
*/
|
|
60
|
+
| {
|
|
61
|
+
readonly status: "uncertain";
|
|
62
|
+
}
|
|
45
63
|
/** The provider was unavailable; the claim is released for a later run. */
|
|
46
64
|
| {
|
|
47
65
|
readonly status: "retry-later";
|
|
66
|
+
readonly httpStatus?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The provider refused the account or its quota is spent: the claim is released, its attempt given back. Stop
|
|
70
|
+
* sending (every other mail fails the same way) and run again once the account can send.
|
|
71
|
+
*/
|
|
72
|
+
| {
|
|
73
|
+
readonly status: "halted";
|
|
74
|
+
readonly reason: HaltingErrorCode;
|
|
75
|
+
readonly httpStatus?: number;
|
|
48
76
|
};
|
|
49
77
|
/**
|
|
50
78
|
* Sends `delivery.mail` unless the ledger already has an outcome (or a fresh claim) for its scope
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliveries.d.ts","sourceRoot":"","sources":["../../src/server/deliveries.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deliveries.d.ts","sourceRoot":"","sources":["../../src/server/deliveries.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAA2C,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAMnF,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAEvD,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,CAAC;AAC9D,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAKtC,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG,0BAA0B,GAAG,wBAAwB,CAAC;AAErF,MAAM,MAAM,eAAe;AACzB,mDAAmD;AACjD;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAClD,4FAA4F;GAC1F;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE;AAClG,qEAAqE;GACnE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE;AACpE,4DAA4D;GAC1D;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE;AAClC;;;GAGG;GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE;AAClC,2EAA2E;GACzE;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE;AAClE;;;GAGG;GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnG;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6BlI"}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
// The delivery ledger (mailing.deliveries): a mail goes out at most once per scope and recipient.
|
|
2
2
|
// The sender claims the row in one conditional upsert before it calls the provider, then closes it
|
|
3
3
|
// with one outcome. A claim left behind by a crash is taken over once it is stale, and the mail is
|
|
4
|
-
// sent again with the same idempotency key, so the provider folds it into the first send.
|
|
4
|
+
// sent again with the same idempotency key, so the provider folds it into the first send. A claim
|
|
5
|
+
// older than the provider keeps that key is "uncertain": it is taken over only when the operator
|
|
6
|
+
// says so. A failure about the sending account (refused key, spent quota) closes nothing: the
|
|
7
|
+
// claim goes back, attempt included, so fixing the account and re-running loses no recipient.
|
|
5
8
|
import {} from "@softure-ai/core";
|
|
6
|
-
import { and, eq, lt, or, sql } from "drizzle-orm";
|
|
9
|
+
import { and, eq, gte, lt, or, sql } from "drizzle-orm";
|
|
7
10
|
import { isMailKind } from "../address.js";
|
|
8
|
-
import { TRANSACTIONAL_KIND } from "../contract.js";
|
|
11
|
+
import { HALTING_ERROR_CODES, TRANSACTIONAL_KIND } from "../contract.js";
|
|
12
|
+
import { DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS } from "../options.js";
|
|
9
13
|
import { deliveries } from "../schema.js";
|
|
14
|
+
import { getMailingOptions } from "./options.js";
|
|
10
15
|
import { sendMail } from "./send-mail.js";
|
|
11
16
|
import { getRecipientKey } from "./unsubscribe-link.js";
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_STALE_CLAIM_MS = 15 * 60_000;
|
|
17
|
+
export { DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS };
|
|
14
18
|
/** Claims a delivery gets before `mailing.unavailable` becomes its final outcome. */
|
|
15
19
|
export const DEFAULT_MAX_ATTEMPTS = 5;
|
|
16
20
|
const MAX_SCOPE_LENGTH = 128;
|
|
@@ -22,26 +26,46 @@ const SCOPE = /^[a-z0-9][a-z0-9._:-]*$/;
|
|
|
22
26
|
* malformed scope or kind, and when `sendMail` throws.
|
|
23
27
|
*/
|
|
24
28
|
export async function deliverOnce(ctx, delivery, options = {}) {
|
|
25
|
-
const
|
|
29
|
+
const windows = resolveClaimWindows(ctx, options);
|
|
30
|
+
const { maxAttempts = DEFAULT_MAX_ATTEMPTS } = options;
|
|
26
31
|
const kind = delivery.mail.kind ?? TRANSACTIONAL_KIND;
|
|
27
32
|
assertDelivery(delivery, kind);
|
|
28
33
|
const recipientKey = getRecipientKey(delivery.mail.to);
|
|
29
34
|
const key = { scope: delivery.scope, recipientKey };
|
|
30
|
-
const attempt = await claimDelivery(ctx, { ...key, kind, campaignId: delivery.campaignId ?? null,
|
|
35
|
+
const attempt = await claimDelivery(ctx, { ...key, kind, campaignId: delivery.campaignId ?? null, ...windows });
|
|
31
36
|
if (attempt === null)
|
|
32
|
-
return readClosedDelivery(ctx, key);
|
|
37
|
+
return readClosedDelivery(ctx, key, windows.uncertainClaimMs);
|
|
33
38
|
const result = await sendMail(ctx, delivery.mail, { idempotencyKey: `${delivery.scope}:${recipientKey}` });
|
|
34
39
|
const fence = { ...key, attempt };
|
|
35
40
|
if (result.ok) {
|
|
36
|
-
await closeDelivery(ctx, fence, { status: "sent", providerMessageId: result.value.id, reason: null });
|
|
41
|
+
await closeDelivery(ctx, fence, { status: "sent", providerMessageId: result.value.id, reason: null, providerStatus: null });
|
|
37
42
|
return { status: "sent", id: result.value.id };
|
|
38
43
|
}
|
|
44
|
+
const providerStatus = result.httpStatus ?? null;
|
|
45
|
+
const httpStatus = result.httpStatus === undefined ? {} : { httpStatus: result.httpStatus };
|
|
46
|
+
if (isHaltingCode(result.error)) {
|
|
47
|
+
await releaseDelivery(ctx, fence, { providerStatus, giveAttemptBack: true });
|
|
48
|
+
return { status: "halted", reason: result.error, ...httpStatus };
|
|
49
|
+
}
|
|
39
50
|
if (result.error === "mailing.unavailable" && attempt < maxAttempts) {
|
|
40
|
-
await releaseDelivery(ctx, fence);
|
|
41
|
-
return { status: "retry-later" };
|
|
51
|
+
await releaseDelivery(ctx, fence, { providerStatus, giveAttemptBack: false });
|
|
52
|
+
return { status: "retry-later", ...httpStatus };
|
|
53
|
+
}
|
|
54
|
+
await closeDelivery(ctx, fence, { status: "rejected", providerMessageId: null, reason: result.error, providerStatus });
|
|
55
|
+
return { status: "rejected", reason: result.error, ...httpStatus };
|
|
56
|
+
}
|
|
57
|
+
function isHaltingCode(code) {
|
|
58
|
+
return HALTING_ERROR_CODES.has(code);
|
|
59
|
+
}
|
|
60
|
+
/** The claim windows: the call's options, else the module's, else the defaults. */
|
|
61
|
+
function resolveClaimWindows(ctx, options) {
|
|
62
|
+
const configured = getMailingOptions(ctx.config);
|
|
63
|
+
const staleClaimMs = options.staleClaimMs ?? configured.staleClaimMs;
|
|
64
|
+
const uncertainClaimMs = options.uncertainClaimMs ?? configured.uncertainClaimMs;
|
|
65
|
+
if (uncertainClaimMs <= staleClaimMs) {
|
|
66
|
+
throw new Error(`@softure-ai/mailing: uncertainClaimMs (${String(uncertainClaimMs)}) must be more than staleClaimMs (${String(staleClaimMs)})`);
|
|
42
67
|
}
|
|
43
|
-
|
|
44
|
-
return { status: "rejected", reason: result.error };
|
|
68
|
+
return { staleClaimMs, uncertainClaimMs, retakeUncertain: options.retakeUncertain ?? false };
|
|
45
69
|
}
|
|
46
70
|
function assertDelivery(delivery, kind) {
|
|
47
71
|
if (delivery.scope.length > MAX_SCOPE_LENGTH || !SCOPE.test(delivery.scope)) {
|
|
@@ -56,28 +80,35 @@ function assertDelivery(delivery, kind) {
|
|
|
56
80
|
}
|
|
57
81
|
/**
|
|
58
82
|
* Takes the row in one statement: a new row, a `pending` one, or a claim older than
|
|
59
|
-
* `staleClaimMs
|
|
60
|
-
* is closed
|
|
83
|
+
* `staleClaimMs` (and, unless `retakeUncertain`, not older than `uncertainClaimMs`). Resolves with
|
|
84
|
+
* the attempt number that fences the outcome, or `null` when the row is closed, claimed by someone
|
|
85
|
+
* else, or uncertain.
|
|
61
86
|
*/
|
|
62
87
|
async function claimDelivery(ctx, claim) {
|
|
63
88
|
const now = ctx.clock.now();
|
|
64
89
|
const staleBefore = new Date(now.getTime() - claim.staleClaimMs);
|
|
90
|
+
const uncertainBefore = new Date(now.getTime() - claim.uncertainClaimMs);
|
|
91
|
+
const isStale = claim.retakeUncertain
|
|
92
|
+
? lt(deliveries.claimedAt, staleBefore)
|
|
93
|
+
: and(lt(deliveries.claimedAt, staleBefore), gte(deliveries.claimedAt, uncertainBefore));
|
|
65
94
|
const rows = await ctx.db
|
|
66
95
|
.insert(deliveries)
|
|
67
96
|
.values({ scope: claim.scope, recipientKey: claim.recipientKey, kind: claim.kind, campaignId: claim.campaignId, status: "claimed", attempts: 1, claimedAt: now, createdAt: now })
|
|
68
97
|
.onConflictDoUpdate({
|
|
69
98
|
target: [deliveries.scope, deliveries.recipientKey],
|
|
70
99
|
set: { status: "claimed", attempts: sql `${deliveries.attempts} + 1`, claimedAt: now },
|
|
71
|
-
setWhere: or(eq(deliveries.status, "pending"), and(eq(deliveries.status, "claimed"),
|
|
100
|
+
setWhere: or(eq(deliveries.status, "pending"), and(eq(deliveries.status, "claimed"), isStale)),
|
|
72
101
|
})
|
|
73
102
|
.returning();
|
|
74
103
|
return rows[0]?.attempts ?? null;
|
|
75
104
|
}
|
|
76
|
-
async function readClosedDelivery(ctx, key) {
|
|
77
|
-
const rows = await ctx.db.select({ status: deliveries.status }).from(deliveries).where(matchKey(key)).limit(1);
|
|
78
|
-
const
|
|
79
|
-
if (status === "sent" || status === "rejected")
|
|
80
|
-
return { status: "done", outcome: status };
|
|
105
|
+
async function readClosedDelivery(ctx, key, uncertainClaimMs) {
|
|
106
|
+
const rows = await ctx.db.select({ status: deliveries.status, claimedAt: deliveries.claimedAt }).from(deliveries).where(matchKey(key)).limit(1);
|
|
107
|
+
const row = rows[0];
|
|
108
|
+
if (row?.status === "sent" || row?.status === "rejected")
|
|
109
|
+
return { status: "done", outcome: row.status };
|
|
110
|
+
if (row?.status === "claimed" && row.claimedAt.getTime() < ctx.clock.now().getTime() - uncertainClaimMs)
|
|
111
|
+
return { status: "uncertain" };
|
|
81
112
|
// A fresh claim; `pending` only between another sender's release and this read.
|
|
82
113
|
return { status: "in-flight" };
|
|
83
114
|
}
|
|
@@ -88,10 +119,14 @@ async function closeDelivery(ctx, fence, outcome) {
|
|
|
88
119
|
.set({ ...outcome, finishedAt: ctx.clock.now() })
|
|
89
120
|
.where(and(matchKey(fence), eq(deliveries.status, "claimed"), eq(deliveries.attempts, fence.attempt)));
|
|
90
121
|
}
|
|
91
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Puts the claim back for a later run. A halt gives its attempt back: the account's failure says
|
|
124
|
+
* nothing about this mail, so it must not bring the delivery closer to `maxAttempts`.
|
|
125
|
+
*/
|
|
126
|
+
async function releaseDelivery(ctx, fence, release) {
|
|
92
127
|
await ctx.db
|
|
93
128
|
.update(deliveries)
|
|
94
|
-
.set({ status: "pending" })
|
|
129
|
+
.set({ status: "pending", providerStatus: release.providerStatus, ...(release.giveAttemptBack ? { attempts: fence.attempt - 1 } : {}) })
|
|
95
130
|
.where(and(matchKey(fence), eq(deliveries.status, "claimed"), eq(deliveries.attempts, fence.attempt)));
|
|
96
131
|
}
|
|
97
132
|
function matchKey(key) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliveries.js","sourceRoot":"","sources":["../../src/server/deliveries.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,mGAAmG;AACnG,mGAAmG;AACnG,
|
|
1
|
+
{"version":3,"file":"deliveries.js","sourceRoot":"","sources":["../../src/server/deliveries.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { getCampaignProblems, parseCampaignFile, parseRecipientList, type CampaignContent, type CampaignFile, type CampaignFileResult } from "./campaign-file.js";
|
|
2
|
-
export { getCampaignContentHash, planCampaign, registerCampaign, sendCampaign, type CampaignErrorCode, type CampaignPlan, type CampaignSummary, type SendCampaignOptions, } from "./campaigns.js";
|
|
3
|
-
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_STALE_CLAIM_MS, deliverOnce, type DeliverOptions, type Delivery, type DeliveryContext, type DeliveryOutcome, } from "./deliveries.js";
|
|
2
|
+
export { getCampaignContentHash, planCampaign, registerCampaign, sendCampaign, type CampaignErrorCode, type CampaignHalt, type CampaignPlan, type CampaignSummary, type SendCampaignOptions, } from "./campaigns.js";
|
|
3
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS, deliverOnce, type DeliverOptions, type Delivery, type DeliveryContext, type DeliveryOutcome, type HaltingErrorCode, } from "./deliveries.js";
|
|
4
4
|
export { checkSenderDns, DEFAULT_DKIM_SELECTORS, getSenderDomain, type CheckSenderDnsOptions, type DnsCheck, type DnsCheckStatus, type DnsFinding, type ResolveTxt, type SenderDnsReport, } from "./dns.js";
|
|
5
5
|
export { checkMailingTables } from "./health.js";
|
|
6
6
|
export { addHtmlFooter, addTextFooter, getListUnsubscribeHeaders, SIGNATURE_SEPARATOR } from "./list-mail.js";
|
|
7
7
|
export { getMailingModule, getMailingOptions, getMailingRoutes, type MailingRoutes } from "./options.js";
|
|
8
8
|
export { sendMail, type MailContext } from "./send-mail.js";
|
|
9
9
|
export { isSuppressed, liftSuppression, suppressRecipient, unsubscribe, type SuppressionContext } from "./suppressions.js";
|
|
10
|
-
export { buildUnsubscribeLinks, getRecipientKey, MIN_UNSUBSCRIBE_SECRET_LENGTH, readUnsubscribeSecrets, readUnsubscribeToken, RECIPIENT_PARAM, signRecipientKey, SIGNATURE_PARAM, UNSUBSCRIBE_PREVIOUS_SECRET_ENV, UNSUBSCRIBE_SECRET_ENV, verifyUnsubscribeToken, type Env, type UnsubscribeLinks, type UnsubscribeSecrets, type UnsubscribeToken, } from "./unsubscribe-link.js";
|
|
10
|
+
export { buildUnsubscribeLinks, getRecipientKey, getUnsubscribeLinkParams, MAX_LEGACY_VALUE_LENGTH, readUnsubscribeLink, MIN_UNSUBSCRIBE_SECRET_LENGTH, readUnsubscribeSecrets, readUnsubscribeToken, RECIPIENT_PARAM, signRecipientKey, SIGNATURE_PARAM, UNSUBSCRIBE_PREVIOUS_SECRET_ENV, UNSUBSCRIBE_SECRET_ENV, verifyUnsubscribeToken, type Env, type LinkParams, type UnsubscribeLink, type UnsubscribeLinks, type UnsubscribeSecrets, type UnsubscribeToken, } from "./unsubscribe-link.js";
|
|
11
11
|
export { validateMail, type MailValidation, type ValidMail } from "./validate-mail.js";
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAClK,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAClK,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC9G,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3H,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,6BAA6B,EAC7B,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,+BAA+B,EAC/B,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// scope; `next/*` imports are not allowed here (ESLint `no-restricted-imports`, NFR-3).
|
|
3
3
|
export { getCampaignProblems, parseCampaignFile, parseRecipientList } from "./campaign-file.js";
|
|
4
4
|
export { getCampaignContentHash, planCampaign, registerCampaign, sendCampaign, } from "./campaigns.js";
|
|
5
|
-
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_STALE_CLAIM_MS, deliverOnce, } from "./deliveries.js";
|
|
5
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_STALE_CLAIM_MS, DEFAULT_UNCERTAIN_CLAIM_MS, deliverOnce, } from "./deliveries.js";
|
|
6
6
|
export { checkSenderDns, DEFAULT_DKIM_SELECTORS, getSenderDomain, } from "./dns.js";
|
|
7
7
|
export { checkMailingTables } from "./health.js";
|
|
8
8
|
export { addHtmlFooter, addTextFooter, getListUnsubscribeHeaders, SIGNATURE_SEPARATOR } from "./list-mail.js";
|
|
9
9
|
export { getMailingModule, getMailingOptions, getMailingRoutes } from "./options.js";
|
|
10
10
|
export { sendMail } from "./send-mail.js";
|
|
11
11
|
export { isSuppressed, liftSuppression, suppressRecipient, unsubscribe } from "./suppressions.js";
|
|
12
|
-
export { buildUnsubscribeLinks, getRecipientKey, MIN_UNSUBSCRIBE_SECRET_LENGTH, readUnsubscribeSecrets, readUnsubscribeToken, RECIPIENT_PARAM, signRecipientKey, SIGNATURE_PARAM, UNSUBSCRIBE_PREVIOUS_SECRET_ENV, UNSUBSCRIBE_SECRET_ENV, verifyUnsubscribeToken, } from "./unsubscribe-link.js";
|
|
12
|
+
export { buildUnsubscribeLinks, getRecipientKey, getUnsubscribeLinkParams, MAX_LEGACY_VALUE_LENGTH, readUnsubscribeLink, MIN_UNSUBSCRIBE_SECRET_LENGTH, readUnsubscribeSecrets, readUnsubscribeToken, RECIPIENT_PARAM, signRecipientKey, SIGNATURE_PARAM, UNSUBSCRIBE_PREVIOUS_SECRET_ENV, UNSUBSCRIBE_SECRET_ENV, verifyUnsubscribeToken, } from "./unsubscribe-link.js";
|
|
13
13
|
export { validateMail } from "./validate-mail.js";
|
|
14
14
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,wFAAwF;AACxF,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAoE,MAAM,oBAAoB,CAAC;AAClK,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,wFAAwF;AACxF,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAoE,MAAM,oBAAoB,CAAC;AAClK,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,YAAY,GAMb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,WAAW,GAMZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,GAOhB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC9G,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAsB,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,QAAQ,EAAoB,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AAC3H,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,6BAA6B,EAC7B,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,+BAA+B,EAC/B,sBAAsB,EACtB,sBAAsB,GAOvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAuC,MAAM,oBAAoB,CAAC"}
|
|
@@ -12,7 +12,9 @@ export interface MailContext {
|
|
|
12
12
|
/**
|
|
13
13
|
* Sends `mail` and resolves with the provider's message id or a `mailing.*` code:
|
|
14
14
|
* `invalid_input` (fix the input), `rejected` (do not retry), `unavailable` (retry later with the
|
|
15
|
-
* same `idempotencyKey`), `suppressed` (a list mail to someone who unsubscribed; do not retry)
|
|
15
|
+
* same `idempotencyKey`), `suppressed` (a list mail to someone who unsubscribed; do not retry),
|
|
16
|
+
* `provider_refused` / `quota_exceeded` (the account cannot send: stop, fix or wait, then retry). A
|
|
17
|
+
* failure the provider answered with an HTTP status carries it as `httpStatus`.
|
|
16
18
|
* Throws only when the module is not enabled, or for list mail without `context.db`.
|
|
17
19
|
*/
|
|
18
20
|
export declare function sendMail(context: MailContext, mail: OutgoingMail, options?: SendMailOptions): Promise<SendMailResult>;
|
|
@@ -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;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,
|
|
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;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAIL,KAAK,YAAY,EAIjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AAQxB;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC;CACzB;AAID;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CA2B/H"}
|