@freecorps/ember-email 0.5.6 → 0.5.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/README.md +4 -0
- package/dist/client.d.ts +2 -0
- package/dist/cloudflare/api.d.ts +9 -0
- package/dist/cloudflare/api.js +22 -0
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/onboard.d.ts +7 -0
- package/dist/cloudflare/onboard.js +26 -1
- package/dist/component.d.ts +4 -0
- package/dist/component.js +26 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -97,6 +97,10 @@ What it does:
|
|
|
97
97
|
routing subdomain, so removal is a dashboard step.
|
|
98
98
|
- **Mailboxes:** it adds one literal rule per mailbox address on the domain → your Worker. It never changes a rule
|
|
99
99
|
that routes somewhere else.
|
|
100
|
+
- **Catch-all:** with `catchAll: true` (a mailbox of the domain is its catch-all, `upsertMailbox({ catchAll: true })`),
|
|
101
|
+
the zone's catch-all rule → your Worker, so every address no mailbox has reaches that mailbox, with the address it
|
|
102
|
+
was sent to in its To. Only for the zone apex, since the rule belongs to the zone. It never takes over a catch-all
|
|
103
|
+
that routes somewhere else. `destroyDomain`, or a sync without `catchAll`, turns it back off.
|
|
100
104
|
- **Events:** it adds an `email.sending` event subscription → the component's queue.
|
|
101
105
|
|
|
102
106
|
## Offline replicas: `@freecorps/ember-email/schema`
|
package/dist/client.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface EmailComponentApi {
|
|
|
133
133
|
text: string;
|
|
134
134
|
html?: string;
|
|
135
135
|
} | null;
|
|
136
|
+
catchAll?: boolean | null;
|
|
136
137
|
}, string>;
|
|
137
138
|
removeMailbox: Ref<"mutation", {
|
|
138
139
|
name: string;
|
|
@@ -143,6 +144,7 @@ export interface EmailComponentApi {
|
|
|
143
144
|
address: string;
|
|
144
145
|
displayName?: string;
|
|
145
146
|
autoReply?: unknown;
|
|
147
|
+
catchAll?: boolean;
|
|
146
148
|
}>>;
|
|
147
149
|
route: Ref<"query", {
|
|
148
150
|
to: string;
|
package/dist/cloudflare/api.d.ts
CHANGED
|
@@ -54,6 +54,15 @@ export declare function ensureWorkerRule(c: CfCreds, zoneId: string, address: st
|
|
|
54
54
|
id: string;
|
|
55
55
|
created: boolean;
|
|
56
56
|
}>;
|
|
57
|
+
/**
|
|
58
|
+
* The zone's catch-all rule → this Worker (every address no literal rule takes). Left alone, with an error, when it
|
|
59
|
+
* is on and sends mail somewhere else; a disabled or "drop" catch-all is taken over.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ensureCatchAllRule(c: CfCreds, zoneId: string, worker: string): Promise<{
|
|
62
|
+
created: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
/** Turns the zone's catch-all back off (drop), only when it points at this Worker. */
|
|
65
|
+
export declare function releaseCatchAllRule(c: CfCreds, zoneId: string, worker: string): Promise<boolean>;
|
|
57
66
|
export declare function deleteRule(c: CfCreds, zoneId: string, id: string): Promise<void>;
|
|
58
67
|
/** Queues event subscription: Email Service delivery events of `domain` → `queue` (by name). */
|
|
59
68
|
export declare function ensureEventSubscription(c: CfCreds, zoneId: string, domain: string, queue: string): Promise<{
|
package/dist/cloudflare/api.js
CHANGED
|
@@ -118,6 +118,28 @@ export async function ensureWorkerRule(c, zoneId, address, worker) {
|
|
|
118
118
|
});
|
|
119
119
|
return { id: r.id, created: true };
|
|
120
120
|
}
|
|
121
|
+
const pointsAt = (r, worker) => r.actions.some((x) => x.type === "worker" && x.value?.includes(worker));
|
|
122
|
+
/**
|
|
123
|
+
* The zone's catch-all rule → this Worker (every address no literal rule takes). Left alone, with an error, when it
|
|
124
|
+
* is on and sends mail somewhere else; a disabled or "drop" catch-all is taken over.
|
|
125
|
+
*/
|
|
126
|
+
export async function ensureCatchAllRule(c, zoneId, worker) {
|
|
127
|
+
const r = await call(c, "GET", `/zones/${zoneId}/email/routing/rules/catch_all`);
|
|
128
|
+
if (r.enabled && pointsAt(r, worker))
|
|
129
|
+
return { created: false };
|
|
130
|
+
if (r.enabled && !r.actions.every((x) => x.type === "drop"))
|
|
131
|
+
throw new CfError(`the zone's catch-all already routes mail elsewhere (${r.actions.map((x) => x.type).join(", ")}); not changing it`, 409, []);
|
|
132
|
+
await call(c, "PUT", `/zones/${zoneId}/email/routing/rules/catch_all`, { enabled: true, matchers: [{ type: "all" }], actions: [{ type: "worker", value: [worker] }] });
|
|
133
|
+
return { created: true };
|
|
134
|
+
}
|
|
135
|
+
/** Turns the zone's catch-all back off (drop), only when it points at this Worker. */
|
|
136
|
+
export async function releaseCatchAllRule(c, zoneId, worker) {
|
|
137
|
+
const r = await call(c, "GET", `/zones/${zoneId}/email/routing/rules/catch_all`);
|
|
138
|
+
if (!pointsAt(r, worker))
|
|
139
|
+
return false;
|
|
140
|
+
await call(c, "PUT", `/zones/${zoneId}/email/routing/rules/catch_all`, { enabled: false, matchers: [{ type: "all" }], actions: [{ type: "drop" }] });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
121
143
|
export async function deleteRule(c, zoneId, id) {
|
|
122
144
|
await call(c, "DELETE", `/zones/${zoneId}/email/routing/rules/${id}`);
|
|
123
145
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureEventSubscription, ensureSending, ensureWorkerRule, findZone, routingState, sendingDomain, type CfCreds, type DnsRecord, type RoutingState, } from "./api.ts";
|
|
1
|
+
export { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureEventSubscription, ensureSending, ensureWorkerRule, ensureCatchAllRule, releaseCatchAllRule, findZone, routingState, sendingDomain, type CfCreds, type DnsRecord, type RoutingState, } from "./api.ts";
|
|
2
2
|
export { destroyDomain, isDomainName, syncDomain, type DomainState, type SyncDomainDeps, type SyncDomainOptions } from "./onboard.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Names `ember deploy` gives an app's Worker and the email component's delivery-events queue (`install` is the name
|
package/dist/cloudflare/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// const { worker, eventsQueue } = emailResources("acme", "prod");
|
|
8
8
|
// let d = await syncDomain(c, { name: "mail.acme.com" }, { prev: null, mailboxes: ["support@mail.acme.com"], worker, queue: eventsQueue });
|
|
9
9
|
// // keep `d` (it holds the ids of what was created); later: await destroyDomain(c, d)
|
|
10
|
-
export { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureEventSubscription, ensureSending, ensureWorkerRule, findZone, routingState, sendingDomain, } from "./api.js";
|
|
10
|
+
export { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureEventSubscription, ensureSending, ensureWorkerRule, ensureCatchAllRule, releaseCatchAllRule, findZone, routingState, sendingDomain, } from "./api.js";
|
|
11
11
|
export { destroyDomain, isDomainName, syncDomain } from "./onboard.js";
|
|
12
12
|
/**
|
|
13
13
|
* Names `ember deploy` gives an app's Worker and the email component's delivery-events queue (`install` is the name
|
|
@@ -12,6 +12,8 @@ export type DomainState = {
|
|
|
12
12
|
ruleIds: string[];
|
|
13
13
|
subscriptionId?: string;
|
|
14
14
|
createdSending?: boolean;
|
|
15
|
+
/** The zone's catch-all rule points at this Worker (set by `catchAll`; `destroyDomain` turns it off). */
|
|
16
|
+
catchAll?: string;
|
|
15
17
|
notes: string[];
|
|
16
18
|
};
|
|
17
19
|
/** A syntactically valid (lowercase) domain name. */
|
|
@@ -35,6 +37,11 @@ export type SyncDomainDeps = {
|
|
|
35
37
|
worker: string;
|
|
36
38
|
/** The component's delivery-events queue (see `emailResources`). */
|
|
37
39
|
queue: string;
|
|
40
|
+
/**
|
|
41
|
+
* A mailbox of this domain is its catch-all: the zone's catch-all rule → `worker`. Only for a zone apex (the rule
|
|
42
|
+
* is the zone's); refused when the catch-all already routes mail elsewhere.
|
|
43
|
+
*/
|
|
44
|
+
catchAll?: boolean;
|
|
38
45
|
};
|
|
39
46
|
/**
|
|
40
47
|
* Onboards (or re-checks) a domain. Idempotent: run it again until `status` is `verified` (a new sending domain
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// subdomain only when asked), one Worker rule per mailbox address, and the delivery-event subscription to the
|
|
3
3
|
// component's queue. The caller keeps the returned `DomainState` (it holds the ids of what was created) and passes it
|
|
4
4
|
// back on the next sync and to `destroyDomain`. Product-agnostic: no storage, no HTTP API, no API keys.
|
|
5
|
-
import { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureEventSubscription, ensureSending, ensureWorkerRule, findZone, routingState } from "./api.js";
|
|
5
|
+
import { CfError, deleteEventSubscription, deleteRule, deleteSending, enableRoutingSubdomain, ensureCatchAllRule, ensureEventSubscription, ensureSending, ensureWorkerRule, findZone, releaseCatchAllRule, routingState } from "./api.js";
|
|
6
6
|
const NAME = /^(?=.{3,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
7
7
|
/** A syntactically valid (lowercase) domain name. */
|
|
8
8
|
export const isDomainName = (name) => NAME.test(name);
|
|
@@ -39,6 +39,7 @@ export async function syncDomain(c, a, deps) {
|
|
|
39
39
|
let routingStatus;
|
|
40
40
|
const ruleIds = [...(prev?.ruleIds ?? [])];
|
|
41
41
|
let subscriptionId = prev?.subscriptionId;
|
|
42
|
+
let catchAll = prev?.catchAll;
|
|
42
43
|
let recvOk = !wantRecv;
|
|
43
44
|
if (wantRecv) {
|
|
44
45
|
let r = await routingState(c, zone, name);
|
|
@@ -64,6 +65,27 @@ export async function syncDomain(c, a, deps) {
|
|
|
64
65
|
notes.push(e.message);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
if (deps.catchAll && name !== zone.name)
|
|
69
|
+
notes.push(`a catch-all is the zone's rule: only ${zone.name} can have one, not ${name}`);
|
|
70
|
+
else if (deps.catchAll) {
|
|
71
|
+
try {
|
|
72
|
+
await ensureCatchAllRule(c, zone.id, deps.worker);
|
|
73
|
+
catchAll = deps.worker;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
notes.push(`catch-all: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (catchAll && name === zone.name) {
|
|
80
|
+
// No catch-all mailbox any more: the rule this sync set goes back off.
|
|
81
|
+
try {
|
|
82
|
+
await releaseCatchAllRule(c, zone.id, catchAll);
|
|
83
|
+
catchAll = undefined;
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
notes.push(`catch-all: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
91
|
if (wantSend && sendingOk) {
|
|
@@ -87,6 +109,7 @@ export async function syncDomain(c, a, deps) {
|
|
|
87
109
|
ruleIds,
|
|
88
110
|
...(subscriptionId ? { subscriptionId } : {}),
|
|
89
111
|
createdSending,
|
|
112
|
+
...(catchAll ? { catchAll } : {}),
|
|
90
113
|
notes,
|
|
91
114
|
};
|
|
92
115
|
}
|
|
@@ -107,6 +130,8 @@ export async function destroyDomain(c, d) {
|
|
|
107
130
|
await attempt(`rule ${id}`, () => deleteRule(c, d.zoneId, id));
|
|
108
131
|
if (d.createdSending && d.sendingTag)
|
|
109
132
|
await attempt("sending domain", () => deleteSending(c, d.zoneId, d.sendingTag));
|
|
133
|
+
if (d.catchAll)
|
|
134
|
+
await attempt("catch-all", async () => void (await releaseCatchAllRule(c, d.zoneId, d.catchAll)));
|
|
110
135
|
}
|
|
111
136
|
if (d.subscriptionId)
|
|
112
137
|
await attempt("event subscription", () => deleteEventSubscription(c, d.subscriptionId));
|
package/dist/component.d.ts
CHANGED
|
@@ -596,6 +596,7 @@ export declare const emailFunctions: {
|
|
|
596
596
|
subject: string;
|
|
597
597
|
html?: string;
|
|
598
598
|
} | null;
|
|
599
|
+
catchAll?: boolean | null;
|
|
599
600
|
}, GenericId<"mailboxes">>;
|
|
600
601
|
removeMailbox: import("@freecorps/ember-server").RegisteredMutation<"public", {
|
|
601
602
|
name: string;
|
|
@@ -613,6 +614,7 @@ export declare const emailFunctions: {
|
|
|
613
614
|
subject: string;
|
|
614
615
|
html?: string;
|
|
615
616
|
};
|
|
617
|
+
catchAll?: boolean;
|
|
616
618
|
}[]>;
|
|
617
619
|
route: import("@freecorps/ember-server").RegisteredQuery<"public", {
|
|
618
620
|
to: string;
|
|
@@ -1403,6 +1405,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1403
1405
|
subject: string;
|
|
1404
1406
|
html?: string;
|
|
1405
1407
|
} | null;
|
|
1408
|
+
catchAll?: boolean | null;
|
|
1406
1409
|
}, GenericId<"mailboxes">>;
|
|
1407
1410
|
removeMailbox: import("@freecorps/ember-server").RegisteredMutation<"public", {
|
|
1408
1411
|
name: string;
|
|
@@ -1420,6 +1423,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1420
1423
|
subject: string;
|
|
1421
1424
|
html?: string;
|
|
1422
1425
|
};
|
|
1426
|
+
catchAll?: boolean;
|
|
1423
1427
|
}[]>;
|
|
1424
1428
|
route: import("@freecorps/ember-server").RegisteredQuery<"public", {
|
|
1425
1429
|
to: string;
|
package/dist/component.js
CHANGED
|
@@ -387,10 +387,18 @@ function applyToEmail(ctx, e, ev) {
|
|
|
387
387
|
}
|
|
388
388
|
// ------------------------------------------------------------------------------------------------ inbound
|
|
389
389
|
const THREAD_WINDOW = 30 * DAY;
|
|
390
|
+
/** The catch-all mailbox of `domain`, if one is set. */
|
|
391
|
+
function catchAllOf(ctx, domain) {
|
|
392
|
+
return ctx.db.query("mailboxes").collect().find((m) => m.catchAll && domainOf(m.address) === domain) ?? null;
|
|
393
|
+
}
|
|
390
394
|
function routeOf(ctx, to) {
|
|
391
395
|
const { base, tag } = splitPlus(to.toLowerCase());
|
|
392
396
|
const m = ctx.db.query("mailboxes").withIndex("by_address", (q) => q.eq("address", base)).first();
|
|
393
|
-
|
|
397
|
+
if (m)
|
|
398
|
+
return { mailbox: m.name, tag };
|
|
399
|
+
// No mailbox has that address: its domain's catch-all takes it (the address it was sent to stays in its To).
|
|
400
|
+
const all = catchAllOf(ctx, domainOf(base));
|
|
401
|
+
return all ? { mailbox: all.name, tag: null } : null;
|
|
394
402
|
}
|
|
395
403
|
function token(ctx) {
|
|
396
404
|
const abc = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -842,6 +850,8 @@ export const emailFunctions = {
|
|
|
842
850
|
displayName: v.optional(v.union(v.null(), v.string())),
|
|
843
851
|
/** Absent: keeps the stored one; null: clears it. */
|
|
844
852
|
autoReply: v.optional(v.union(v.null(), v.object({ subject: v.string(), text: v.string(), html: v.optional(v.string()) }))),
|
|
853
|
+
/** The domain's catch-all (one per domain). Absent: keeps the stored value; false or null: clears it. */
|
|
854
|
+
catchAll: v.optional(v.union(v.null(), v.boolean())),
|
|
845
855
|
},
|
|
846
856
|
handler: (ctx, a) => {
|
|
847
857
|
if (!/^[a-z][a-z0-9_-]{0,39}$/.test(a.name))
|
|
@@ -856,7 +866,21 @@ export const emailFunctions = {
|
|
|
856
866
|
// Patch semantics: an absent field keeps its stored value, null clears it.
|
|
857
867
|
const displayName = a.displayName === null ? undefined : (a.displayName ?? prev?.displayName);
|
|
858
868
|
const autoReply = a.autoReply === null ? undefined : (a.autoReply ?? prev?.autoReply);
|
|
859
|
-
const
|
|
869
|
+
const catchAll = a.catchAll === undefined ? !!prev?.catchAll : !!a.catchAll;
|
|
870
|
+
if (catchAll) {
|
|
871
|
+
const other = catchAllOf(ctx, domainOf(address));
|
|
872
|
+
if (other && other.name !== a.name)
|
|
873
|
+
fail("ValidationError", `${domainOf(address)} already has a catch-all: mailbox ${other.name}`);
|
|
874
|
+
}
|
|
875
|
+
const doc = {
|
|
876
|
+
name: a.name,
|
|
877
|
+
address,
|
|
878
|
+
...(displayName ? { displayName } : {}),
|
|
879
|
+
...(autoReply ? { autoReply } : {}),
|
|
880
|
+
...(catchAll ? { catchAll: true } : {}),
|
|
881
|
+
nextNumber: prev?.nextNumber ?? 1,
|
|
882
|
+
createdAt: prev?.createdAt ?? ctx.now,
|
|
883
|
+
};
|
|
860
884
|
if (prev) {
|
|
861
885
|
ctx.db.replace(prev._id, doc);
|
|
862
886
|
return prev._id;
|
package/dist/schema.d.ts
CHANGED
|
@@ -169,6 +169,8 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
|
|
|
169
169
|
text: import("@freecorps/ember-values").Validator<string, "required", never>;
|
|
170
170
|
html: import("@freecorps/ember-values").Validator<string, "optional", never>;
|
|
171
171
|
}> | undefined>>;
|
|
172
|
+
/** Receives mail for any address of its domain that no other mailbox has (one per domain). */
|
|
173
|
+
catchAll: import("@freecorps/ember-values").Validator<boolean, "optional", never>;
|
|
172
174
|
nextNumber: import("@freecorps/ember-values").Validator<number, "required", never>;
|
|
173
175
|
createdAt: import("@freecorps/ember-values").Validator<number, "required", never>;
|
|
174
176
|
}>, {
|
package/dist/schema.js
CHANGED
|
@@ -93,6 +93,8 @@ export const emailSchema = defineSchema({
|
|
|
93
93
|
address: v.string(),
|
|
94
94
|
displayName: v.optional(v.string()),
|
|
95
95
|
autoReply: v.optional(v.object({ subject: v.string(), text: v.string(), html: v.optional(v.string()) })),
|
|
96
|
+
/** Receives mail for any address of its domain that no other mailbox has (one per domain). */
|
|
97
|
+
catchAll: v.optional(v.boolean()),
|
|
96
98
|
nextNumber: v.number(),
|
|
97
99
|
createdAt: v.number(),
|
|
98
100
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freecorps/ember-email",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Email component for Ember on Cloudflare: send (atomic from mutations, scheduled, idempotent) through Email Service, templates, suppressions and per-project rate limits, delivery events from the Queue, inbound mail (Email Routing) parsed into mailboxes and threads. `@freecorps/ember-email/cloudflare` onboards a domain (sending, routing rules, event subscription) and removes it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ember",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@freecorps/ember-rate-limit": "0.5.
|
|
56
|
-
"@freecorps/ember-server": "0.5.
|
|
57
|
-
"@freecorps/ember-values": "0.5.
|
|
55
|
+
"@freecorps/ember-rate-limit": "0.5.7",
|
|
56
|
+
"@freecorps/ember-server": "0.5.7",
|
|
57
|
+
"@freecorps/ember-values": "0.5.7",
|
|
58
58
|
"postal-mime": "^4.1.4"
|
|
59
59
|
}
|
|
60
60
|
}
|