@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
|
@@ -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
|
// ============================================================================
|
|
@@ -1,52 +1,28 @@
|
|
|
1
1
|
// kumiko-feature-version: 1
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
5
|
-
// extension points, separate state — architectural model is
|
|
6
|
-
// file-foundation (the foundation owns state), not mail-transport.
|
|
7
|
-
//
|
|
8
|
-
// **Multi-provider from day one** (as with billing-foundation): NO global
|
|
9
|
-
// `provider` config key. A tenant can connect IMAP and (later) M365/Gmail
|
|
10
|
-
// mailboxes in parallel; the provider is specified per MailAccount.
|
|
2
|
+
// inbound-mail-foundation: plugin host for inbound e-mail (inbox federation), the counterpart to
|
|
3
|
+
// mail-foundation (outbound) with its own extension points and state. Modelled on file-foundation
|
|
4
|
+
// (the foundation owns state), not on mail-transport.
|
|
11
5
|
//
|
|
12
6
|
// **What this foundation provides:**
|
|
13
|
-
// 1. **Plugin API** for
|
|
14
|
-
// ("inboundMailProvider"): verify/fetch/oauth?/watch? (types.ts).
|
|
7
|
+
// 1. **Plugin API** for providers via r.extendsRegistrar: verify/fetch/oauth?/watch? (types.ts).
|
|
15
8
|
// 2. **3 event-sourced streams** — mail-account (lifecycle),
|
|
16
9
|
// inbound-message (exactly ONE received event per message, deterministic
|
|
17
10
|
// aggregateId ⇒ exactly-once despite at-least-once ingest),
|
|
18
|
-
// mail-thread (rollup)
|
|
19
|
-
//
|
|
20
|
-
// 3. **Inline projections** into read_mail_accounts /
|
|
21
|
-
// read_inbound_messages / read_mail_threads (read-your-own-write).
|
|
11
|
+
// mail-thread (rollup), fully replayable for later consumers.
|
|
12
|
+
// 3. **Inline projections** into read_mail_accounts/_inbound_messages/_mail_threads (RYOW).
|
|
22
13
|
// 4. **Write/query handlers**: connect/update/disconnect-account,
|
|
23
14
|
// ingest-message (programmatic, SystemAdmin), account:list +
|
|
24
|
-
// message:list (PII-decrypted
|
|
25
|
-
// 5. **createInboundMailConnectRoutes**: OAuth connect + anonymous
|
|
26
|
-
// callback (outside /api/*), with HMAC-signed state.
|
|
27
|
-
// 6. **createInboundMailSupervisor**: app-wired watch (IDLE)
|
|
28
|
-
// supervisor with backoff + reconciliation poll.
|
|
15
|
+
// message:list (PII-decrypted; personal mail only for its owner).
|
|
29
16
|
//
|
|
30
|
-
// **
|
|
31
|
-
//
|
|
32
|
-
// completed) — those belong to the app consumer (for example, via an
|
|
33
|
-
// event job on inbound-message-received + its own projection).
|
|
34
|
-
// - No body blob in the event store: rawMime → file-foundation via
|
|
35
|
-
// the Supervisor-storeBody-Hook; the event carries only bodyRef.
|
|
36
|
-
// - No IMAP flag write-back (V1) — inbox status belongs to the consumer.
|
|
17
|
+
// **Not here:** business semantics (intent, ticket, assignment, read/completed) belong to the
|
|
18
|
+
// app consumer, for example an event job on inbound-message-received with its own projection.
|
|
37
19
|
//
|
|
38
|
-
// **PII/GDPR:** address/from/to/cc/subject/snippet are `personal: "tenant"
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// hook additionally archives all streams + deletes the rows.
|
|
20
|
+
// **PII/GDPR:** address/from/to/cc/subject/snippet are `personal: "tenant"`, encrypted BEFORE every
|
|
21
|
+
// event append (the only write path), so event log AND projections carry ciphertext; tenant-destroy
|
|
22
|
+
// shreds the subject key (crypto-shredding, as in billing-foundation) and its hook additionally
|
|
23
|
+
// archives all streams and deletes the rows.
|
|
43
24
|
|
|
44
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
45
25
|
import { defineFeature, EXT_TENANT_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
46
|
-
import {
|
|
47
|
-
createFileProviderForTenant,
|
|
48
|
-
type FileStorageProvider,
|
|
49
|
-
} from "@cosmicdrift/kumiko-framework/files";
|
|
50
26
|
import { INBOUND_MAIL_FOUNDATION_FEATURE, INBOUND_MAIL_PROVIDER_EXTENSION } from "./constants.ts";
|
|
51
27
|
import {
|
|
52
28
|
inboundMessageEntity,
|
|
@@ -57,8 +33,11 @@ import {
|
|
|
57
33
|
} from "./entities.ts";
|
|
58
34
|
import {
|
|
59
35
|
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
36
|
+
INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN,
|
|
37
|
+
INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT,
|
|
60
38
|
INBOUND_MESSAGE_RECEIVED_EVENT_QN,
|
|
61
39
|
INBOUND_MESSAGE_RECEIVED_EVENT_SHORT,
|
|
40
|
+
inboundMessageContentFetchedEventPayloadSchema,
|
|
62
41
|
inboundMessageEventPayloadSchema,
|
|
63
42
|
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
64
43
|
MAIL_ACCOUNT_CONNECTED_EVENT_QN,
|
|
@@ -73,15 +52,31 @@ import {
|
|
|
73
52
|
mailAccountEventPayloadSchema,
|
|
74
53
|
mailThreadEventPayloadSchema,
|
|
75
54
|
} from "./events.ts";
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
55
|
+
import { createAcknowledgeQuarantineHandler } from "./handlers/acknowledge-quarantine.write.ts";
|
|
56
|
+
import { attachMessageContentHandler } from "./handlers/attach-message-content.write.ts";
|
|
57
|
+
import { createConnectAccountHandler } from "./handlers/connect-account.write.ts";
|
|
58
|
+
import { createDisconnectAccountHandler } from "./handlers/disconnect-account.write.ts";
|
|
78
59
|
import { ingestMessageHandler } from "./handlers/ingest-message.write.ts";
|
|
79
|
-
import {
|
|
80
|
-
import {
|
|
81
|
-
import {
|
|
60
|
+
import { createListAccountsQuery } from "./handlers/list-accounts.query.ts";
|
|
61
|
+
import { createListMessagesQuery } from "./handlers/list-messages.query.ts";
|
|
62
|
+
import { createListQuarantineQuery } from "./handlers/list-quarantine.query.ts";
|
|
63
|
+
import { markCredentialStoredHandler } from "./handlers/mark-credential-stored.write.ts";
|
|
64
|
+
import { createMessageContentQuery } from "./handlers/message-content.query.ts";
|
|
65
|
+
import { createMessageDetailQuery } from "./handlers/message-detail.query.ts";
|
|
66
|
+
import { createReactivateAccountHandler } from "./handlers/reactivate-account.write.ts";
|
|
67
|
+
import { createRetryQuarantinedMessageHandler } from "./handlers/retry-quarantined-message.write.ts";
|
|
68
|
+
import { createSetTrustedAuthservIdHandler } from "./handlers/set-trusted-authserv-id.write.ts";
|
|
69
|
+
import { createUpdateAccountHandler } from "./handlers/update-account.write.ts";
|
|
82
70
|
import { INBOUND_MAIL_FOUNDATION_I18N } from "./i18n.ts";
|
|
71
|
+
import { ingestFailureTable } from "./ingest-failures.ts";
|
|
72
|
+
import {
|
|
73
|
+
type InboundMailRoleOptions,
|
|
74
|
+
resolveInboundMailRoles,
|
|
75
|
+
roleDedupeOptions,
|
|
76
|
+
} from "./mail-roles.ts";
|
|
83
77
|
import { registerMailboxProbe } from "./probe.ts";
|
|
84
78
|
import {
|
|
79
|
+
applyInboundMessageContentFetched,
|
|
85
80
|
applyInboundMessageReceived,
|
|
86
81
|
applyMailAccountConnected,
|
|
87
82
|
applyMailAccountDisconnected,
|
|
@@ -91,16 +86,20 @@ import {
|
|
|
91
86
|
mailAccountsProjectionTable,
|
|
92
87
|
mailThreadsProjectionTable,
|
|
93
88
|
} from "./projection.ts";
|
|
94
|
-
import {
|
|
95
|
-
import {
|
|
89
|
+
import { quarantineTable } from "./quarantine-entities.ts";
|
|
90
|
+
import { createMailQuarantineScreen } from "./quarantine-screens.ts";
|
|
91
|
+
import { registerInboundMailRetentionJob } from "./retention-job.ts";
|
|
92
|
+
import { createMailAccountScreens } from "./screens.ts";
|
|
96
93
|
import {
|
|
97
94
|
INBOUND_MAIL_TENANT_DESTROY_ARCHIVE_REASON,
|
|
98
95
|
inboundMessageTenantDestroyHook,
|
|
99
96
|
mailAccountTenantDestroyHook,
|
|
100
97
|
mailThreadTenantDestroyHook,
|
|
101
98
|
} from "./tenant-destroy-hook.ts";
|
|
99
|
+
import { registerMailAccountWakeConsumer } from "./wake-consumer.ts";
|
|
102
100
|
|
|
103
|
-
export function createInboundMailFoundationFeature() {
|
|
101
|
+
export function createInboundMailFoundationFeature(options: InboundMailRoleOptions = {}) {
|
|
102
|
+
const roles = resolveInboundMailRoles(options);
|
|
104
103
|
return defineFeature(
|
|
105
104
|
INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
106
105
|
(r) => {
|
|
@@ -116,28 +115,84 @@ export function createInboundMailFoundationFeature() {
|
|
|
116
115
|
// The EXT_TENANT_DATA hook requires the tenant-lifecycle host.
|
|
117
116
|
r.requires({ features: ["tenant-lifecycle"] });
|
|
118
117
|
|
|
119
|
-
// 5 fine-grained domain-events (Payload-Schemas: events.ts).
|
|
120
118
|
// piiFields: "none" — address/subject/snippet are already ciphertext under the tenant subject key before append (see events.ts).
|
|
121
119
|
// "displayName" stays plaintext (often the mailbox owner's real name for
|
|
122
120
|
// personal accounts); encrypting it would break the entity-projection read
|
|
123
121
|
// path that carries it as `personal: false`.
|
|
122
|
+
// v2: lastError, trustedAuthservId. Defaults are shared references across upcast events, so none of them is mutated.
|
|
123
|
+
const mailAccountMigrations = [
|
|
124
|
+
{
|
|
125
|
+
fromVersion: 1,
|
|
126
|
+
toVersion: 2,
|
|
127
|
+
transform: { default: { lastError: null, trustedAuthservId: null } },
|
|
128
|
+
},
|
|
129
|
+
] as const;
|
|
124
130
|
r.defineEvent(MAIL_ACCOUNT_CONNECTED_EVENT_SHORT, mailAccountEventPayloadSchema, {
|
|
125
131
|
piiFields: "none",
|
|
132
|
+
version: 2,
|
|
133
|
+
migrations: mailAccountMigrations,
|
|
126
134
|
});
|
|
127
135
|
r.defineEvent(MAIL_ACCOUNT_UPDATED_EVENT_SHORT, mailAccountEventPayloadSchema, {
|
|
128
136
|
piiFields: "none",
|
|
137
|
+
version: 2,
|
|
138
|
+
migrations: mailAccountMigrations,
|
|
129
139
|
});
|
|
130
140
|
r.defineEvent(MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT, mailAccountEventPayloadSchema, {
|
|
131
141
|
piiFields: "none",
|
|
142
|
+
version: 2,
|
|
143
|
+
migrations: mailAccountMigrations,
|
|
132
144
|
});
|
|
145
|
+
// v1 mails carry the snippet only: bodyText "" and contentState "snippet" let the content
|
|
146
|
+
// refetch find them. v3 adds the reason a message has no content.
|
|
133
147
|
r.defineEvent(INBOUND_MESSAGE_RECEIVED_EVENT_SHORT, inboundMessageEventPayloadSchema, {
|
|
134
148
|
piiFields: "none",
|
|
149
|
+
version: 3,
|
|
150
|
+
migrations: [
|
|
151
|
+
{
|
|
152
|
+
fromVersion: 1,
|
|
153
|
+
toVersion: 2,
|
|
154
|
+
transform: {
|
|
155
|
+
default: {
|
|
156
|
+
bodyText: "",
|
|
157
|
+
bodyTextTruncated: false,
|
|
158
|
+
attachmentFileIds: [],
|
|
159
|
+
skippedAttachments: [],
|
|
160
|
+
contentState: "snippet",
|
|
161
|
+
senderAuthentication: null,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
fromVersion: 2,
|
|
167
|
+
toVersion: 3,
|
|
168
|
+
transform: { default: { contentUnavailableReason: null } },
|
|
169
|
+
},
|
|
170
|
+
],
|
|
135
171
|
});
|
|
172
|
+
r.defineEvent(
|
|
173
|
+
INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT,
|
|
174
|
+
inboundMessageContentFetchedEventPayloadSchema,
|
|
175
|
+
{
|
|
176
|
+
piiFields: "none",
|
|
177
|
+
version: 3,
|
|
178
|
+
migrations: [
|
|
179
|
+
{
|
|
180
|
+
fromVersion: 1,
|
|
181
|
+
toVersion: 2,
|
|
182
|
+
transform: { default: { contentUnavailableReason: null } },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
fromVersion: 2,
|
|
186
|
+
toVersion: 3,
|
|
187
|
+
transform: { default: { accountId: null, ownerUserId: null } },
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
);
|
|
136
192
|
r.defineEvent(MAIL_THREAD_UPDATED_EVENT_SHORT, mailThreadEventPayloadSchema, {
|
|
137
193
|
piiFields: "none",
|
|
138
194
|
});
|
|
139
195
|
|
|
140
|
-
// Inline projections — apply in the same transaction as the append.
|
|
141
196
|
r.projection({
|
|
142
197
|
name: "mail-account",
|
|
143
198
|
source: MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
@@ -156,6 +211,7 @@ export function createInboundMailFoundationFeature() {
|
|
|
156
211
|
entity: inboundMessageEntity,
|
|
157
212
|
apply: {
|
|
158
213
|
[INBOUND_MESSAGE_RECEIVED_EVENT_QN]: applyInboundMessageReceived,
|
|
214
|
+
[INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN]: applyInboundMessageContentFetched,
|
|
159
215
|
},
|
|
160
216
|
});
|
|
161
217
|
r.projection({
|
|
@@ -168,6 +224,8 @@ export function createInboundMailFoundationFeature() {
|
|
|
168
224
|
},
|
|
169
225
|
});
|
|
170
226
|
|
|
227
|
+
registerMailAccountWakeConsumer(r);
|
|
228
|
+
|
|
171
229
|
// Plugin extension point — providers register via
|
|
172
230
|
// r.useExtension("inboundMailProvider", "<key>", plugin).
|
|
173
231
|
r.extendsRegistrar(INBOUND_MAIL_PROVIDER_EXTENSION, {
|
|
@@ -187,22 +245,34 @@ export function createInboundMailFoundationFeature() {
|
|
|
187
245
|
reason: "read_side.mail_seen_messages_direct_write",
|
|
188
246
|
});
|
|
189
247
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
r.storeTable(quarantineTable, {
|
|
249
|
+
reason: "read_side.mail_quarantine_direct_write",
|
|
250
|
+
});
|
|
251
|
+
r.storeTable(ingestFailureTable, {
|
|
252
|
+
reason: "read_side.mail_ingest_failures_direct_write",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ingest-message is programmatic (supervisor/poll, SystemAdmin); the rest is account lifecycle.
|
|
193
256
|
r.writeHandler(ingestMessageHandler);
|
|
194
|
-
r.writeHandler(
|
|
195
|
-
r.writeHandler(
|
|
196
|
-
r.writeHandler(
|
|
257
|
+
r.writeHandler(createConnectAccountHandler(roles));
|
|
258
|
+
r.writeHandler(createUpdateAccountHandler(roles));
|
|
259
|
+
r.writeHandler(createDisconnectAccountHandler(roles));
|
|
260
|
+
r.writeHandler(createReactivateAccountHandler(roles));
|
|
261
|
+
r.writeHandler(createSetTrustedAuthservIdHandler(roles));
|
|
262
|
+
r.writeHandler(createAcknowledgeQuarantineHandler(roles));
|
|
263
|
+
r.writeHandler(createRetryQuarantinedMessageHandler(roles));
|
|
264
|
+
r.writeHandler(attachMessageContentHandler);
|
|
265
|
+
r.writeHandler(markCredentialStoredHandler);
|
|
197
266
|
|
|
198
|
-
|
|
199
|
-
r.queryHandler(
|
|
200
|
-
r.queryHandler(
|
|
267
|
+
r.queryHandler(createListAccountsQuery(roles));
|
|
268
|
+
r.queryHandler(createListMessagesQuery(roles));
|
|
269
|
+
r.queryHandler(createListQuarantineQuery(roles));
|
|
270
|
+
r.queryHandler(createMessageContentQuery(roles));
|
|
271
|
+
r.queryHandler(createMessageDetailQuery(roles));
|
|
201
272
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
r.screen(
|
|
205
|
-
r.screen(mailAccountEditScreen);
|
|
273
|
+
// The app places the mailbox lists via r.nav (admins in settings, users next to their connect flow).
|
|
274
|
+
for (const screen of createMailAccountScreens(roles)) r.screen(screen);
|
|
275
|
+
r.screen(createMailQuarantineScreen(roles));
|
|
206
276
|
r.translations({ keys: INBOUND_MAIL_FOUNDATION_I18N });
|
|
207
277
|
registerMailboxProbe(r);
|
|
208
278
|
|
|
@@ -221,64 +291,9 @@ export function createInboundMailFoundationFeature() {
|
|
|
221
291
|
escapeHatch: { reason: INBOUND_MAIL_TENANT_DESTROY_ARCHIVE_REASON },
|
|
222
292
|
});
|
|
223
293
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// helpers need the unfiltered runner, so the handler declares escapeHatch
|
|
227
|
-
// and reaches for ctx.db.unsafeRaw(). Without this cron, read_inbound_messages
|
|
228
|
-
// + the bodies grow unbounded (GDPR Art. 5).
|
|
229
|
-
const INBOUND_MAIL_RETENTION_REASON =
|
|
230
|
-
"inbound-mail retention runs raw deletes; every statement filters by this run's tenantId";
|
|
231
|
-
r.job({
|
|
232
|
-
name: "inbound-mail-retention",
|
|
233
|
-
trigger: { cron: "0 3 * * *" },
|
|
234
|
-
perTenant: true,
|
|
235
|
-
concurrency: "skip",
|
|
236
|
-
escapeHatch: { reason: INBOUND_MAIL_RETENTION_REASON },
|
|
237
|
-
handler: async (_payload, ctx) => {
|
|
238
|
-
if (!ctx.registry) {
|
|
239
|
-
throw new Error(
|
|
240
|
-
"inbound-mail-retention: ctx.registry required (JobContext incomplete)",
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
const tenantId = ctx.systemUser?.tenantId ?? ctx._tenantId;
|
|
244
|
-
if (tenantId === undefined) {
|
|
245
|
-
// skip: cron fired without a perTenant fan-out tenant — nothing scoped
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
249
|
-
const retentionDb = ctx.db.unsafeRaw() as DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
250
|
-
|
|
251
|
-
// Body objects are stored in file-foundation. The provider is lazy and memoized —
|
|
252
|
-
// ingest currently writes no bodyRef (the storeBody hook is not wired),
|
|
253
|
-
// so the provider is never built. Once storeBody is wired, retention
|
|
254
|
-
// also cleans up the bodies.
|
|
255
|
-
let providerPromise: Promise<FileStorageProvider> | null = null;
|
|
256
|
-
const deleteBodyObject = async (bodyRef: string): Promise<void> => {
|
|
257
|
-
if (!providerPromise) {
|
|
258
|
-
providerPromise = createFileProviderForTenant(
|
|
259
|
-
{ config: ctx.config, registry: ctx.registry, secrets: ctx.services.secrets },
|
|
260
|
-
tenantId,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
const provider = await providerPromise;
|
|
264
|
-
await provider.delete(bodyRef);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const report = await runInboundMailRetention({
|
|
268
|
-
db: retentionDb,
|
|
269
|
-
tenantId,
|
|
270
|
-
now: T.Now.instant(),
|
|
271
|
-
deleteBodyObject,
|
|
272
|
-
});
|
|
273
|
-
if (report.bodyObjectErrors > 0) {
|
|
274
|
-
// biome-ignore lint/suspicious/noConsole: operator-visibility for failed body deletes
|
|
275
|
-
console.warn(
|
|
276
|
-
`[inbound-mail-foundation:inbound-mail-retention] tenant=${tenantId} bodyObjectErrors=${report.bodyObjectErrors}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
});
|
|
294
|
+
registerInboundMailRetentionJob(r);
|
|
295
|
+
return { roles };
|
|
281
296
|
},
|
|
282
|
-
{ dedupeOptions:
|
|
297
|
+
{ dedupeOptions: roleDedupeOptions(roles) },
|
|
283
298
|
);
|
|
284
299
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
+
|
|
4
|
+
export const FILE_STORAGE_WRITE_FAILED_CODE = "file_storage_write_failed";
|
|
5
|
+
|
|
6
|
+
// Own code on purpose: the supervisor treats validation/unprocessable as a poison mail and
|
|
7
|
+
// quarantines it, but a storage outage says nothing about the mail.
|
|
8
|
+
export class FileStorageWriteFailedError extends KumikoError {
|
|
9
|
+
readonly code = FILE_STORAGE_WRITE_FAILED_CODE;
|
|
10
|
+
readonly httpStatus = 503;
|
|
11
|
+
|
|
12
|
+
constructor(cause?: Error) {
|
|
13
|
+
super({
|
|
14
|
+
message: "inbound mail content could not be written to file storage",
|
|
15
|
+
i18nKey: "errors.internal",
|
|
16
|
+
...(cause && { cause }),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isFileStorageWriteFailure(error: unknown): boolean {
|
|
22
|
+
return errorCodeOf(error) === FILE_STORAGE_WRITE_FAILED_CODE;
|
|
23
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Address lookup across the tenant's mailboxes. Addresses are ciphertext in the projection, so the
|
|
2
|
+
// comparison happens in memory after decrypting (a tenant has a handful of mailboxes).
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
configuredPiiSubjectKms,
|
|
6
|
+
decryptPiiFieldValues,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
8
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import { InboundMailAccountStatuses, InboundMailAuthMethods } from "../constants.ts";
|
|
10
|
+
import { MAIL_ACCOUNT_PII_FIELDS } from "../entities.ts";
|
|
11
|
+
import { mailAccountsProjectionTable } from "../projection.ts";
|
|
12
|
+
|
|
13
|
+
export type AddressMatch = {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
readonly authMethod: string;
|
|
17
|
+
readonly ownerUserId: string | null;
|
|
18
|
+
readonly status: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function normalizeMailAddress(address: string): string {
|
|
22
|
+
return address.trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function textOf(value: unknown): string {
|
|
26
|
+
return typeof value === "string" ? value : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A live mailbox wins over a disconnected one: connecting next to a live mailbox is always a duplicate.
|
|
30
|
+
export async function findMailboxByAddress(
|
|
31
|
+
ctx: Pick<HandlerContext, "db">,
|
|
32
|
+
tenantId: string,
|
|
33
|
+
address: string,
|
|
34
|
+
): Promise<AddressMatch | undefined> {
|
|
35
|
+
const wanted = normalizeMailAddress(address);
|
|
36
|
+
const rows = await ctx.db.selectMany(mailAccountsProjectionTable, { tenantId });
|
|
37
|
+
const piiKms = configuredPiiSubjectKms();
|
|
38
|
+
const matches: AddressMatch[] = [];
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
const readable = piiKms
|
|
41
|
+
? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
42
|
+
requestId: `inbound-mail-foundation:connect-account:${tenantId}`,
|
|
43
|
+
})
|
|
44
|
+
: row;
|
|
45
|
+
if (normalizeMailAddress(textOf(readable["address"])) !== wanted) continue;
|
|
46
|
+
matches.push({
|
|
47
|
+
id: textOf(row["id"]),
|
|
48
|
+
provider: textOf(row["provider"]),
|
|
49
|
+
authMethod: textOf(row["authMethod"]),
|
|
50
|
+
ownerUserId: typeof row["ownerUserId"] === "string" ? row["ownerUserId"] : null,
|
|
51
|
+
status: textOf(row["status"]),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return matches.find((m) => m.status !== InboundMailAccountStatuses.disconnected) ?? matches[0];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ConnectConflict = "already_connected" | "address_disconnected" | "reopen_in_place";
|
|
58
|
+
|
|
59
|
+
// A disconnected or sign-in-failed OAuth mailbox of the same provider and owner is reopened (the
|
|
60
|
+
// callback writes the new token into the same slot); everything else must not silently create a second stream for one address.
|
|
61
|
+
export function connectConflictOf(
|
|
62
|
+
existing: AddressMatch,
|
|
63
|
+
wanted: {
|
|
64
|
+
readonly authMethod: string;
|
|
65
|
+
readonly provider: string;
|
|
66
|
+
readonly ownerUserId: string | null;
|
|
67
|
+
},
|
|
68
|
+
): ConnectConflict {
|
|
69
|
+
const disconnected = existing.status === InboundMailAccountStatuses.disconnected;
|
|
70
|
+
// A sign-in error of an OAuth mailbox is healed by signing in again, so it reopens like a disconnect.
|
|
71
|
+
const signInFailed = existing.status === InboundMailAccountStatuses.authError;
|
|
72
|
+
if (!disconnected && !signInFailed) return "already_connected";
|
|
73
|
+
const bothOauth =
|
|
74
|
+
wanted.authMethod === InboundMailAuthMethods.oauth &&
|
|
75
|
+
existing.authMethod === InboundMailAuthMethods.oauth &&
|
|
76
|
+
existing.provider === wanted.provider;
|
|
77
|
+
if (!bothOauth) return disconnected ? "address_disconnected" : "already_connected";
|
|
78
|
+
return existing.ownerUserId === wanted.ownerUserId ? "reopen_in_place" : "already_connected";
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// acknowledge-quarantine — the mailbox owner (an admin for a shared mailbox) has seen quarantined mails. Open entries (all of
|
|
2
|
+
// the mailbox, or just one when providerMessageId is given) are marked acknowledged; once none is left
|
|
3
|
+
// the supervisor returns the mailbox to active.
|
|
4
|
+
|
|
5
|
+
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import {
|
|
7
|
+
AccessDeniedError,
|
|
8
|
+
failNotFound,
|
|
9
|
+
writeFailure,
|
|
10
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
11
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
12
|
+
import * as z from "zod";
|
|
13
|
+
import type { InboundMailRoles } from "../mail-roles.ts";
|
|
14
|
+
import { quarantineTable } from "../quarantine-entities.ts";
|
|
15
|
+
import { loadCurrentMailAccountPayload } from "./account-state.ts";
|
|
16
|
+
import { mayManageQuarantine } from "./scope-visibility.ts";
|
|
17
|
+
|
|
18
|
+
export const acknowledgeQuarantineSchema = z.object({
|
|
19
|
+
accountId: z.uuid(),
|
|
20
|
+
providerMessageId: z.string().min(1).max(500).optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function createAcknowledgeQuarantineHandler({
|
|
24
|
+
memberRoles,
|
|
25
|
+
adminRoles,
|
|
26
|
+
}: InboundMailRoles): WriteHandlerDef {
|
|
27
|
+
return {
|
|
28
|
+
name: "acknowledge-quarantine",
|
|
29
|
+
schema: acknowledgeQuarantineSchema,
|
|
30
|
+
access: { roles: [...memberRoles] },
|
|
31
|
+
agent: { expose: false },
|
|
32
|
+
description:
|
|
33
|
+
"Marks the open quarantine entries of a mailbox as acknowledged, all of them or only the one mail named by providerMessageId, i.e. the mails the import could not take have been looked at; the mailbox leaves the interrupted state on the next sync. The owner of a personal mailbox or, for a shared one, an admin may call it. Returns the number of acknowledged entries.",
|
|
34
|
+
handler: async (event, ctx) => {
|
|
35
|
+
const payload = acknowledgeQuarantineSchema.parse(event.payload);
|
|
36
|
+
const account = await loadCurrentMailAccountPayload(ctx, payload.accountId);
|
|
37
|
+
if (!account) return failNotFound("mail-account", payload.accountId);
|
|
38
|
+
if (!mayManageQuarantine(account.ownerUserId, event.user, adminRoles)) {
|
|
39
|
+
return writeFailure(new AccessDeniedError({ details: { reason: "not_mailbox_owner" } }));
|
|
40
|
+
}
|
|
41
|
+
const updated = await ctx.db.updateMany(
|
|
42
|
+
quarantineTable,
|
|
43
|
+
{ acknowledgedAt: Temporal.Now.instant().toString() },
|
|
44
|
+
{
|
|
45
|
+
accountId: payload.accountId,
|
|
46
|
+
acknowledgedAt: null,
|
|
47
|
+
...(payload.providerMessageId !== undefined && {
|
|
48
|
+
providerMessageId: payload.providerMessageId,
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
return { isSuccess: true as const, data: { acknowledged: updated.length } };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|