@softure-ai/mailing 0.1.7 → 0.1.9
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 +41 -0
- package/README.md +131 -23
- package/dist/cli/run.d.ts +6 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +297 -40
- package/dist/cli/run.js.map +1 -1
- package/dist/contract.d.ts +35 -5
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next/import-deliveries.d.ts +4 -0
- package/dist/next/import-deliveries.d.ts.map +1 -0
- package/dist/next/import-deliveries.js +9 -0
- package/dist/next/import-deliveries.js.map +1 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +1 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/route.d.ts +3 -2
- package/dist/next/route.d.ts.map +1 -1
- package/dist/next/route.js +5 -4
- package/dist/next/route.js.map +1 -1
- package/dist/options.d.ts +19 -2
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +30 -6
- package/dist/options.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 +5 -0
- package/dist/server/campaigns.d.ts.map +1 -1
- package/dist/server/campaigns.js +10 -0
- package/dist/server/campaigns.js.map +1 -1
- package/dist/server/deliveries.d.ts +9 -5
- package/dist/server/deliveries.d.ts.map +1 -1
- package/dist/server/deliveries.js +9 -7
- package/dist/server/deliveries.js.map +1 -1
- package/dist/server/dns.d.ts +48 -1
- package/dist/server/dns.d.ts.map +1 -1
- package/dist/server/dns.js +124 -15
- package/dist/server/dns.js.map +1 -1
- package/dist/server/import-deliveries.d.ts +68 -0
- package/dist/server/import-deliveries.d.ts.map +1 -0
- package/dist/server/import-deliveries.js +105 -0
- package/dist/server/import-deliveries.js.map +1 -0
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/suppressions.d.ts.map +1 -1
- package/dist/server/suppressions.js +9 -6
- package/dist/server/suppressions.js.map +1 -1
- package/dist/server/unsubscribe-link.d.ts +3 -2
- package/dist/server/unsubscribe-link.d.ts.map +1 -1
- package/dist/server/unsubscribe-link.js +12 -3
- package/dist/server/unsubscribe-link.js.map +1 -1
- package/migrations/0004_allow_imported_deliveries.sql +20 -0
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/cli/run.ts +311 -39
- package/src/contract.ts +30 -5
- package/src/index.ts +4 -1
- package/src/next/import-deliveries.ts +9 -0
- package/src/next/index.ts +1 -0
- package/src/next/route.ts +5 -4
- package/src/options.ts +36 -7
- package/src/schema.ts +2 -0
- package/src/server/campaigns.ts +13 -0
- package/src/server/deliveries.ts +15 -8
- package/src/server/dns.ts +174 -16
- package/src/server/import-deliveries.ts +149 -0
- package/src/server/index.ts +21 -0
- package/src/server/suppressions.ts +15 -8
- package/src/server/unsubscribe-link.ts +10 -3
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { err, ok, type ModuleContext, type Result } from "@softure-ai/core";
|
|
7
7
|
import type { Queryable } from "@softure-ai/db";
|
|
8
8
|
import { and, eq, inArray } from "drizzle-orm";
|
|
9
|
-
import type { SuppressionSource, UnsubscribeErrorCode } from "../contract.js";
|
|
9
|
+
import type { SuppressionSource, UnsubscribeErrorCode, VerifiedUnsubscribeLink } from "../contract.js";
|
|
10
10
|
import { suppressions } from "../schema.js";
|
|
11
11
|
import { getMailingOptions } from "./options.js";
|
|
12
12
|
import { getRecipientKey, readUnsubscribeSecrets, verifyUnsubscribeToken, type Env, type UnsubscribeLink, type UnsubscribeToken } from "./unsubscribe-link.js";
|
|
@@ -67,26 +67,33 @@ export async function unsubscribe(
|
|
|
67
67
|
source: Exclude<SuppressionSource, "operator">,
|
|
68
68
|
env: Env = process.env,
|
|
69
69
|
): Promise<Result<undefined, UnsubscribeErrorCode>> {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
70
|
+
const verified = await verifyLink(ctx, link, env);
|
|
71
|
+
if (verified === null) return err("mailing.invalid_link");
|
|
72
|
+
const { recipientKey } = verified;
|
|
72
73
|
const { onUnsubscribed } = getMailingOptions(ctx.config);
|
|
73
74
|
await ctx.db.transaction(async (tx) => {
|
|
74
75
|
const txCtx: SuppressionContext = { ...ctx, db: tx };
|
|
75
76
|
await recordSuppression(txCtx, recipientKey, source);
|
|
76
|
-
await onUnsubscribed?.({ recipientKey, source }, txCtx);
|
|
77
|
+
await onUnsubscribed?.({ recipientKey, source, link: verified.link }, txCtx);
|
|
77
78
|
});
|
|
78
79
|
return ok();
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
interface VerifiedLink {
|
|
83
|
+
readonly recipientKey: string;
|
|
84
|
+
readonly link: VerifiedUnsubscribeLink;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The recipient key a link proves and the link it came through, or `null` when it proves none. */
|
|
88
|
+
async function verifyLink(ctx: SuppressionContext, link: UnsubscribeLink | UnsubscribeToken | null, env: Env): Promise<VerifiedLink | null> {
|
|
83
89
|
if (link === null) return null;
|
|
84
90
|
if (!("scheme" in link) || link.scheme === "signed") {
|
|
85
91
|
const token = "scheme" in link ? link.token : link;
|
|
86
|
-
return verifyUnsubscribeToken(token, readUnsubscribeSecrets(env)) ? token.recipientKey : null;
|
|
92
|
+
return verifyUnsubscribeToken(token, readUnsubscribeSecrets(env)) ? { recipientKey: token.recipientKey, link: { scheme: "signed" } } : null;
|
|
87
93
|
}
|
|
88
94
|
const legacy = getMailingOptions(ctx.config).legacyUnsubscribe;
|
|
89
95
|
if (legacy === undefined) return null;
|
|
90
96
|
const address: unknown = await legacy.verify(link.values, ctx);
|
|
91
|
-
|
|
97
|
+
if (typeof address !== "string" || address.trim() === "") return null;
|
|
98
|
+
return { recipientKey: getRecipientKey(address), link: { scheme: "legacy", values: link.values } };
|
|
92
99
|
}
|
|
@@ -117,8 +117,9 @@ export interface LinkParams {
|
|
|
117
117
|
/**
|
|
118
118
|
* The link in a query or a form. A link with the recipient parameter (`r`) is a signed link, whatever else it
|
|
119
119
|
* carries, so a crafted URL cannot route a signed link to the app's legacy check. Without it, and with
|
|
120
|
-
* `mailing({ legacyUnsubscribe })`, a link that has every legacy parameter (non-empty, at most 512
|
|
121
|
-
* a legacy link
|
|
120
|
+
* `mailing({ legacyUnsubscribe })`, a link that has every required legacy parameter (non-empty, at most 512
|
|
121
|
+
* characters) is a legacy link; its values also hold the optional parameters it has (an empty one is absent, one
|
|
122
|
+
* over 512 characters makes the link invalid). Anything else is `null`. Checks no signature: `unsubscribe` does.
|
|
122
123
|
*/
|
|
123
124
|
export function readUnsubscribeLink(params: LinkParams, config: SoftureConfig): UnsubscribeLink | null {
|
|
124
125
|
if (params.get(RECIPIENT_PARAM) !== null && params.get(RECIPIENT_PARAM) !== undefined) {
|
|
@@ -128,11 +129,17 @@ export function readUnsubscribeLink(params: LinkParams, config: SoftureConfig):
|
|
|
128
129
|
const legacy = getMailingOptions(config).legacyUnsubscribe;
|
|
129
130
|
if (legacy === undefined) return null;
|
|
130
131
|
const values: Record<string, string> = {};
|
|
131
|
-
for (const name of legacy.params) {
|
|
132
|
+
for (const name of legacy.params.required) {
|
|
132
133
|
const value = params.get(name);
|
|
133
134
|
if (typeof value !== "string" || value === "" || value.length > MAX_LEGACY_VALUE_LENGTH) return null;
|
|
134
135
|
values[name] = value;
|
|
135
136
|
}
|
|
137
|
+
for (const name of legacy.params.optional) {
|
|
138
|
+
const value = params.get(name);
|
|
139
|
+
if (typeof value !== "string" || value === "") continue;
|
|
140
|
+
if (value.length > MAX_LEGACY_VALUE_LENGTH) return null;
|
|
141
|
+
values[name] = value;
|
|
142
|
+
}
|
|
136
143
|
return { scheme: "legacy", values };
|
|
137
144
|
}
|
|
138
145
|
|