@cosmicdrift/kumiko-samples 0.392.2 → 0.394.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +12 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -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
|
+
}
|