@freecorps/ember-email 0.5.7 → 0.5.8
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 +7 -0
- package/dist/component.d.ts +40 -0
- package/dist/component.js +94 -2
- package/dist/schema.d.ts +17 -0
- package/dist/schema.js +11 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -71,6 +71,10 @@ export const signup = mutation({
|
|
|
71
71
|
(with their size). Mind the provider's message size: for a large file, put a download link in the text
|
|
72
72
|
(`ctx.storage.getUrl(file, { download, expiresIn })`) and pass it with `link: true`. The email then has no MIME part
|
|
73
73
|
for it, and the message still holds the file (the thread lists it, and it is not collected).
|
|
74
|
+
- **Retention:** `upsertMailbox({ retention: { closedDays: 730, spamDays: 30 } })` keeps closed and spam conversations
|
|
75
|
+
that many days, counted from the latest status change or message; a daily job (`lib:purge`, 04:00
|
|
76
|
+
UTC) then deletes the conversation and its messages, and file GC deletes their `.eml` and attachments. Without
|
|
77
|
+
`retention` (or with `retention: null`) nothing is deleted.
|
|
74
78
|
|
|
75
79
|
## Domains: `@freecorps/ember-email/cloudflare`
|
|
76
80
|
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { FunctionReference } from "@freecorps/ember-server";
|
|
|
2
2
|
import type { SendInput, SendResult } from "./component.ts";
|
|
3
3
|
import type { EmailStatus } from "./events.ts";
|
|
4
4
|
type Ref<T extends "query" | "mutation" | "action", A, R> = FunctionReference<T, "public", A, R>;
|
|
5
|
+
/** Days closed and spam conversations are kept (`upsertMailbox({ retention })`); a missing kind is kept forever. */
|
|
6
|
+
export type MailboxRetention = {
|
|
7
|
+
closedDays?: number;
|
|
8
|
+
spamDays?: number;
|
|
9
|
+
};
|
|
5
10
|
type Page<T> = {
|
|
6
11
|
page: T[];
|
|
7
12
|
isDone: boolean;
|
|
@@ -134,6 +139,7 @@ export interface EmailComponentApi {
|
|
|
134
139
|
html?: string;
|
|
135
140
|
} | null;
|
|
136
141
|
catchAll?: boolean | null;
|
|
142
|
+
retention?: MailboxRetention | null;
|
|
137
143
|
}, string>;
|
|
138
144
|
removeMailbox: Ref<"mutation", {
|
|
139
145
|
name: string;
|
|
@@ -145,6 +151,7 @@ export interface EmailComponentApi {
|
|
|
145
151
|
displayName?: string;
|
|
146
152
|
autoReply?: unknown;
|
|
147
153
|
catchAll?: boolean;
|
|
154
|
+
retention?: MailboxRetention;
|
|
148
155
|
}>>;
|
|
149
156
|
route: Ref<"query", {
|
|
150
157
|
to: string;
|
package/dist/component.d.ts
CHANGED
|
@@ -513,6 +513,15 @@ export declare const emailFunctions: {
|
|
|
513
513
|
} | null>;
|
|
514
514
|
/** Cron: a send claimed more than LEASE_MS ago never finished (Worker died mid-send): fail it, never resend blind. */
|
|
515
515
|
sweep: import("@freecorps/ember-server").RegisteredMutation<"internal", {}, number>;
|
|
516
|
+
/**
|
|
517
|
+
* Daily: deletes the closed and spam conversations past their mailbox's retention (with their messages; the
|
|
518
|
+
* messages' files are then collected by file GC). Bounded per run; schedules itself again while there is more.
|
|
519
|
+
*/
|
|
520
|
+
purge: import("@freecorps/ember-server").RegisteredMutation<"internal", {}, {
|
|
521
|
+
threads: number;
|
|
522
|
+
messages: number;
|
|
523
|
+
more: boolean;
|
|
524
|
+
}>;
|
|
516
525
|
applyEvents: import("@freecorps/ember-server").RegisteredMutation<"internal", {
|
|
517
526
|
events: any[];
|
|
518
527
|
}, ({
|
|
@@ -597,6 +606,10 @@ export declare const emailFunctions: {
|
|
|
597
606
|
html?: string;
|
|
598
607
|
} | null;
|
|
599
608
|
catchAll?: boolean | null;
|
|
609
|
+
retention?: {
|
|
610
|
+
closedDays?: number;
|
|
611
|
+
spamDays?: number;
|
|
612
|
+
} | null;
|
|
600
613
|
}, GenericId<"mailboxes">>;
|
|
601
614
|
removeMailbox: import("@freecorps/ember-server").RegisteredMutation<"public", {
|
|
602
615
|
name: string;
|
|
@@ -615,6 +628,10 @@ export declare const emailFunctions: {
|
|
|
615
628
|
html?: string;
|
|
616
629
|
};
|
|
617
630
|
catchAll?: boolean;
|
|
631
|
+
retention?: {
|
|
632
|
+
closedDays?: number;
|
|
633
|
+
spamDays?: number;
|
|
634
|
+
};
|
|
618
635
|
}[]>;
|
|
619
636
|
route: import("@freecorps/ember-server").RegisteredQuery<"public", {
|
|
620
637
|
to: string;
|
|
@@ -644,6 +661,7 @@ export declare const emailFunctions: {
|
|
|
644
661
|
lastMessageAt: number;
|
|
645
662
|
snippet: string;
|
|
646
663
|
contactName?: string;
|
|
664
|
+
activityAt?: number;
|
|
647
665
|
lastInboundAt?: number;
|
|
648
666
|
assignee?: string;
|
|
649
667
|
assigneeName?: string;
|
|
@@ -666,6 +684,7 @@ export declare const emailFunctions: {
|
|
|
666
684
|
lastMessageAt: number;
|
|
667
685
|
snippet: string;
|
|
668
686
|
contactName?: string;
|
|
687
|
+
activityAt?: number;
|
|
669
688
|
lastInboundAt?: number;
|
|
670
689
|
assignee?: string;
|
|
671
690
|
assigneeName?: string;
|
|
@@ -830,6 +849,7 @@ export declare const emailFunctions: {
|
|
|
830
849
|
lastMessageAt: number;
|
|
831
850
|
snippet: string;
|
|
832
851
|
contactName?: string;
|
|
852
|
+
activityAt?: number;
|
|
833
853
|
lastInboundAt?: number;
|
|
834
854
|
assignee?: string;
|
|
835
855
|
assigneeName?: string;
|
|
@@ -1322,6 +1342,15 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1322
1342
|
} | null>;
|
|
1323
1343
|
/** Cron: a send claimed more than LEASE_MS ago never finished (Worker died mid-send): fail it, never resend blind. */
|
|
1324
1344
|
sweep: import("@freecorps/ember-server").RegisteredMutation<"internal", {}, number>;
|
|
1345
|
+
/**
|
|
1346
|
+
* Daily: deletes the closed and spam conversations past their mailbox's retention (with their messages; the
|
|
1347
|
+
* messages' files are then collected by file GC). Bounded per run; schedules itself again while there is more.
|
|
1348
|
+
*/
|
|
1349
|
+
purge: import("@freecorps/ember-server").RegisteredMutation<"internal", {}, {
|
|
1350
|
+
threads: number;
|
|
1351
|
+
messages: number;
|
|
1352
|
+
more: boolean;
|
|
1353
|
+
}>;
|
|
1325
1354
|
applyEvents: import("@freecorps/ember-server").RegisteredMutation<"internal", {
|
|
1326
1355
|
events: any[];
|
|
1327
1356
|
}, ({
|
|
@@ -1406,6 +1435,10 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1406
1435
|
html?: string;
|
|
1407
1436
|
} | null;
|
|
1408
1437
|
catchAll?: boolean | null;
|
|
1438
|
+
retention?: {
|
|
1439
|
+
closedDays?: number;
|
|
1440
|
+
spamDays?: number;
|
|
1441
|
+
} | null;
|
|
1409
1442
|
}, GenericId<"mailboxes">>;
|
|
1410
1443
|
removeMailbox: import("@freecorps/ember-server").RegisteredMutation<"public", {
|
|
1411
1444
|
name: string;
|
|
@@ -1424,6 +1457,10 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1424
1457
|
html?: string;
|
|
1425
1458
|
};
|
|
1426
1459
|
catchAll?: boolean;
|
|
1460
|
+
retention?: {
|
|
1461
|
+
closedDays?: number;
|
|
1462
|
+
spamDays?: number;
|
|
1463
|
+
};
|
|
1427
1464
|
}[]>;
|
|
1428
1465
|
route: import("@freecorps/ember-server").RegisteredQuery<"public", {
|
|
1429
1466
|
to: string;
|
|
@@ -1453,6 +1490,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1453
1490
|
lastMessageAt: number;
|
|
1454
1491
|
snippet: string;
|
|
1455
1492
|
contactName?: string;
|
|
1493
|
+
activityAt?: number;
|
|
1456
1494
|
lastInboundAt?: number;
|
|
1457
1495
|
assignee?: string;
|
|
1458
1496
|
assigneeName?: string;
|
|
@@ -1475,6 +1513,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1475
1513
|
lastMessageAt: number;
|
|
1476
1514
|
snippet: string;
|
|
1477
1515
|
contactName?: string;
|
|
1516
|
+
activityAt?: number;
|
|
1478
1517
|
lastInboundAt?: number;
|
|
1479
1518
|
assignee?: string;
|
|
1480
1519
|
assigneeName?: string;
|
|
@@ -1639,6 +1678,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1639
1678
|
lastMessageAt: number;
|
|
1640
1679
|
snippet: string;
|
|
1641
1680
|
contactName?: string;
|
|
1681
|
+
activityAt?: number;
|
|
1642
1682
|
lastInboundAt?: number;
|
|
1643
1683
|
assignee?: string;
|
|
1644
1684
|
assigneeName?: string;
|
package/dist/component.js
CHANGED
|
@@ -477,6 +477,7 @@ function ingest(ctx, a) {
|
|
|
477
477
|
contact,
|
|
478
478
|
...((a.replyTo ?? a.from).name ? { contactName: (a.replyTo ?? a.from).name } : {}),
|
|
479
479
|
status: spam ? "spam" : "open",
|
|
480
|
+
activityAt: ctx.now,
|
|
480
481
|
token: token(ctx),
|
|
481
482
|
messageCount: 0,
|
|
482
483
|
unreadCount: 0,
|
|
@@ -513,6 +514,7 @@ function ingest(ctx, a) {
|
|
|
513
514
|
const status = thread.status === "spam" ? "spam" : "open";
|
|
514
515
|
ctx.db.patch(thread._id, {
|
|
515
516
|
status,
|
|
517
|
+
activityAt: ctx.now,
|
|
516
518
|
messageCount: thread.messageCount + 1,
|
|
517
519
|
unreadCount: thread.unreadCount + 1,
|
|
518
520
|
lastMessageAt: ctx.now,
|
|
@@ -601,10 +603,83 @@ function appendOutbound(ctx, thread, mb, a) {
|
|
|
601
603
|
ctx.db.patch(thread._id, {
|
|
602
604
|
messageCount: thread.messageCount + 1,
|
|
603
605
|
lastMessageAt: ctx.now,
|
|
606
|
+
activityAt: ctx.now,
|
|
604
607
|
...(a.note ? {} : { status: thread.status === "spam" ? "spam" : "waiting", snippet }),
|
|
605
608
|
});
|
|
606
609
|
return id;
|
|
607
610
|
}
|
|
611
|
+
const DAY_MS = 86_400_000;
|
|
612
|
+
const MAX_RETENTION_DAYS = 36_500;
|
|
613
|
+
/** Rows (messages and threads) one purge transaction deletes before it schedules the rest. */
|
|
614
|
+
const PURGE_BUDGET = 500;
|
|
615
|
+
/** Candidates one purge run looks at per mailbox and status (some may not be due yet: see `purgeExpired`). */
|
|
616
|
+
const PURGE_SCAN = 200;
|
|
617
|
+
function retentionOf(r) {
|
|
618
|
+
const out = {};
|
|
619
|
+
for (const k of ["closedDays", "spamDays"]) {
|
|
620
|
+
const d = r[k];
|
|
621
|
+
if (d === undefined)
|
|
622
|
+
continue;
|
|
623
|
+
if (!Number.isInteger(d) || d < 1 || d > MAX_RETENTION_DAYS)
|
|
624
|
+
fail("ValidationError", `retention.${k}: a whole number of days in 1..${MAX_RETENTION_DAYS}`);
|
|
625
|
+
out[k] = d;
|
|
626
|
+
}
|
|
627
|
+
return out.closedDays === undefined && out.spamDays === undefined ? undefined : out;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* One purge pass: the closed and spam threads whose latest status change or message (`activityAt`) is older than
|
|
631
|
+
* their mailbox's retention, straight from the `by_mailbox_status_activity` index (every candidate is due, so a
|
|
632
|
+
* thread kept longer never blocks the ones behind it). Threads from before `activityAt` (absent, first in the index)
|
|
633
|
+
* get it from `lastMessageAt` when they are not due yet, so they leave the head of the index.
|
|
634
|
+
*/
|
|
635
|
+
function purgeExpired(ctx) {
|
|
636
|
+
let budget = PURGE_BUDGET;
|
|
637
|
+
let threads = 0;
|
|
638
|
+
let messages = 0;
|
|
639
|
+
let more = false;
|
|
640
|
+
for (const mb of ctx.db.query("mailboxes").collect()) {
|
|
641
|
+
for (const [status, days] of [
|
|
642
|
+
["closed", mb.retention?.closedDays],
|
|
643
|
+
["spam", mb.retention?.spamDays],
|
|
644
|
+
]) {
|
|
645
|
+
if (!days || more)
|
|
646
|
+
continue;
|
|
647
|
+
const cutoff = ctx.now - days * DAY_MS;
|
|
648
|
+
const due = ctx.db
|
|
649
|
+
.query("threads")
|
|
650
|
+
.withIndex("by_mailbox_status_activity", (q) => q.eq("mailbox", mb.name).eq("status", status).lte("activityAt", cutoff))
|
|
651
|
+
.take(PURGE_SCAN);
|
|
652
|
+
for (const t of due) {
|
|
653
|
+
if (budget <= 0) {
|
|
654
|
+
more = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
if (t.activityAt === undefined && t.lastMessageAt > cutoff) {
|
|
658
|
+
ctx.db.patch(t._id, { activityAt: t.lastMessageAt });
|
|
659
|
+
budget--;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const batch = ctx.db.query("messages").withIndex("by_thread", (q) => q.eq("threadId", t._id)).take(budget);
|
|
663
|
+
for (const m of batch)
|
|
664
|
+
ctx.db.delete(m._id);
|
|
665
|
+
messages += batch.length;
|
|
666
|
+
budget -= batch.length;
|
|
667
|
+
if (budget <= 0) {
|
|
668
|
+
more = true; // the thread goes on the next run, once its last messages are gone
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
ctx.db.delete(t._id);
|
|
672
|
+
threads++;
|
|
673
|
+
budget--;
|
|
674
|
+
}
|
|
675
|
+
if (due.length === PURGE_SCAN)
|
|
676
|
+
more = true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (more)
|
|
680
|
+
ctx.scheduler.runAfter(0, "lib:purge", {});
|
|
681
|
+
return { threads, messages, more };
|
|
682
|
+
}
|
|
608
683
|
// ------------------------------------------------------------------------------------------------ functions
|
|
609
684
|
const pageV = v.optional(v.object({ numItems: v.number(), cursor: v.union(v.string(), v.null()) }));
|
|
610
685
|
export const emailFunctions = {
|
|
@@ -765,6 +840,14 @@ export const emailFunctions = {
|
|
|
765
840
|
return n;
|
|
766
841
|
},
|
|
767
842
|
}),
|
|
843
|
+
/**
|
|
844
|
+
* Daily: deletes the closed and spam conversations past their mailbox's retention (with their messages; the
|
|
845
|
+
* messages' files are then collected by file GC). Bounded per run; schedules itself again while there is more.
|
|
846
|
+
*/
|
|
847
|
+
purge: internalMutation({
|
|
848
|
+
args: {},
|
|
849
|
+
handler: (ctx) => purgeExpired(ctx),
|
|
850
|
+
}),
|
|
768
851
|
// ---------------------------------------------------------------- events
|
|
769
852
|
applyEvents: internalMutation({
|
|
770
853
|
args: { events: v.array(v.any()) },
|
|
@@ -852,6 +935,11 @@ export const emailFunctions = {
|
|
|
852
935
|
autoReply: v.optional(v.union(v.null(), v.object({ subject: v.string(), text: v.string(), html: v.optional(v.string()) }))),
|
|
853
936
|
/** The domain's catch-all (one per domain). Absent: keeps the stored value; false or null: clears it. */
|
|
854
937
|
catchAll: v.optional(v.union(v.null(), v.boolean())),
|
|
938
|
+
/**
|
|
939
|
+
* Days closed and spam conversations are kept (whole days, 1..36500; a missing kind is kept forever).
|
|
940
|
+
* Absent: keeps the stored policy; null: clears it (kept forever).
|
|
941
|
+
*/
|
|
942
|
+
retention: v.optional(v.union(v.null(), v.object({ closedDays: v.optional(v.number()), spamDays: v.optional(v.number()) }))),
|
|
855
943
|
},
|
|
856
944
|
handler: (ctx, a) => {
|
|
857
945
|
if (!/^[a-z][a-z0-9_-]{0,39}$/.test(a.name))
|
|
@@ -867,6 +955,7 @@ export const emailFunctions = {
|
|
|
867
955
|
const displayName = a.displayName === null ? undefined : (a.displayName ?? prev?.displayName);
|
|
868
956
|
const autoReply = a.autoReply === null ? undefined : (a.autoReply ?? prev?.autoReply);
|
|
869
957
|
const catchAll = a.catchAll === undefined ? !!prev?.catchAll : !!a.catchAll;
|
|
958
|
+
const retention = a.retention === null ? undefined : a.retention === undefined ? prev?.retention : retentionOf(a.retention);
|
|
870
959
|
if (catchAll) {
|
|
871
960
|
const other = catchAllOf(ctx, domainOf(address));
|
|
872
961
|
if (other && other.name !== a.name)
|
|
@@ -878,6 +967,7 @@ export const emailFunctions = {
|
|
|
878
967
|
...(displayName ? { displayName } : {}),
|
|
879
968
|
...(autoReply ? { autoReply } : {}),
|
|
880
969
|
...(catchAll ? { catchAll: true } : {}),
|
|
970
|
+
...(retention ? { retention } : {}),
|
|
881
971
|
nextNumber: prev?.nextNumber ?? 1,
|
|
882
972
|
createdAt: prev?.createdAt ?? ctx.now,
|
|
883
973
|
};
|
|
@@ -1037,7 +1127,8 @@ export const emailFunctions = {
|
|
|
1037
1127
|
const t = ctx.db.get(a.threadId);
|
|
1038
1128
|
if (!t)
|
|
1039
1129
|
return fail("NotFound", "thread not found");
|
|
1040
|
-
|
|
1130
|
+
if (t.status !== a.status)
|
|
1131
|
+
ctx.db.patch(a.threadId, { status: a.status, activityAt: ctx.now });
|
|
1041
1132
|
return a.status;
|
|
1042
1133
|
},
|
|
1043
1134
|
}),
|
|
@@ -1163,6 +1254,7 @@ export const emailFunctions = {
|
|
|
1163
1254
|
messageCount: 0,
|
|
1164
1255
|
unreadCount: 0,
|
|
1165
1256
|
lastMessageAt: ctx.now,
|
|
1257
|
+
activityAt: ctx.now,
|
|
1166
1258
|
snippet: snippetOf(a.text),
|
|
1167
1259
|
});
|
|
1168
1260
|
const thread = ctx.db.get(threadId);
|
|
@@ -1289,7 +1381,7 @@ export const email = defineComponent({
|
|
|
1289
1381
|
bindings: (config) => (config?.transport ?? "binding") === "binding"
|
|
1290
1382
|
? [{ type: "send_email", name: config?.binding ?? "EMAIL", ...(config?.allowedSenders?.length ? { allowed_sender_addresses: config.allowedSenders } : {}) }]
|
|
1291
1383
|
: [],
|
|
1292
|
-
crons: (config) => cronJobs({ shard: config?.shard ?? "global:main" }).interval("sweep", { minutes: 10 }, "lib:sweep"),
|
|
1384
|
+
crons: (config) => cronJobs({ shard: config?.shard ?? "global:main" }).interval("sweep", { minutes: 10 }, "lib:sweep").daily("purge", { hour: 4 }, "lib:purge"),
|
|
1293
1385
|
queues: {
|
|
1294
1386
|
events: { shard: (config) => config?.shard ?? "global:main", consumer: { maxBatchSize: 100, maxBatchTimeoutSeconds: 2, maxRetries: 10 }, handler: onEvents },
|
|
1295
1387
|
},
|
package/dist/schema.d.ts
CHANGED
|
@@ -171,6 +171,17 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
|
|
|
171
171
|
}> | undefined>>;
|
|
172
172
|
/** Receives mail for any address of its domain that no other mailbox has (one per domain). */
|
|
173
173
|
catchAll: import("@freecorps/ember-values").Validator<boolean, "optional", never>;
|
|
174
|
+
/**
|
|
175
|
+
* How long closed and spam conversations are kept, in days, counted from the latest status change or message
|
|
176
|
+
* (`threads.activityAt`). Absent: kept forever. A daily job deletes the conversation, its messages and their files.
|
|
177
|
+
*/
|
|
178
|
+
retention: import("@freecorps/ember-values").Validator<{
|
|
179
|
+
closedDays?: number;
|
|
180
|
+
spamDays?: number;
|
|
181
|
+
}, "optional", NonNullable<import("@freecorps/ember-values").ObjectFieldPaths<{
|
|
182
|
+
closedDays: import("@freecorps/ember-values").Validator<number, "optional", never>;
|
|
183
|
+
spamDays: import("@freecorps/ember-values").Validator<number, "optional", never>;
|
|
184
|
+
}> | undefined>>;
|
|
174
185
|
nextNumber: import("@freecorps/ember-values").Validator<number, "required", never>;
|
|
175
186
|
createdAt: import("@freecorps/ember-values").Validator<number, "required", never>;
|
|
176
187
|
}>, {
|
|
@@ -187,6 +198,11 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
|
|
|
187
198
|
contactName: import("@freecorps/ember-values").Validator<string, "optional", never>;
|
|
188
199
|
/** open | waiting | closed | spam */
|
|
189
200
|
status: import("@freecorps/ember-values").Validator<string, "required", never>;
|
|
201
|
+
/**
|
|
202
|
+
* The latest status change or message: retention counts from it. Absent on threads from before 0.5.8 (the first
|
|
203
|
+
* purge fills it in from `lastMessageAt`).
|
|
204
|
+
*/
|
|
205
|
+
activityAt: import("@freecorps/ember-values").Validator<number, "optional", never>;
|
|
190
206
|
/** Plus-address token: replies to `support+t_<token>@` join this thread from any sender. */
|
|
191
207
|
token: import("@freecorps/ember-values").Validator<string, "required", never>;
|
|
192
208
|
messageCount: import("@freecorps/ember-values").Validator<number, "required", never>;
|
|
@@ -200,6 +216,7 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
|
|
|
200
216
|
}>, {
|
|
201
217
|
by_mailbox_last: ["mailbox", "lastMessageAt", "_creationTime"];
|
|
202
218
|
by_mailbox_status: ["mailbox", "status", "lastMessageAt", "_creationTime"];
|
|
219
|
+
by_mailbox_status_activity: ["mailbox", "status", "activityAt", "_creationTime"];
|
|
203
220
|
by_token: ["token", "_creationTime"];
|
|
204
221
|
by_contact_subject: ["mailbox", "contact", "subjectKey", "_creationTime"];
|
|
205
222
|
}, {
|
package/dist/schema.js
CHANGED
|
@@ -95,6 +95,11 @@ export const emailSchema = defineSchema({
|
|
|
95
95
|
autoReply: v.optional(v.object({ subject: v.string(), text: v.string(), html: v.optional(v.string()) })),
|
|
96
96
|
/** Receives mail for any address of its domain that no other mailbox has (one per domain). */
|
|
97
97
|
catchAll: v.optional(v.boolean()),
|
|
98
|
+
/**
|
|
99
|
+
* How long closed and spam conversations are kept, in days, counted from the latest status change or message
|
|
100
|
+
* (`threads.activityAt`). Absent: kept forever. A daily job deletes the conversation, its messages and their files.
|
|
101
|
+
*/
|
|
102
|
+
retention: v.optional(v.object({ closedDays: v.optional(v.number()), spamDays: v.optional(v.number()) })),
|
|
98
103
|
nextNumber: v.number(),
|
|
99
104
|
createdAt: v.number(),
|
|
100
105
|
})
|
|
@@ -110,6 +115,11 @@ export const emailSchema = defineSchema({
|
|
|
110
115
|
contactName: v.optional(v.string()),
|
|
111
116
|
/** open | waiting | closed | spam */
|
|
112
117
|
status: v.string(),
|
|
118
|
+
/**
|
|
119
|
+
* The latest status change or message: retention counts from it. Absent on threads from before 0.5.8 (the first
|
|
120
|
+
* purge fills it in from `lastMessageAt`).
|
|
121
|
+
*/
|
|
122
|
+
activityAt: v.optional(v.number()),
|
|
113
123
|
/** Plus-address token: replies to `support+t_<token>@` join this thread from any sender. */
|
|
114
124
|
token: v.string(),
|
|
115
125
|
messageCount: v.number(),
|
|
@@ -123,6 +133,7 @@ export const emailSchema = defineSchema({
|
|
|
123
133
|
})
|
|
124
134
|
.index("by_mailbox_last", ["mailbox", "lastMessageAt"])
|
|
125
135
|
.index("by_mailbox_status", ["mailbox", "status", "lastMessageAt"])
|
|
136
|
+
.index("by_mailbox_status_activity", ["mailbox", "status", "activityAt"])
|
|
126
137
|
.index("by_token", ["token"])
|
|
127
138
|
.index("by_contact_subject", ["mailbox", "contact", "subjectKey"])
|
|
128
139
|
.aggregate("unread", { by: ["mailbox"], count: true, sum: ["unreadCount"] }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freecorps/ember-email",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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.8",
|
|
56
|
+
"@freecorps/ember-server": "0.5.8",
|
|
57
|
+
"@freecorps/ember-values": "0.5.8",
|
|
58
58
|
"postal-mime": "^4.1.4"
|
|
59
59
|
}
|
|
60
60
|
}
|