@cosmicdrift/kumiko-samples 0.393.0 → 0.395.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 +143 -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 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -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 +238 -89
- 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 +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -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 +61 -136
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- 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 +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- 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-imap/watch-mailbox.ts +109 -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/changes.json +7 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
- package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
- package/packages/bundled-features/src/managed-pages/table.ts +6 -2
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
- package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -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/README.md +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
- package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
- 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-set.write.ts +24 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
- package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
- package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
- package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
- package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
- package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
- package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
- 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 -40
- 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 +8 -1
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
- 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 +8 -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
|
+
// Quarantine retry: mails flagged by retry-quarantined-message are loaded from the provider again
|
|
2
|
+
// after a successful sync. A mail that now goes through leaves the quarantine; one that fails again
|
|
3
|
+
// stays in it (flag reset, so it is not retried on every tick). An outage (provider, KMS, file
|
|
4
|
+
// storage) is not the mail's failure: the flag stays and the retry runs after the recovery.
|
|
5
|
+
|
|
6
|
+
import { ENCRYPTION_UNAVAILABLE_CODE } from "@cosmicdrift/kumiko-framework/crypto";
|
|
7
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
8
|
+
import { deleteMany, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
+
import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
|
+
import { inboundMessageAggregateId } from "./aggregate-id.ts";
|
|
11
|
+
import { InboundMailFoundationHandlers } from "./constants.ts";
|
|
12
|
+
import { describeError } from "./describe-error.ts";
|
|
13
|
+
import { isFileStorageWriteFailure } from "./file-storage-failure.ts";
|
|
14
|
+
import { clearIngestFailures } from "./ingest-failures.ts";
|
|
15
|
+
import { ingestPayloadOf } from "./ingest-payload.ts";
|
|
16
|
+
import {
|
|
17
|
+
type NormalizedInboundMessage,
|
|
18
|
+
normalizeRawInboundMessage,
|
|
19
|
+
} from "./normalize-inbound-message.ts";
|
|
20
|
+
import { inboundMessagesProjectionTable } from "./projection.ts";
|
|
21
|
+
import { quarantineTable } from "./quarantine-entities.ts";
|
|
22
|
+
import type { DispatchWrite } from "./supervisor-ingest.ts";
|
|
23
|
+
import type { InboundMailContext, InboundMailProviderPlugin, MailAccountRecord } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
const RETRY_BUDGET_PER_TICK = 20;
|
|
26
|
+
|
|
27
|
+
function isOutageFailure(error: unknown): boolean {
|
|
28
|
+
return isFileStorageWriteFailure(error) || errorCodeOf(error) === ENCRYPTION_UNAVAILABLE_CODE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A mail with a header-only entry gets its content attached, one without any entry is ingested in full.
|
|
32
|
+
function reloadMessage(
|
|
33
|
+
dispatchWrite: DispatchWrite,
|
|
34
|
+
account: MailAccountRecord,
|
|
35
|
+
msg: NormalizedInboundMessage,
|
|
36
|
+
hasHeaderRow: boolean,
|
|
37
|
+
): ReturnType<DispatchWrite> {
|
|
38
|
+
return hasHeaderRow
|
|
39
|
+
? dispatchWrite({
|
|
40
|
+
handlerQn: InboundMailFoundationHandlers.attachMessageContent,
|
|
41
|
+
tenantId: account.tenantId,
|
|
42
|
+
payload: {
|
|
43
|
+
accountId: account.id,
|
|
44
|
+
providerMessageId: msg.providerMessageId,
|
|
45
|
+
outcome: "fetched",
|
|
46
|
+
bodyText: msg.bodyText,
|
|
47
|
+
bodyTextTruncated: msg.bodyTextTruncated,
|
|
48
|
+
...(msg.rawMime != null && { rawMime: msg.rawMime }),
|
|
49
|
+
...(msg.bodyHtml !== null && { bodyHtml: msg.bodyHtml }),
|
|
50
|
+
attachments: msg.attachments,
|
|
51
|
+
skippedAttachments: msg.skippedAttachments,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
: dispatchWrite({
|
|
55
|
+
handlerQn: InboundMailFoundationHandlers.ingestMessage,
|
|
56
|
+
tenantId: account.tenantId,
|
|
57
|
+
payload: ingestPayloadOf(account, msg, {
|
|
58
|
+
providerCursor: "quarantine-retry",
|
|
59
|
+
bodyRef: "",
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function retryQuarantinedMessages(args: {
|
|
65
|
+
readonly db: DbConnection;
|
|
66
|
+
readonly dispatchWrite: DispatchWrite;
|
|
67
|
+
readonly providerCtx: InboundMailContext;
|
|
68
|
+
readonly account: MailAccountRecord;
|
|
69
|
+
readonly plugin: InboundMailProviderPlugin;
|
|
70
|
+
readonly accessToken?: string | undefined;
|
|
71
|
+
readonly signal?: AbortSignal | undefined;
|
|
72
|
+
readonly log: (line: string) => void;
|
|
73
|
+
}): Promise<void> {
|
|
74
|
+
const { db, account, plugin, dispatchWrite, log } = args;
|
|
75
|
+
// skip: the provider cannot reload single mails.
|
|
76
|
+
if (!plugin.fetchContent) return;
|
|
77
|
+
const requested = await selectMany<{ providerMessageId: string }>(
|
|
78
|
+
db,
|
|
79
|
+
quarantineTable,
|
|
80
|
+
{ accountId: account.id, retryRequestedAt: { ne: null } },
|
|
81
|
+
{ fields: ["providerMessageId"], limit: RETRY_BUDGET_PER_TICK },
|
|
82
|
+
);
|
|
83
|
+
// skip: nothing was asked for.
|
|
84
|
+
if (requested.length === 0) return;
|
|
85
|
+
const ids = requested.map((row) => row.providerMessageId);
|
|
86
|
+
const done = new Set<string>();
|
|
87
|
+
const held = new Set<string>();
|
|
88
|
+
try {
|
|
89
|
+
const result = await plugin.fetchContent(args.providerCtx, account, ids, {
|
|
90
|
+
...(args.accessToken !== undefined && { accessToken: args.accessToken }),
|
|
91
|
+
...(args.signal !== undefined && { signal: args.signal }),
|
|
92
|
+
});
|
|
93
|
+
const existing = new Set(
|
|
94
|
+
(
|
|
95
|
+
await selectMany<{ id: string }>(
|
|
96
|
+
db,
|
|
97
|
+
inboundMessagesProjectionTable,
|
|
98
|
+
{
|
|
99
|
+
tenantId: account.tenantId,
|
|
100
|
+
id: { in: ids.map((id) => inboundMessageAggregateId(account.id, id)) },
|
|
101
|
+
},
|
|
102
|
+
{ fields: ["id"] },
|
|
103
|
+
)
|
|
104
|
+
).map((row) => row.id),
|
|
105
|
+
);
|
|
106
|
+
for (const raw of result.messages) {
|
|
107
|
+
const msg = normalizeRawInboundMessage(raw);
|
|
108
|
+
const hasHeaderRow = existing.has(
|
|
109
|
+
inboundMessageAggregateId(account.id, msg.providerMessageId),
|
|
110
|
+
);
|
|
111
|
+
const outcome = await reloadMessage(dispatchWrite, account, msg, hasHeaderRow);
|
|
112
|
+
if (outcome.isSuccess) done.add(msg.providerMessageId);
|
|
113
|
+
else if (isOutageFailure(outcome.error)) held.add(msg.providerMessageId);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log(`inbound-mail: account ${account.id} quarantine retry failed: ${describeError(err)}`);
|
|
117
|
+
for (const id of ids) held.add(id);
|
|
118
|
+
}
|
|
119
|
+
const doneIds = [...done];
|
|
120
|
+
if (doneIds.length > 0) {
|
|
121
|
+
await deleteMany(db, quarantineTable, {
|
|
122
|
+
accountId: account.id,
|
|
123
|
+
providerMessageId: { in: doneIds },
|
|
124
|
+
});
|
|
125
|
+
await clearIngestFailures(db, account.id, doneIds);
|
|
126
|
+
}
|
|
127
|
+
const failed = ids.filter((id) => !done.has(id) && !held.has(id));
|
|
128
|
+
if (failed.length > 0) {
|
|
129
|
+
await updateMany(
|
|
130
|
+
db,
|
|
131
|
+
quarantineTable,
|
|
132
|
+
{ retryRequestedAt: null },
|
|
133
|
+
{ accountId: account.id, providerMessageId: { in: failed } },
|
|
134
|
+
);
|
|
135
|
+
log(`inbound-mail: account ${account.id} ${failed.length} quarantined mail(s) still failing`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
i18nKey,
|
|
3
|
+
MAX_LIST_LIMIT,
|
|
4
|
+
type ProjectionListScreenDefinition,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { InboundMailFoundationHandlers, InboundMailFoundationQueries } from "./constants.ts";
|
|
7
|
+
import type { InboundMailRoles } from "./mail-roles.ts";
|
|
8
|
+
import { MAIL_QUARANTINE_SCREEN_ID } from "./screens.ts";
|
|
9
|
+
|
|
10
|
+
export const QUARANTINE_REASON_OPTION_KEY_PREFIX = "inboundMail.quarantine.reason.";
|
|
11
|
+
|
|
12
|
+
// Same roles as the handlers; the bound query shows each caller only the mailboxes they may manage.
|
|
13
|
+
// dormant: reached through the row action "Quarantine" of the mailbox list.
|
|
14
|
+
export function createMailQuarantineScreen({
|
|
15
|
+
memberRoles,
|
|
16
|
+
}: InboundMailRoles): ProjectionListScreenDefinition {
|
|
17
|
+
return {
|
|
18
|
+
id: MAIL_QUARANTINE_SCREEN_ID,
|
|
19
|
+
type: "projectionList",
|
|
20
|
+
agent: { expose: false },
|
|
21
|
+
query: InboundMailFoundationQueries.listQuarantine,
|
|
22
|
+
pagination: false,
|
|
23
|
+
pageSize: MAX_LIST_LIMIT,
|
|
24
|
+
columns: [
|
|
25
|
+
{ field: "sender", label: i18nKey("inboundMail.quarantine.col.sender"), sortable: false },
|
|
26
|
+
{ field: "subject", label: i18nKey("inboundMail.quarantine.col.subject"), sortable: false },
|
|
27
|
+
{
|
|
28
|
+
field: "reason",
|
|
29
|
+
label: i18nKey("inboundMail.quarantine.col.reason"),
|
|
30
|
+
sortable: false,
|
|
31
|
+
renderer: { format: "enumOption", keyPrefix: QUARANTINE_REASON_OPTION_KEY_PREFIX },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
field: "accountName",
|
|
35
|
+
label: i18nKey("inboundMail.quarantine.col.mailbox"),
|
|
36
|
+
sortable: false,
|
|
37
|
+
hideOnNarrow: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
field: "quarantinedAt",
|
|
41
|
+
label: i18nKey("inboundMail.quarantine.col.date"),
|
|
42
|
+
sortable: false,
|
|
43
|
+
renderer: { format: "timestamp" },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
rowActions: [
|
|
47
|
+
{
|
|
48
|
+
kind: "writeHandler",
|
|
49
|
+
id: "retry",
|
|
50
|
+
label: i18nKey("inboundMail.quarantine.retry"),
|
|
51
|
+
handler: InboundMailFoundationHandlers.retryQuarantinedMessage,
|
|
52
|
+
payload: { map: { accountId: "accountId", providerMessageId: "providerMessageId" } },
|
|
53
|
+
visible: { field: "retryRequestedAt", eq: null },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
kind: "writeHandler",
|
|
57
|
+
id: "acknowledge",
|
|
58
|
+
label: i18nKey("inboundMail.quarantine.acknowledge"),
|
|
59
|
+
handler: InboundMailFoundationHandlers.acknowledgeQuarantine,
|
|
60
|
+
payload: { map: { accountId: "accountId", providerMessageId: "providerMessageId" } },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
access: { roles: [...memberRoles] },
|
|
64
|
+
dormant: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Quarantine of poison mails: messages the supervisor cannot ingest (unparseable MIME, payload the
|
|
2
|
+
// ingest handler rejects for good, or repeated hard failures). A quarantined id counts as seen, so the
|
|
3
|
+
// cursor moves on instead of retrying the same mail forever. Rows carry ids only, no mail content.
|
|
4
|
+
|
|
5
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
6
|
+
import { fetchOne, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
8
|
+
import { type QuarantineReasonCode, quarantineTable } from "./quarantine-entities.ts";
|
|
9
|
+
|
|
10
|
+
const LOOKUP_CHUNK_SIZE = 1000;
|
|
11
|
+
export const QUARANTINE_ACKNOWLEDGED_RETENTION_DAYS = 90;
|
|
12
|
+
|
|
13
|
+
type QuarantineTarget = {
|
|
14
|
+
readonly tenantId: string;
|
|
15
|
+
readonly accountId: string;
|
|
16
|
+
readonly providerMessageId: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Returns true when a new row was written, false when the message was already quarantined. */
|
|
20
|
+
export async function quarantineMessage(
|
|
21
|
+
db: DbConnection,
|
|
22
|
+
target: QuarantineTarget,
|
|
23
|
+
reasonCode: QuarantineReasonCode,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
const key = { accountId: target.accountId, providerMessageId: target.providerMessageId };
|
|
26
|
+
if (await fetchOne<{ id: string }>(db, quarantineTable, key)) return false;
|
|
27
|
+
try {
|
|
28
|
+
await insertOne(db, quarantineTable, {
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
tenantId: target.tenantId,
|
|
31
|
+
...key,
|
|
32
|
+
reasonCode,
|
|
33
|
+
quarantinedAt: Temporal.Now.instant().toString(),
|
|
34
|
+
});
|
|
35
|
+
return true;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// A concurrent worker quarantined the same message between the check and the insert.
|
|
38
|
+
if (await fetchOne<{ id: string }>(db, quarantineTable, key)) return false;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function quarantinedMessageIds(
|
|
44
|
+
db: DbConnection,
|
|
45
|
+
accountId: string,
|
|
46
|
+
providerMessageIds: readonly string[],
|
|
47
|
+
): Promise<ReadonlySet<string>> {
|
|
48
|
+
const ids = new Set<string>();
|
|
49
|
+
for (let start = 0; start < providerMessageIds.length; start += LOOKUP_CHUNK_SIZE) {
|
|
50
|
+
const rows = await selectMany<{ providerMessageId: string }>(
|
|
51
|
+
db,
|
|
52
|
+
quarantineTable,
|
|
53
|
+
{
|
|
54
|
+
accountId,
|
|
55
|
+
providerMessageId: { in: providerMessageIds.slice(start, start + LOOKUP_CHUNK_SIZE) },
|
|
56
|
+
},
|
|
57
|
+
{ fields: ["providerMessageId"] },
|
|
58
|
+
);
|
|
59
|
+
for (const row of rows) ids.add(row.providerMessageId);
|
|
60
|
+
}
|
|
61
|
+
return ids;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function hasOpenQuarantine(db: DbConnection, accountId: string): Promise<boolean> {
|
|
65
|
+
const rows = await selectMany<{ id: string }>(
|
|
66
|
+
db,
|
|
67
|
+
quarantineTable,
|
|
68
|
+
{ accountId, acknowledgedAt: null },
|
|
69
|
+
{ fields: ["id"], limit: 1 },
|
|
70
|
+
);
|
|
71
|
+
return rows.length > 0;
|
|
72
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import {
|
|
4
|
+
createFileProviderForTenant,
|
|
5
|
+
deleteFileRefsBoundToRecord,
|
|
6
|
+
type FileStorageProvider,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
8
|
+
import { INBOUND_MESSAGE_AGGREGATE_TYPE } from "./events.ts";
|
|
9
|
+
import { runInboundMailRetention } from "./retention-sweep.ts";
|
|
10
|
+
|
|
11
|
+
export function registerInboundMailRetentionJob(r: FeatureRegistrar<"inbound-mail-foundation">) {
|
|
12
|
+
// Data-Retention — perTenant fan-out (one run per tenant, like
|
|
13
|
+
// data-retention:retention-cleanup): runInboundMailRetention's raw query
|
|
14
|
+
// helpers need the unfiltered runner, so the handler declares escapeHatch
|
|
15
|
+
// and reaches for ctx.db.unsafeRaw(). Without this cron, read_inbound_messages
|
|
16
|
+
// + the bodies grow unbounded (GDPR Art. 5).
|
|
17
|
+
const INBOUND_MAIL_RETENTION_REASON =
|
|
18
|
+
"inbound-mail retention runs raw deletes; every statement filters by this run's tenantId";
|
|
19
|
+
r.job({
|
|
20
|
+
name: "inbound-mail-retention",
|
|
21
|
+
trigger: { cron: "0 3 * * *" },
|
|
22
|
+
perTenant: true,
|
|
23
|
+
concurrency: "skip",
|
|
24
|
+
escapeHatch: { reason: INBOUND_MAIL_RETENTION_REASON },
|
|
25
|
+
handler: async (_payload, ctx) => {
|
|
26
|
+
if (!ctx.registry) {
|
|
27
|
+
throw new Error("inbound-mail-retention: ctx.registry required (JobContext incomplete)");
|
|
28
|
+
}
|
|
29
|
+
const tenantId = ctx.systemUser?.tenantId ?? ctx._tenantId;
|
|
30
|
+
if (tenantId === undefined) {
|
|
31
|
+
// skip: cron fired without a perTenant fan-out tenant — nothing scoped
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
35
|
+
const retentionDb = ctx.db.unsafeRaw() as DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
36
|
+
|
|
37
|
+
// Original (.eml/.html) objects live in file-foundation; the provider is lazy and
|
|
38
|
+
// memoized so a tenant without purgeable mails never builds it.
|
|
39
|
+
let providerPromise: Promise<FileStorageProvider> | null = null;
|
|
40
|
+
const deleteBodyObject = async (bodyRef: string): Promise<void> => {
|
|
41
|
+
if (!providerPromise) {
|
|
42
|
+
providerPromise = createFileProviderForTenant(
|
|
43
|
+
{ config: ctx.config, registry: ctx.registry, secrets: ctx.services.secrets },
|
|
44
|
+
tenantId,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const provider = await providerPromise;
|
|
48
|
+
await provider.delete(bodyRef);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const { systemUser } = ctx;
|
|
52
|
+
const deleteAttachments = systemUser
|
|
53
|
+
? async (messageId: string): Promise<void> => {
|
|
54
|
+
await deleteFileRefsBoundToRecord({
|
|
55
|
+
db: ctx.db,
|
|
56
|
+
user: systemUser,
|
|
57
|
+
entityType: INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
58
|
+
entityId: messageId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
const report = await runInboundMailRetention({
|
|
64
|
+
db: retentionDb,
|
|
65
|
+
tenantId,
|
|
66
|
+
now: T.Now.instant(),
|
|
67
|
+
deleteBodyObject,
|
|
68
|
+
...(deleteAttachments ? { deleteAttachments } : {}),
|
|
69
|
+
});
|
|
70
|
+
if (report.bodyObjectErrors > 0) {
|
|
71
|
+
// biome-ignore lint/suspicious/noConsole: operator-visibility for failed body deletes
|
|
72
|
+
console.warn(
|
|
73
|
+
`[inbound-mail-foundation:inbound-mail-retention] tenant=${tenantId} bodyObjectErrors=${report.bodyObjectErrors}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
// directly through the table's tenant_id column (table-builder convention),
|
|
21
21
|
// without an Account join.
|
|
22
22
|
//
|
|
23
|
+
// store_mail_ingest_failures (unmanaged): counters of mails the ingest could not take. A counter
|
|
24
|
+
// whose mail never failed again (it went through or was quarantined, which clear it) is stale
|
|
25
|
+
// after the seen retention.
|
|
26
|
+
//
|
|
27
|
+
// store_mail_quarantine (unmanaged): acknowledged entries are purged after
|
|
28
|
+
// QUARANTINE_ACKNOWLEDGED_RETENTION_DAYS; unacknowledged ones stay, they
|
|
29
|
+
// keep the mailbox flagged until someone looked at them.
|
|
30
|
+
//
|
|
31
|
+
// Attachments are fileRefs bound to the mail row: they go with it (soft delete through the
|
|
32
|
+
// fileRef executor; the generic soft-delete cleanup removes the bytes). A failure keeps the Row
|
|
33
|
+
// for the next sweep, like a failed body delete.
|
|
34
|
+
//
|
|
23
35
|
// LIMITATION: the PII in the message events remains decryptable under the
|
|
24
36
|
// TENANT subject key—per-Row retention does not shred a key. Actual Art.-17
|
|
25
37
|
// erasure of event payloads happens only during tenant-destroy (key erasure).
|
|
@@ -35,7 +47,10 @@ import {
|
|
|
35
47
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
36
48
|
import { seenMessageTable } from "./entities.ts";
|
|
37
49
|
import { INBOUND_MESSAGE_AGGREGATE_TYPE } from "./events.ts";
|
|
50
|
+
import { ingestFailureTable } from "./ingest-failures.ts";
|
|
38
51
|
import { inboundMessagesProjectionTable } from "./projection.ts";
|
|
52
|
+
import { QUARANTINE_ACKNOWLEDGED_RETENTION_DAYS } from "./quarantine.ts";
|
|
53
|
+
import { quarantineTable } from "./quarantine-entities.ts";
|
|
39
54
|
|
|
40
55
|
// Provides Temporal.Instant without forcing the caller to obtain it from
|
|
41
56
|
// globalThis (same pattern as data-retention/keep-for.ts).
|
|
@@ -59,6 +74,8 @@ export interface InboundRetentionArgs {
|
|
|
59
74
|
/** Deletes a file-storage body object by bodyRef key. Wired lazily by the job
|
|
60
75
|
* (createFileProviderForTenant); if absent, no body is deleted. */
|
|
61
76
|
readonly deleteBodyObject?: (bodyRef: string) => Promise<void>;
|
|
77
|
+
/** Removes the fileRefs bound to an expired mail (its attachments); wired by the job. */
|
|
78
|
+
readonly deleteAttachments?: (messageId: string) => Promise<void>;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
export interface InboundRetentionReport {
|
|
@@ -66,6 +83,8 @@ export interface InboundRetentionReport {
|
|
|
66
83
|
readonly bodyObjectsDeleted: number;
|
|
67
84
|
readonly bodyObjectErrors: number;
|
|
68
85
|
readonly seenPurged: number;
|
|
86
|
+
readonly quarantinePurged: number;
|
|
87
|
+
readonly ingestFailuresPurged: number;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
export async function runInboundMailRetention(
|
|
@@ -79,7 +98,20 @@ export async function runInboundMailRetention(
|
|
|
79
98
|
const messages = await purgeExpiredMessages(args, messageCutoff);
|
|
80
99
|
const seenPurged = await purgeExpiredSeenAnchors(args.db, args.tenantId, seenCutoff);
|
|
81
100
|
|
|
82
|
-
|
|
101
|
+
const quarantineCutoff = args.now.subtract({
|
|
102
|
+
hours: QUARANTINE_ACKNOWLEDGED_RETENTION_DAYS * 24,
|
|
103
|
+
});
|
|
104
|
+
const quarantinePurged = await deleteMany(args.db, quarantineTable, {
|
|
105
|
+
tenantId: args.tenantId,
|
|
106
|
+
acknowledgedAt: { lt: quarantineCutoff },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const ingestFailuresPurged = await deleteMany(args.db, ingestFailureTable, {
|
|
110
|
+
tenantId: args.tenantId,
|
|
111
|
+
lastFailedAt: { lt: seenCutoff },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { ...messages, seenPurged, quarantinePurged, ingestFailuresPurged };
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
type ExpiredMessageRow = { readonly id: string; readonly bodyRef: string | null };
|
|
@@ -87,7 +119,9 @@ type ExpiredMessageRow = { readonly id: string; readonly bodyRef: string | null
|
|
|
87
119
|
async function purgeExpiredMessages(
|
|
88
120
|
args: InboundRetentionArgs,
|
|
89
121
|
cutoff: Instant,
|
|
90
|
-
): Promise<
|
|
122
|
+
): Promise<
|
|
123
|
+
Omit<InboundRetentionReport, "seenPurged" | "quarantinePurged" | "ingestFailuresPurged">
|
|
124
|
+
> {
|
|
91
125
|
let messagesPurged = 0;
|
|
92
126
|
let bodyObjectsDeleted = 0;
|
|
93
127
|
let bodyObjectErrors = 0;
|
|
@@ -125,15 +159,16 @@ async function purgeExpiredMessages(
|
|
|
125
159
|
archivedBy: ARCHIVED_BY,
|
|
126
160
|
reason: REASON,
|
|
127
161
|
});
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
try {
|
|
163
|
+
if (row.bodyRef && args.deleteBodyObject) {
|
|
130
164
|
await args.deleteBodyObject(row.bodyRef);
|
|
131
165
|
bodyObjectsDeleted++;
|
|
132
|
-
} catch {
|
|
133
|
-
bodyObjectErrors++;
|
|
134
|
-
failedIds.add(row.id);
|
|
135
|
-
continue;
|
|
136
166
|
}
|
|
167
|
+
await args.deleteAttachments?.(row.id);
|
|
168
|
+
} catch {
|
|
169
|
+
bodyObjectErrors++;
|
|
170
|
+
failedIds.add(row.id);
|
|
171
|
+
continue;
|
|
137
172
|
}
|
|
138
173
|
ids.push(row.id);
|
|
139
174
|
}
|