@cosmicdrift/kumiko-samples 0.392.2 → 0.394.0
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/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +12 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Reloads the content of mails that were imported as snippet only (event version 1, before full text
|
|
2
|
+
// storage existed). Runs after a successful poll with a small per-account budget, so a large backlog
|
|
3
|
+
// is worked off over several ticks without slowing the regular sync. "unavailable" is terminal:
|
|
4
|
+
// mails that are gone from the server stop being candidates.
|
|
5
|
+
|
|
6
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
8
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
9
|
+
import { inboundMessageAggregateId } from "./aggregate-id.ts";
|
|
10
|
+
import { InboundMailFoundationHandlers } from "./constants.ts";
|
|
11
|
+
import { seenMessageTable } from "./entities.ts";
|
|
12
|
+
import { normalizeRawInboundMessage } from "./normalize-inbound-message.ts";
|
|
13
|
+
import { inboundMessagesProjectionTable } from "./projection.ts";
|
|
14
|
+
import type { DispatchWrite } from "./supervisor-ingest.ts";
|
|
15
|
+
import type { InboundMailContext, InboundMailProviderPlugin, MailAccountRecord } from "./types.ts";
|
|
16
|
+
|
|
17
|
+
export const CONTENT_REFETCH_BUDGET_PER_TICK = 50;
|
|
18
|
+
const SEEN_PAGE_SIZE = 250;
|
|
19
|
+
const SEEN_MAX_PAGES = 4;
|
|
20
|
+
|
|
21
|
+
type SeenRow = { readonly providerMessageId: string; readonly seenAt: Temporal.Instant };
|
|
22
|
+
type MessageRow = {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly bodyRef?: string | null;
|
|
25
|
+
readonly contentState?: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function needsContent(row: MessageRow): boolean {
|
|
29
|
+
const bodyMissing = row.bodyRef === undefined || row.bodyRef === null || row.bodyRef === "";
|
|
30
|
+
return bodyMissing && (row.contentState == null || row.contentState === "snippet");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function findSnippetOnlyMessageIds(
|
|
34
|
+
db: DbConnection,
|
|
35
|
+
account: MailAccountRecord,
|
|
36
|
+
windowDays: number,
|
|
37
|
+
): Promise<string[]> {
|
|
38
|
+
const cutoff = Temporal.Now.instant().subtract({ hours: windowDays * 24 });
|
|
39
|
+
const found: string[] = [];
|
|
40
|
+
let before: Temporal.Instant | undefined;
|
|
41
|
+
for (
|
|
42
|
+
let page = 0;
|
|
43
|
+
page < SEEN_MAX_PAGES && found.length < CONTENT_REFETCH_BUDGET_PER_TICK;
|
|
44
|
+
page++
|
|
45
|
+
) {
|
|
46
|
+
const seen = await selectMany<SeenRow>(
|
|
47
|
+
db,
|
|
48
|
+
seenMessageTable,
|
|
49
|
+
{
|
|
50
|
+
tenantId: account.tenantId,
|
|
51
|
+
accountId: account.id,
|
|
52
|
+
seenAt: before ? { gte: cutoff, lt: before } : { gte: cutoff },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
fields: ["providerMessageId", "seenAt"],
|
|
56
|
+
orderBy: { col: "seenAt", direction: "desc" },
|
|
57
|
+
limit: SEEN_PAGE_SIZE,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
if (seen.length === 0) break;
|
|
61
|
+
const aggregateToProvider = new Map(
|
|
62
|
+
seen.map((row) => [
|
|
63
|
+
inboundMessageAggregateId(account.id, row.providerMessageId),
|
|
64
|
+
row.providerMessageId,
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
const rows = await selectMany<MessageRow>(
|
|
68
|
+
db,
|
|
69
|
+
inboundMessagesProjectionTable,
|
|
70
|
+
{ tenantId: account.tenantId, id: { in: [...aggregateToProvider.keys()] } },
|
|
71
|
+
{ fields: ["id", "bodyRef", "contentState"] },
|
|
72
|
+
);
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const providerMessageId = aggregateToProvider.get(row.id);
|
|
75
|
+
if (providerMessageId !== undefined && needsContent(row)) found.push(providerMessageId);
|
|
76
|
+
}
|
|
77
|
+
const oldest = seen[seen.length - 1];
|
|
78
|
+
if (seen.length < SEEN_PAGE_SIZE || !oldest) break;
|
|
79
|
+
before = oldest.seenAt;
|
|
80
|
+
}
|
|
81
|
+
return found.slice(0, CONTENT_REFETCH_BUDGET_PER_TICK);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function refetchSnippetOnlyMessages(args: {
|
|
85
|
+
readonly db: DbConnection;
|
|
86
|
+
readonly dispatchWrite: DispatchWrite;
|
|
87
|
+
readonly providerCtx: InboundMailContext;
|
|
88
|
+
readonly account: MailAccountRecord;
|
|
89
|
+
readonly plugin: InboundMailProviderPlugin;
|
|
90
|
+
readonly windowDays: number;
|
|
91
|
+
readonly accessToken?: string | undefined;
|
|
92
|
+
readonly signal?: AbortSignal | undefined;
|
|
93
|
+
}): Promise<void> {
|
|
94
|
+
const { account, plugin, dispatchWrite } = args;
|
|
95
|
+
// skip: the provider cannot reload content.
|
|
96
|
+
if (!plugin.fetchContent) return;
|
|
97
|
+
const ids = await findSnippetOnlyMessageIds(args.db, account, args.windowDays);
|
|
98
|
+
// skip: no snippet-only mail to reload.
|
|
99
|
+
if (ids.length === 0) return;
|
|
100
|
+
const result = await plugin.fetchContent(
|
|
101
|
+
args.providerCtx,
|
|
102
|
+
account,
|
|
103
|
+
ids,
|
|
104
|
+
args.accessToken !== undefined || args.signal !== undefined
|
|
105
|
+
? {
|
|
106
|
+
...(args.accessToken !== undefined && { accessToken: args.accessToken }),
|
|
107
|
+
...(args.signal !== undefined && { signal: args.signal }),
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
);
|
|
111
|
+
const attach = (providerMessageId: string, content: Record<string, unknown>) =>
|
|
112
|
+
dispatchWrite({
|
|
113
|
+
handlerQn: InboundMailFoundationHandlers.attachMessageContent,
|
|
114
|
+
tenantId: account.tenantId,
|
|
115
|
+
payload: { accountId: account.id, providerMessageId, ...content },
|
|
116
|
+
});
|
|
117
|
+
for (const raw of result.messages) {
|
|
118
|
+
const msg = normalizeRawInboundMessage(raw);
|
|
119
|
+
if (msg.bodyText === "" && msg.rawMime == null && msg.bodyHtml === null) {
|
|
120
|
+
// Nothing usable came back: attach would leave the mail at "snippet", i.e. a candidate on
|
|
121
|
+
// every tick. "unavailable" is terminal.
|
|
122
|
+
await attach(msg.providerMessageId, { outcome: "unavailable" });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
await attach(msg.providerMessageId, {
|
|
126
|
+
outcome: "fetched",
|
|
127
|
+
bodyText: msg.bodyText,
|
|
128
|
+
bodyTextTruncated: msg.bodyTextTruncated,
|
|
129
|
+
...(msg.rawMime != null && { rawMime: msg.rawMime }),
|
|
130
|
+
...(msg.bodyHtml !== null && { bodyHtml: msg.bodyHtml }),
|
|
131
|
+
attachments: msg.attachments,
|
|
132
|
+
skippedAttachments: msg.skippedAttachments,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const gone = [...result.missing, ...result.rejected.map((r) => r.providerMessageId)];
|
|
136
|
+
for (const providerMessageId of gone) await attach(providerMessageId, { outcome: "unavailable" });
|
|
137
|
+
}
|
|
@@ -2,7 +2,10 @@ import { collectPiiSubjectFields } from "@cosmicdrift/kumiko-framework/crypto";
|
|
|
2
2
|
import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import {
|
|
4
4
|
access,
|
|
5
|
+
createBooleanField,
|
|
5
6
|
createEntity,
|
|
7
|
+
createFilesField,
|
|
8
|
+
createLongTextField,
|
|
6
9
|
createNumberField,
|
|
7
10
|
createTextField,
|
|
8
11
|
createTimestampField,
|
|
@@ -64,7 +67,15 @@ export const mailAccountEntity = createEntity({
|
|
|
64
67
|
personal: false,
|
|
65
68
|
reason: "technical_reference",
|
|
66
69
|
}),
|
|
70
|
+
// Code of InboundMailLastErrors, never provider error text. Null on rows from before the column.
|
|
71
|
+
lastError: createTextField({ maxLength: 50, personal: false, reason: "technical_reference" }),
|
|
67
72
|
watchState: createTextField({ maxLength: 100, personal: false, reason: "technical_reference" }),
|
|
73
|
+
// Lower-case authserv-id of the mailbox's own server; null = none set.
|
|
74
|
+
trustedAuthservId: createTextField({
|
|
75
|
+
maxLength: 255,
|
|
76
|
+
personal: false,
|
|
77
|
+
reason: "technical_reference",
|
|
78
|
+
}),
|
|
68
79
|
connectedAt: createTimestampField({ required: true }),
|
|
69
80
|
},
|
|
70
81
|
});
|
|
@@ -82,8 +93,7 @@ export const inboundMessageEntity = createEntity({
|
|
|
82
93
|
reason: "technical_reference",
|
|
83
94
|
}),
|
|
84
95
|
// Scope is inherited from the account at ingest time: messages from a
|
|
85
|
-
// personal mailbox are visible only to the owner
|
|
86
|
-
// without joining read_mail_accounts.
|
|
96
|
+
// personal mailbox are visible only to the owner, without joining read_mail_accounts.
|
|
87
97
|
ownerUserId: createTextField({ maxLength: 36, personal: false, reason: "pseudonymous_fk" }),
|
|
88
98
|
messageIdHeader: createTextField({
|
|
89
99
|
required: true,
|
|
@@ -106,7 +116,40 @@ export const inboundMessageEntity = createEntity({
|
|
|
106
116
|
receivedAt: createTimestampField({ required: true }),
|
|
107
117
|
bodyRef: createTextField({ maxLength: 500, personal: false, reason: "technical_reference" }),
|
|
108
118
|
scope: createTextField({ maxLength: 200, personal: false, reason: "technical_reference" }),
|
|
119
|
+
// Full plaintext, capped at 32_000 characters before encryption (ciphertext is ~2.3x plus envelope).
|
|
120
|
+
// Null on rows from before the column; list-messages never selects it.
|
|
121
|
+
bodyText: createLongTextField({ maxLength: 200_000, personal: "tenant", find: "none" }),
|
|
122
|
+
bodyTextTruncated: createBooleanField(),
|
|
123
|
+
// Bound to the message (entityType = the aggregate type), so the file guard answers with the
|
|
124
|
+
// mail's visibility. The fileRefs are bound at ingest, the projection copies the ids.
|
|
125
|
+
attachmentFileIds: createFilesField({ personal: false, reason: "technical_reference" }),
|
|
126
|
+
// JSON-stringified SkippedInboundAttachment[]: nullable JSON stays a text column
|
|
127
|
+
// (jsonb fields are NOT NULL), like to/cc.
|
|
128
|
+
skippedAttachments: createTextField({
|
|
129
|
+
maxLength: 20_000,
|
|
130
|
+
personal: false,
|
|
131
|
+
reason: "technical_reference",
|
|
132
|
+
}),
|
|
133
|
+
// JSON-stringified SenderAuthentication or "null"; a SQL null (rows from before the column) reads as null.
|
|
134
|
+
senderAuthentication: createTextField({
|
|
135
|
+
maxLength: 4000,
|
|
136
|
+
personal: false,
|
|
137
|
+
reason: "technical_reference",
|
|
138
|
+
}),
|
|
139
|
+
// "complete" | "snippet" | "unavailable"; null counts as "snippet".
|
|
140
|
+
contentState: createTextField({
|
|
141
|
+
maxLength: 20,
|
|
142
|
+
personal: false,
|
|
143
|
+
reason: "technical_reference",
|
|
144
|
+
}),
|
|
145
|
+
// One of INBOUND_CONTENT_UNAVAILABLE_REASONS; null while the content is there or not yet missing.
|
|
146
|
+
contentUnavailableReason: createTextField({
|
|
147
|
+
maxLength: 30,
|
|
148
|
+
personal: false,
|
|
149
|
+
reason: "technical_reference",
|
|
150
|
+
}),
|
|
109
151
|
},
|
|
152
|
+
attachedFileAccess: "record-only",
|
|
110
153
|
});
|
|
111
154
|
|
|
112
155
|
/** Thread-Rollup. PK = aggregateId = mailThreadAggregateId(tenantId,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import * as z from "zod";
|
|
24
24
|
import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailAccountStatuses } from "./constants.ts";
|
|
25
|
+
import { senderAuthenticationFieldSchema } from "./sender-auth.ts";
|
|
25
26
|
|
|
26
27
|
// Aggregate types for the event store.
|
|
27
28
|
export const MAIL_ACCOUNT_AGGREGATE_TYPE = "mail-account" as const;
|
|
@@ -35,6 +36,8 @@ export const MAIL_ACCOUNT_UPDATED_EVENT_SHORT = "mail-account-updated" as const;
|
|
|
35
36
|
export const MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT = "mail-account-disconnected" as const;
|
|
36
37
|
export const INBOUND_MESSAGE_RECEIVED_EVENT_SHORT = "inbound-message-received" as const;
|
|
37
38
|
export const MAIL_THREAD_UPDATED_EVENT_SHORT = "mail-thread-updated" as const;
|
|
39
|
+
export const INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT =
|
|
40
|
+
"inbound-message-content-fetched" as const;
|
|
38
41
|
|
|
39
42
|
export const MAIL_ACCOUNT_CONNECTED_EVENT_QN =
|
|
40
43
|
`${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_ACCOUNT_CONNECTED_EVENT_SHORT}` as const;
|
|
@@ -44,6 +47,8 @@ export const MAIL_ACCOUNT_DISCONNECTED_EVENT_QN =
|
|
|
44
47
|
`${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT}` as const;
|
|
45
48
|
export const INBOUND_MESSAGE_RECEIVED_EVENT_QN =
|
|
46
49
|
`${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${INBOUND_MESSAGE_RECEIVED_EVENT_SHORT}` as const;
|
|
50
|
+
export const INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN =
|
|
51
|
+
`${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT}` as const;
|
|
47
52
|
export const MAIL_THREAD_UPDATED_EVENT_QN =
|
|
48
53
|
`${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_THREAD_UPDATED_EVENT_SHORT}` as const;
|
|
49
54
|
|
|
@@ -76,9 +81,51 @@ export const mailAccountEventPayloadSchema = z.object({
|
|
|
76
81
|
/** Provider-agnostic watch state ("idle", "watching", "backoff:3", ...)
|
|
77
82
|
* — an operational signal only, not PII. */
|
|
78
83
|
watchState: z.string().max(100),
|
|
84
|
+
/** Why the mailbox is degraded (InboundMailLastErrors code); null while healthy. A string rather
|
|
85
|
+
* than an enum, like authMethod, so a new code needs no upcaster. */
|
|
86
|
+
lastError: z.string().max(50).nullable(),
|
|
87
|
+
/** authserv-id (lower case) of the mailbox's own server; only an Authentication-Results header
|
|
88
|
+
* with exactly this id is believed (sender-auth.ts). null = not set, no sender verdicts. */
|
|
89
|
+
trustedAuthservId: z.string().max(255).nullable(),
|
|
79
90
|
});
|
|
80
91
|
export type MailAccountEventPayload = z.infer<typeof mailAccountEventPayloadSchema>;
|
|
81
92
|
|
|
93
|
+
export const INBOUND_CONTENT_STATES = ["complete", "snippet", "unavailable"] as const;
|
|
94
|
+
export type InboundContentState = (typeof INBOUND_CONTENT_STATES)[number];
|
|
95
|
+
|
|
96
|
+
/** Why a message has no content: held back by the quarantine (retry possible), no file storage wired, or gone from the server. */
|
|
97
|
+
export const INBOUND_CONTENT_UNAVAILABLE_REASONS = [
|
|
98
|
+
"quarantined",
|
|
99
|
+
"no_file_storage",
|
|
100
|
+
"gone_from_server",
|
|
101
|
+
] as const;
|
|
102
|
+
export type InboundContentUnavailableReason = (typeof INBOUND_CONTENT_UNAVAILABLE_REASONS)[number];
|
|
103
|
+
|
|
104
|
+
export const skippedAttachmentSchema = z.object({
|
|
105
|
+
mimeType: z.string().max(255),
|
|
106
|
+
size: z.number().int().min(0),
|
|
107
|
+
reason: z.enum([
|
|
108
|
+
"too_large",
|
|
109
|
+
"mail_too_large",
|
|
110
|
+
"rejected",
|
|
111
|
+
"no_file_storage",
|
|
112
|
+
"nesting_too_deep",
|
|
113
|
+
]),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Shared by inbound-message-received (v2+) and inbound-message-content-fetched.
|
|
117
|
+
// bodyText is ciphertext: 32_000 characters become roughly 75 KB, hence the generous max.
|
|
118
|
+
const inboundMessageContentShape = {
|
|
119
|
+
bodyText: z.string().max(200_000),
|
|
120
|
+
bodyTextTruncated: z.boolean(),
|
|
121
|
+
/** file-foundation FileRef ids (plaintext uuids). Attachments outlive the mail: apps turn them
|
|
122
|
+
* into documents, so the retention sweep never deletes them. */
|
|
123
|
+
attachmentFileIds: z.array(z.uuid()).max(50),
|
|
124
|
+
skippedAttachments: z.array(skippedAttachmentSchema).max(200),
|
|
125
|
+
contentState: z.enum(INBOUND_CONTENT_STATES),
|
|
126
|
+
contentUnavailableReason: z.enum(INBOUND_CONTENT_UNAVAILABLE_REASONS).nullable(),
|
|
127
|
+
};
|
|
128
|
+
|
|
82
129
|
// ============================================================================
|
|
83
130
|
// inbound-message stream — exactly ONE received event per message
|
|
84
131
|
// ============================================================================
|
|
@@ -110,15 +157,35 @@ export const inboundMessageEventPayloadSchema = z.object({
|
|
|
110
157
|
/** ISO instant — receipt time reported by the provider (INTERNALDATE or
|
|
111
158
|
* receivedDateTime), not ingest time. */
|
|
112
159
|
receivedAtIso: z.string().min(1),
|
|
113
|
-
/** file-foundation reference to the
|
|
114
|
-
*
|
|
160
|
+
/** file-foundation reference to the original (.eml, or .html for HTML-only mails). Empty for
|
|
161
|
+
* snippet-only mails and when no file storage is available. */
|
|
115
162
|
bodyRef: z.string().max(500),
|
|
116
163
|
/** Domain scope hint for downstream business processes — generic
|
|
117
164
|
* ("inbox", folder name), with NO app-specific semantics. */
|
|
118
165
|
scope: z.string().max(200),
|
|
166
|
+
/** SPF/DKIM/DMARC as reported by the mailbox's own server; null when there is no trustworthy
|
|
167
|
+
* statement. Not PII. */
|
|
168
|
+
senderAuthentication: senderAuthenticationFieldSchema,
|
|
169
|
+
...inboundMessageContentShape,
|
|
119
170
|
});
|
|
120
171
|
export type InboundMessageEventPayload = z.infer<typeof inboundMessageEventPayloadSchema>;
|
|
121
172
|
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Content of a message that was imported as snippet only and re-downloaded later.
|
|
175
|
+
// Same aggregate as the received event; applies as an update of the projection row.
|
|
176
|
+
// ============================================================================
|
|
177
|
+
export const inboundMessageContentFetchedEventPayloadSchema = z.object({
|
|
178
|
+
/** Account and scope of the message, so a subscriber can route the event without loading the
|
|
179
|
+
* stream. Null on events stored before v3: the stream id cannot be reversed to the account. */
|
|
180
|
+
accountId: z.uuid().nullable(),
|
|
181
|
+
ownerUserId: z.string().max(36).nullable(),
|
|
182
|
+
bodyRef: z.string().max(500),
|
|
183
|
+
...inboundMessageContentShape,
|
|
184
|
+
});
|
|
185
|
+
export type InboundMessageContentFetchedEventPayload = z.infer<
|
|
186
|
+
typeof inboundMessageContentFetchedEventPayloadSchema
|
|
187
|
+
>;
|
|
188
|
+
|
|
122
189
|
// ============================================================================
|
|
123
190
|
// mail-thread-Stream — Thread-Rollup pro (tenantId, threadKey)
|
|
124
191
|
// ============================================================================
|