@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
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { fieldLabelKey } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
5
|
+
InboundMailAccountStatuses,
|
|
6
|
+
InboundMailConnectErrors,
|
|
7
|
+
InboundMailLastErrors,
|
|
8
|
+
} from "./constants.ts";
|
|
4
9
|
import { INBOUND_PROBE_I18N } from "./probe-keys.ts";
|
|
10
|
+
import { QUARANTINE_REASON_OPTION_KEY_PREFIX } from "./quarantine-screens.ts";
|
|
5
11
|
import {
|
|
6
12
|
MAIL_ACCOUNT_EDIT_SCREEN_ID,
|
|
13
|
+
MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX,
|
|
7
14
|
MAIL_ACCOUNT_LIST_SCREEN_ID,
|
|
15
|
+
MAIL_ACCOUNT_SENDER_CHECK_SCREEN_ID,
|
|
8
16
|
MAIL_ACCOUNT_STATUS_OPTION_KEY_PREFIX,
|
|
17
|
+
MAIL_QUARANTINE_SCREEN_ID,
|
|
18
|
+
MY_MAIL_ACCOUNT_LIST_SCREEN_ID,
|
|
9
19
|
} from "./screens.ts";
|
|
10
20
|
import { MAIL_SEND_ERRORS } from "./send-from-account.ts";
|
|
11
21
|
|
|
@@ -15,12 +25,42 @@ const formFieldKey = (field: string): string =>
|
|
|
15
25
|
export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en: string }>> = {
|
|
16
26
|
...INBOUND_PROBE_I18N,
|
|
17
27
|
[`screen:${MAIL_ACCOUNT_LIST_SCREEN_ID}.title`]: { en: "Mailboxes" },
|
|
28
|
+
[`screen:${MY_MAIL_ACCOUNT_LIST_SCREEN_ID}.title`]: { en: "Your mailboxes" },
|
|
18
29
|
[`screen:${MAIL_ACCOUNT_EDIT_SCREEN_ID}.title`]: { en: "Edit mailbox" },
|
|
30
|
+
[`screen:${MAIL_ACCOUNT_SENDER_CHECK_SCREEN_ID}.title`]: { en: "Sender check" },
|
|
31
|
+
[`screen:${MAIL_QUARANTINE_SCREEN_ID}.title`]: { en: "Quarantined mails" },
|
|
19
32
|
"inboundMail.accounts.col.name": { en: "Name" },
|
|
20
33
|
"inboundMail.accounts.col.address": { en: "Address" },
|
|
21
34
|
"inboundMail.accounts.col.provider": { en: "Provider" },
|
|
22
35
|
"inboundMail.accounts.col.status": { en: "Status" },
|
|
23
36
|
"inboundMail.accounts.col.connected": { en: "Connected" },
|
|
37
|
+
"inboundMail.accounts.col.lastError": { en: "Problem" },
|
|
38
|
+
"inboundMail.accounts.col.quarantined": { en: "Quarantined" },
|
|
39
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.connectionFailed}`]: {
|
|
40
|
+
en: "The mail server could not be reached. Syncing is retried automatically.",
|
|
41
|
+
},
|
|
42
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.rateLimited}`]: {
|
|
43
|
+
en: "The mail server is limiting requests. Syncing is retried automatically.",
|
|
44
|
+
},
|
|
45
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.messageQuarantined}`]: {
|
|
46
|
+
en: "A mail could not be imported and was set aside. Confirm the quarantine once you have looked into it.",
|
|
47
|
+
},
|
|
48
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.fileStorageUnavailable}`]: {
|
|
49
|
+
en: "File storage is not available, so attachments and originals are not saved.",
|
|
50
|
+
},
|
|
51
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.encryptionUnavailable}`]: {
|
|
52
|
+
en: "The key store is unavailable. New mails are fetched again in the next interval.",
|
|
53
|
+
},
|
|
54
|
+
[`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.ingestUnavailable}`]: {
|
|
55
|
+
en: "Mails cannot be imported right now. The next interval tries again.",
|
|
56
|
+
},
|
|
57
|
+
[InboundMailConnectErrors.addressAlreadyConnected.i18nKey]: {
|
|
58
|
+
en: "This mailbox is already connected.",
|
|
59
|
+
},
|
|
60
|
+
[InboundMailConnectErrors.addressDisconnected.i18nKey]: {
|
|
61
|
+
en: "This mailbox was disconnected before. Use Reactivate in the mailbox list to keep its history.",
|
|
62
|
+
},
|
|
63
|
+
"inboundMail.accounts.acknowledgeQuarantine": { en: "Confirm quarantine" },
|
|
24
64
|
[`${MAIL_ACCOUNT_STATUS_OPTION_KEY_PREFIX}${InboundMailAccountStatuses.active}`]: {
|
|
25
65
|
en: "Active",
|
|
26
66
|
},
|
|
@@ -34,9 +74,41 @@ export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en
|
|
|
34
74
|
en: "Disconnected",
|
|
35
75
|
},
|
|
36
76
|
"inboundMail.accounts.connectImap": { en: "Connect mailbox" },
|
|
37
|
-
"inboundMail.accounts.
|
|
77
|
+
"inboundMail.accounts.reactivateOauth.confirm": {
|
|
78
|
+
en: "You are taken to the provider's sign-in page. After signing in, the mailbox fetches mail again and keeps its history.",
|
|
79
|
+
},
|
|
80
|
+
"inboundMail.accounts.reconnect.invalidRow": {
|
|
81
|
+
en: "This mailbox cannot be reactivated from here.",
|
|
82
|
+
},
|
|
83
|
+
"inboundMail.accounts.reactivate": { en: "Reactivate" },
|
|
84
|
+
"inboundMail.accounts.quarantine": { en: "Quarantine" },
|
|
85
|
+
[InboundMailConnectErrors.credentialRequired.i18nKey]: {
|
|
86
|
+
en: "This mailbox has no stored sign-in. Enter the password again to reactivate it.",
|
|
87
|
+
},
|
|
88
|
+
"inboundMail.quarantine.col.sender": { en: "Sender" },
|
|
89
|
+
"inboundMail.quarantine.col.subject": { en: "Subject" },
|
|
90
|
+
"inboundMail.quarantine.col.reason": { en: "Reason" },
|
|
91
|
+
"inboundMail.quarantine.col.mailbox": { en: "Mailbox" },
|
|
92
|
+
"inboundMail.quarantine.col.date": { en: "Set aside" },
|
|
93
|
+
"inboundMail.quarantine.retry": { en: "Fetch again" },
|
|
94
|
+
"inboundMail.quarantine.acknowledge": { en: "Confirm" },
|
|
95
|
+
[`${QUARANTINE_REASON_OPTION_KEY_PREFIX}unparseable`]: {
|
|
96
|
+
en: "The mail could not be read.",
|
|
97
|
+
},
|
|
98
|
+
[`${QUARANTINE_REASON_OPTION_KEY_PREFIX}invalid_message`]: {
|
|
99
|
+
en: "The mail is incomplete or invalid.",
|
|
100
|
+
},
|
|
101
|
+
[`${QUARANTINE_REASON_OPTION_KEY_PREFIX}ingest_failed`]: {
|
|
102
|
+
en: "Importing the mail failed repeatedly.",
|
|
103
|
+
},
|
|
38
104
|
"inboundMail.accounts.edit": { en: "Rename" },
|
|
39
105
|
"inboundMail.accounts.edit.submit": { en: "Save" },
|
|
106
|
+
"inboundMail.accounts.senderCheck": { en: "Sender check" },
|
|
107
|
+
"inboundMail.accounts.senderCheck.submit": { en: "Save" },
|
|
108
|
+
"inboundMail.accounts.senderCheckHint": {
|
|
109
|
+
en: "Identifier of the mail server from the Authentication-Results header, for example mx.example.com. Only with it the system checks whether a sender is genuine. Leave empty to turn the check off.",
|
|
110
|
+
},
|
|
111
|
+
[formFieldKey("trustedAuthservId")]: { en: "Mail server ID" },
|
|
40
112
|
"inboundMail.accounts.disconnect": { en: "Disconnect" },
|
|
41
113
|
"inboundMail.accounts.disconnect.confirm": {
|
|
42
114
|
en: "Disconnect this mailbox? No new mail is fetched; stored mail is kept.",
|
|
@@ -58,4 +130,10 @@ export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en
|
|
|
58
130
|
[MAIL_SEND_ERRORS.invalidMessage.i18nKey]: {
|
|
59
131
|
en: "The mail was not sent: a recipient, the subject or the reply reference is invalid.",
|
|
60
132
|
},
|
|
133
|
+
[MAIL_SEND_ERRORS.attachmentInvalid.i18nKey]: {
|
|
134
|
+
en: "The mail was not sent: an attachment is no longer available.",
|
|
135
|
+
},
|
|
136
|
+
[MAIL_SEND_ERRORS.attachmentsTooLarge.i18nKey]: {
|
|
137
|
+
en: "The mail was not sent: the attachments together are larger than 18 MB.",
|
|
138
|
+
},
|
|
61
139
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { InboundMailContext, MailAccountRecord } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export type OAuthTokenSet = {
|
|
4
|
+
readonly accessToken: string;
|
|
5
|
+
/** ISO-Instant. TTL typisch ~1h ⇒ Foundation refresht before-poll. */
|
|
6
|
+
readonly expiresAt: string;
|
|
7
|
+
readonly refreshToken?: string;
|
|
8
|
+
readonly scopesGranted: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type OAuthCodeExchange = OAuthTokenSet & {
|
|
12
|
+
/** Mailbox address of the account that signed in, from a token claim or the provider's profile
|
|
13
|
+
* call. The callback refuses a mailbox whose address differs, so nobody claims someone else's. */
|
|
14
|
+
readonly accountAddress: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OAuth portion of the contract — OAuth providers only; imap in password
|
|
19
|
+
* mode omits `oauth`. The Foundation provides the routes
|
|
20
|
+
* (`/inbound-mail/connect`, `/inbound-mail/oauth/callback`) generically;
|
|
21
|
+
* providers MUST NOT register their own routes.
|
|
22
|
+
*/
|
|
23
|
+
export type InboundOAuthFlow = {
|
|
24
|
+
/** Scopes: request receive AND send in a SINGLE consent; re-consent is costly. */
|
|
25
|
+
readonly scopes: {
|
|
26
|
+
readonly receive: readonly string[];
|
|
27
|
+
readonly send?: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
readonly buildAuthorizeUrl: (
|
|
30
|
+
ctx: InboundMailContext,
|
|
31
|
+
p: { readonly state: string; readonly redirectUri: string },
|
|
32
|
+
) => Promise<string>;
|
|
33
|
+
readonly exchangeCode: (
|
|
34
|
+
ctx: InboundMailContext,
|
|
35
|
+
p: { readonly code: string; readonly redirectUri: string },
|
|
36
|
+
) => Promise<OAuthCodeExchange>;
|
|
37
|
+
readonly refreshAccessToken: (
|
|
38
|
+
ctx: InboundMailContext,
|
|
39
|
+
account: MailAccountRecord,
|
|
40
|
+
refreshToken: string,
|
|
41
|
+
/** The foundation aborts `signal` after a hard timeout while holding a
|
|
42
|
+
* per-account lock (a pool connection is pinned meanwhile). Providers
|
|
43
|
+
* MUST pass it to their token-endpoint fetch; the foundation awaits the
|
|
44
|
+
* provider's result and never drops a late one, because that could lose
|
|
45
|
+
* a rotated refresh token. */
|
|
46
|
+
opts?: { readonly signal?: AbortSignal },
|
|
47
|
+
) => Promise<OAuthTokenSet>;
|
|
48
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Public API of the inbound-mail-foundation bundled-feature.
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
type MailAccountWakeListener,
|
|
5
|
+
registerMailAccountWakeListener,
|
|
6
|
+
wakeMailAccount,
|
|
7
|
+
} from "./account-wake.ts";
|
|
3
8
|
export {
|
|
4
9
|
inboundMessageAggregateId,
|
|
5
10
|
mailThreadAggregateId,
|
|
@@ -9,15 +14,23 @@ export {
|
|
|
9
14
|
type InboundMailConnectRoutesOptions,
|
|
10
15
|
} from "./connect-routes.ts";
|
|
11
16
|
export {
|
|
17
|
+
IMAP_PROVIDER_KEY,
|
|
18
|
+
INBOUND_MAIL_CONNECT_PATH,
|
|
12
19
|
INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
13
20
|
INBOUND_MAIL_PROVIDER_EXTENSION,
|
|
21
|
+
INBOUND_MAIL_RECONNECT_OAUTH_ACTION,
|
|
14
22
|
type InboundMailAccountStatus,
|
|
15
23
|
InboundMailAccountStatuses,
|
|
16
24
|
type InboundMailAuthMethod,
|
|
17
25
|
InboundMailAuthMethods,
|
|
26
|
+
InboundMailConnectErrors,
|
|
18
27
|
InboundMailFoundationHandlers,
|
|
19
28
|
InboundMailFoundationQueries,
|
|
29
|
+
type InboundMailLastError,
|
|
30
|
+
InboundMailLastErrors,
|
|
20
31
|
inboundCredentialSecretKey,
|
|
32
|
+
type MailAccountReactivationPath,
|
|
33
|
+
MailAccountReactivationPaths,
|
|
21
34
|
} from "./constants.ts";
|
|
22
35
|
export {
|
|
23
36
|
INBOUND_MESSAGE_PII_FIELDS,
|
|
@@ -33,10 +46,14 @@ export {
|
|
|
33
46
|
} from "./entities.ts";
|
|
34
47
|
export {
|
|
35
48
|
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
49
|
+
INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN,
|
|
50
|
+
INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT,
|
|
36
51
|
INBOUND_MESSAGE_RECEIVED_EVENT_QN,
|
|
37
52
|
INBOUND_MESSAGE_RECEIVED_EVENT_SHORT,
|
|
53
|
+
type InboundMessageContentFetchedEventPayload,
|
|
38
54
|
type InboundMessageEventHeaders,
|
|
39
55
|
type InboundMessageEventPayload,
|
|
56
|
+
inboundMessageContentFetchedEventPayloadSchema,
|
|
40
57
|
inboundMessageEventPayloadSchema,
|
|
41
58
|
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
42
59
|
MAIL_ACCOUNT_CONNECTED_EVENT_QN,
|
|
@@ -55,6 +72,36 @@ export {
|
|
|
55
72
|
mailThreadEventPayloadSchema,
|
|
56
73
|
} from "./events.ts";
|
|
57
74
|
export { createInboundMailFoundationFeature } from "./feature.ts";
|
|
75
|
+
export { type AddressMatch, findMailboxByAddress } from "./handlers/account-address.ts";
|
|
76
|
+
export {
|
|
77
|
+
type InboundMessageContent,
|
|
78
|
+
loadInboundMessageContent,
|
|
79
|
+
} from "./handlers/message-content.query.ts";
|
|
80
|
+
export { isMailAdmin, mayReactivateMailbox } from "./handlers/scope-visibility.ts";
|
|
81
|
+
export type { InboundOAuthFlow, OAuthCodeExchange, OAuthTokenSet } from "./inbound-oauth-flow.ts";
|
|
82
|
+
export { ingestFailureEntity, ingestFailureTable } from "./ingest-failures.ts";
|
|
83
|
+
export {
|
|
84
|
+
assertRolesMatchFoundation,
|
|
85
|
+
DEFAULT_INBOUND_MAIL_ADMIN_ROLES,
|
|
86
|
+
DEFAULT_INBOUND_MAIL_USER_ROLES,
|
|
87
|
+
type InboundMailRoleOptions,
|
|
88
|
+
type InboundMailRoles,
|
|
89
|
+
readInboundMailRoles,
|
|
90
|
+
resolveInboundMailRoles,
|
|
91
|
+
roleDedupeOptions,
|
|
92
|
+
SYSTEM_DISPATCHED_MAIL_HANDLERS,
|
|
93
|
+
} from "./mail-roles.ts";
|
|
94
|
+
export {
|
|
95
|
+
INBOUND_ATTACHMENT_MAX_BYTES,
|
|
96
|
+
INBOUND_ATTACHMENTS_MAX_COUNT,
|
|
97
|
+
INBOUND_BODY_HTML_MAX_CHARS,
|
|
98
|
+
INBOUND_BODY_TEXT_MAX_CHARS,
|
|
99
|
+
INBOUND_MAIL_ATTACHMENTS_MAX_BYTES,
|
|
100
|
+
INBOUND_MISSING_SENDER,
|
|
101
|
+
INBOUND_SKIPPED_ATTACHMENTS_MAX_COUNT,
|
|
102
|
+
type NormalizedInboundMessage,
|
|
103
|
+
normalizeRawInboundMessage,
|
|
104
|
+
} from "./normalize-inbound-message.ts";
|
|
58
105
|
export {
|
|
59
106
|
createOAuthAccessTokenManager,
|
|
60
107
|
inboundRefreshTokenSecretOptions,
|
|
@@ -68,30 +115,57 @@ export {
|
|
|
68
115
|
type VerifyOAuthStateResult,
|
|
69
116
|
verifyOAuthState,
|
|
70
117
|
} from "./oauth-state.ts";
|
|
118
|
+
export { readableFileName } from "./outbound-attachments.ts";
|
|
71
119
|
export {
|
|
72
120
|
isMailSendNotConfiguredError,
|
|
73
121
|
isMailSendRejectedError,
|
|
74
122
|
MailSendNotConfiguredError,
|
|
75
123
|
MailSendRejectedError,
|
|
124
|
+
OUTBOUND_ATTACHMENTS_MAX_BYTES,
|
|
125
|
+
type OutboundAttachment,
|
|
76
126
|
type OutboundMailMessage,
|
|
77
127
|
type OutboundMailSent,
|
|
78
128
|
outboundMailMessageSchema,
|
|
129
|
+
type ProviderSentMail,
|
|
79
130
|
} from "./outbound-mail.ts";
|
|
80
131
|
export {
|
|
81
132
|
inboundMessagesProjectionTable,
|
|
82
133
|
mailAccountsProjectionTable,
|
|
83
134
|
mailThreadsProjectionTable,
|
|
84
135
|
} from "./projection.ts";
|
|
136
|
+
export {
|
|
137
|
+
InboundAuthError,
|
|
138
|
+
InboundCredentialNotStoredError,
|
|
139
|
+
InboundCursorInvalidError,
|
|
140
|
+
InboundRateLimitError,
|
|
141
|
+
InboundTransientError,
|
|
142
|
+
isInboundAuthError,
|
|
143
|
+
isInboundCredentialNotStoredError,
|
|
144
|
+
isInboundCursorInvalidError,
|
|
145
|
+
isInboundRateLimitError,
|
|
146
|
+
isInboundTransientError,
|
|
147
|
+
} from "./provider-errors.ts";
|
|
85
148
|
export {
|
|
86
149
|
resolveInboundProviderForAccount,
|
|
87
150
|
resolveInboundProviderForKey,
|
|
88
151
|
} from "./provider-factory.ts";
|
|
152
|
+
export {
|
|
153
|
+
QUARANTINE_REASON_CODES,
|
|
154
|
+
type QuarantineReasonCode,
|
|
155
|
+
quarantineEntity,
|
|
156
|
+
quarantineTable,
|
|
157
|
+
} from "./quarantine-entities.ts";
|
|
89
158
|
export {
|
|
90
159
|
IMAP_CONNECT_SCREEN_ID,
|
|
91
160
|
IMAP_RECONNECT_SCREEN_ID,
|
|
92
161
|
MAIL_ACCOUNT_EDIT_SCREEN_ID,
|
|
93
162
|
MAIL_ACCOUNT_LIST_SCREEN_ID,
|
|
94
163
|
MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
164
|
+
MAIL_QUARANTINE_SCREEN_ID,
|
|
165
|
+
MY_IMAP_CONNECT_SCREEN_ID,
|
|
166
|
+
MY_IMAP_RECONNECT_SCREEN_ID,
|
|
167
|
+
MY_MAIL_ACCOUNT_LIST_SCREEN_ID,
|
|
168
|
+
MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
95
169
|
} from "./screens.ts";
|
|
96
170
|
export {
|
|
97
171
|
MAIL_SEND_ERRORS,
|
|
@@ -101,26 +175,30 @@ export {
|
|
|
101
175
|
sendMailFromAccount,
|
|
102
176
|
} from "./send-from-account.ts";
|
|
103
177
|
export {
|
|
104
|
-
|
|
105
|
-
|
|
178
|
+
fromDomainOf,
|
|
179
|
+
isSenderAuthenticated,
|
|
180
|
+
normalizeTrustedAuthservId,
|
|
181
|
+
parseSenderAuthentication,
|
|
182
|
+
SENDER_AUTH_VERDICTS,
|
|
183
|
+
type SenderAuthentication,
|
|
184
|
+
type SenderAuthVerdict,
|
|
185
|
+
senderAuthenticationSchema,
|
|
186
|
+
} from "./sender-auth.ts";
|
|
187
|
+
export {
|
|
188
|
+
type InboundContentFetchResult,
|
|
106
189
|
type InboundFetchResult,
|
|
107
190
|
type InboundMailContext,
|
|
108
191
|
type InboundMailProviderPlugin,
|
|
109
|
-
type InboundOAuthFlow,
|
|
110
|
-
InboundRateLimitError,
|
|
111
|
-
InboundTransientError,
|
|
112
|
-
isInboundAuthError,
|
|
113
|
-
isInboundCursorInvalidError,
|
|
114
192
|
isInboundMailProviderPlugin,
|
|
115
|
-
isInboundRateLimitError,
|
|
116
|
-
isInboundTransientError,
|
|
117
193
|
type MailAccountRecord,
|
|
118
|
-
type
|
|
194
|
+
type RawInboundAttachment,
|
|
119
195
|
type RawInboundMessage,
|
|
196
|
+
type RejectedInboundMessage,
|
|
197
|
+
type SkippedInboundAttachment,
|
|
120
198
|
type SyncCursorPayload,
|
|
121
199
|
} from "./types.ts";
|
|
122
|
-
export {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} from "./watch-supervisor.ts";
|
|
200
|
+
export { createInboundMailSupervisor } from "./watch-supervisor.ts";
|
|
201
|
+
export type {
|
|
202
|
+
InboundMailSupervisor,
|
|
203
|
+
InboundMailSupervisorDeps,
|
|
204
|
+
} from "./watch-supervisor-types.ts";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Failure counter of mails the ingest could not take. Persisted, so every worker replica and every
|
|
2
|
+
// restart sees the same count; a poison mail therefore reaches quarantine instead of blocking the
|
|
3
|
+
// cursor of its mailbox forever. Rows carry ids only, no mail content.
|
|
4
|
+
|
|
5
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
6
|
+
import {
|
|
7
|
+
deleteMany,
|
|
8
|
+
deriveEntityTableMeta,
|
|
9
|
+
fetchOne,
|
|
10
|
+
insertOnConflictDoNothing,
|
|
11
|
+
updateMany,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
13
|
+
import {
|
|
14
|
+
access,
|
|
15
|
+
createEntity,
|
|
16
|
+
createNumberField,
|
|
17
|
+
createTextField,
|
|
18
|
+
createTimestampField,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
21
|
+
|
|
22
|
+
export const MAX_COUNTED_INGEST_FAILURES = 3;
|
|
23
|
+
const COMPARE_AND_SET_ATTEMPTS = 3;
|
|
24
|
+
|
|
25
|
+
const technicalReference = {
|
|
26
|
+
access: { write: access.privileged },
|
|
27
|
+
personal: false,
|
|
28
|
+
reason: "technical_reference",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const ingestFailureEntity = createEntity({
|
|
32
|
+
table: "store_mail_ingest_failures",
|
|
33
|
+
softDelete: false,
|
|
34
|
+
indexes: [{ unique: true, columns: ["tenantId", "accountId", "providerMessageId"] }],
|
|
35
|
+
fields: {
|
|
36
|
+
accountId: createTextField({ required: true, maxLength: 36, ...technicalReference }),
|
|
37
|
+
providerMessageId: createTextField({ required: true, maxLength: 500, ...technicalReference }),
|
|
38
|
+
failureCount: createNumberField({ required: true, integer: true, default: 0 }),
|
|
39
|
+
// Double precision: epoch milliseconds divided by a short poll interval overflow a 32-bit integer.
|
|
40
|
+
lastCountedBucket: createNumberField({ required: true, default: 0 }),
|
|
41
|
+
lastFailedAt: createTimestampField({ required: true, access: { write: access.privileged } }),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const ingestFailureTable = deriveEntityTableMeta(
|
|
46
|
+
"mail-ingest-failure",
|
|
47
|
+
ingestFailureEntity,
|
|
48
|
+
{ source: "unmanaged" },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export type IngestFailureTarget = {
|
|
52
|
+
readonly tenantId: string;
|
|
53
|
+
readonly accountId: string;
|
|
54
|
+
readonly providerMessageId: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type CounterRow = { readonly failureCount: number };
|
|
58
|
+
|
|
59
|
+
/** Bucket of one poll interval: replicas and overlapping polls inside the same interval count a mail once. */
|
|
60
|
+
export function countBucketOf(now: Temporal.Instant, pollIntervalMs: number): number {
|
|
61
|
+
return Math.floor(now.epochMilliseconds / pollIntervalMs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function currentCountBucket(
|
|
65
|
+
now: (() => Temporal.Instant) | undefined,
|
|
66
|
+
pollIntervalMs: number,
|
|
67
|
+
): number {
|
|
68
|
+
return countBucketOf((now ?? Temporal.Now.instant)(), pollIntervalMs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The new failure count, or null when this bucket was already counted (or another worker won the race). */
|
|
72
|
+
export async function countIngestFailure(
|
|
73
|
+
db: DbConnection,
|
|
74
|
+
target: IngestFailureTarget,
|
|
75
|
+
bucket: number,
|
|
76
|
+
): Promise<number | null> {
|
|
77
|
+
const key = { accountId: target.accountId, providerMessageId: target.providerMessageId };
|
|
78
|
+
const lastFailedAt = Temporal.Now.instant().toString();
|
|
79
|
+
const inserted = await insertOnConflictDoNothing(
|
|
80
|
+
db,
|
|
81
|
+
ingestFailureTable,
|
|
82
|
+
{
|
|
83
|
+
id: crypto.randomUUID(),
|
|
84
|
+
tenantId: target.tenantId,
|
|
85
|
+
...key,
|
|
86
|
+
failureCount: 1,
|
|
87
|
+
lastCountedBucket: bucket,
|
|
88
|
+
lastFailedAt,
|
|
89
|
+
},
|
|
90
|
+
{ conflictKeys: ["tenantId", "accountId", "providerMessageId"] },
|
|
91
|
+
);
|
|
92
|
+
if (inserted) return 1;
|
|
93
|
+
for (let attempt = 0; attempt < COMPARE_AND_SET_ATTEMPTS; attempt++) {
|
|
94
|
+
const row = await fetchOne<CounterRow>(db, ingestFailureTable, key);
|
|
95
|
+
if (!row) return null;
|
|
96
|
+
const next = row.failureCount + 1;
|
|
97
|
+
const updated = await updateMany(
|
|
98
|
+
db,
|
|
99
|
+
ingestFailureTable,
|
|
100
|
+
{ failureCount: next, lastCountedBucket: bucket, lastFailedAt },
|
|
101
|
+
{ ...key, failureCount: row.failureCount, lastCountedBucket: { lt: bucket } },
|
|
102
|
+
);
|
|
103
|
+
if (updated.length > 0) return next;
|
|
104
|
+
const current = await fetchOne<CounterRow & { readonly lastCountedBucket: number }>(
|
|
105
|
+
db,
|
|
106
|
+
ingestFailureTable,
|
|
107
|
+
key,
|
|
108
|
+
);
|
|
109
|
+
// skip: this bucket is counted already, a second count would double the failure of one interval
|
|
110
|
+
if (!current || current.lastCountedBucket >= bucket) return null;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function hasCountedIngestFailure(
|
|
116
|
+
db: DbConnection,
|
|
117
|
+
target: IngestFailureTarget,
|
|
118
|
+
): Promise<boolean> {
|
|
119
|
+
const row = await fetchOne<CounterRow>(db, ingestFailureTable, {
|
|
120
|
+
accountId: target.accountId,
|
|
121
|
+
providerMessageId: target.providerMessageId,
|
|
122
|
+
});
|
|
123
|
+
return row !== undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function clearIngestFailures(
|
|
127
|
+
db: DbConnection,
|
|
128
|
+
accountId: string,
|
|
129
|
+
providerMessageIds: readonly string[],
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
// skip: nothing to clear
|
|
132
|
+
if (providerMessageIds.length === 0) return;
|
|
133
|
+
await deleteMany(db, ingestFailureTable, {
|
|
134
|
+
accountId,
|
|
135
|
+
providerMessageId: { in: [...providerMessageIds] },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The ingest-message payload built from a provider message. Shared by the supervisor's regular
|
|
2
|
+
// ingest, the header import of quarantined mails and the quarantine retry.
|
|
3
|
+
|
|
4
|
+
import type { NormalizedInboundMessage } from "./normalize-inbound-message.ts";
|
|
5
|
+
import type { MailAccountRecord } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export function ingestPayloadOf(
|
|
8
|
+
account: MailAccountRecord,
|
|
9
|
+
msg: NormalizedInboundMessage,
|
|
10
|
+
options: {
|
|
11
|
+
readonly providerCursor: string;
|
|
12
|
+
readonly bodyRef: string;
|
|
13
|
+
readonly contentUnavailableReason?: "quarantined";
|
|
14
|
+
},
|
|
15
|
+
): Record<string, unknown> {
|
|
16
|
+
return {
|
|
17
|
+
accountId: account.id,
|
|
18
|
+
ownerUserId: account.ownerUserId,
|
|
19
|
+
providerName: account.provider,
|
|
20
|
+
providerMessageId: msg.providerMessageId,
|
|
21
|
+
messageIdHeader: msg.messageIdHeader,
|
|
22
|
+
providerThreadId: msg.providerThreadId,
|
|
23
|
+
references: msg.references,
|
|
24
|
+
from: msg.from,
|
|
25
|
+
to: msg.to,
|
|
26
|
+
cc: msg.cc,
|
|
27
|
+
subject: msg.subject,
|
|
28
|
+
snippet: msg.snippet,
|
|
29
|
+
receivedAtIso: msg.receivedAtIso,
|
|
30
|
+
bodyRef: options.bodyRef,
|
|
31
|
+
scope: msg.scope,
|
|
32
|
+
providerCursor: options.providerCursor,
|
|
33
|
+
bodyText: msg.bodyText,
|
|
34
|
+
bodyTextTruncated: msg.bodyTextTruncated,
|
|
35
|
+
...(msg.rawMime != null && { rawMime: msg.rawMime }),
|
|
36
|
+
...(msg.bodyHtml !== null && { bodyHtml: msg.bodyHtml }),
|
|
37
|
+
attachments: msg.attachments,
|
|
38
|
+
skippedAttachments: msg.skippedAttachments,
|
|
39
|
+
...(msg.senderAuthentication !== undefined && {
|
|
40
|
+
senderAuthentication: msg.senderAuthentication,
|
|
41
|
+
}),
|
|
42
|
+
...(options.contentUnavailableReason !== undefined && {
|
|
43
|
+
contentUnavailableReason: options.contentUnavailableReason,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
2
|
+
import type { FeatureDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
+
import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailFoundationHandlers } from "./constants.ts";
|
|
5
|
+
|
|
6
|
+
export type InboundMailRoleOptions = {
|
|
7
|
+
/** Roles that use their own and the shared mailboxes. Admins are always members too. */
|
|
8
|
+
readonly userRoles?: readonly string[];
|
|
9
|
+
/** Roles that administer shared mailboxes and see the metadata of personal ones. */
|
|
10
|
+
readonly adminRoles?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type InboundMailRoles = {
|
|
14
|
+
readonly userRoles: readonly string[];
|
|
15
|
+
readonly adminRoles: readonly string[];
|
|
16
|
+
/** admin ∪ user, deduplicated: everyone who may touch the mail feature at all. */
|
|
17
|
+
readonly memberRoles: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_INBOUND_MAIL_USER_ROLES = ["User"] as const;
|
|
21
|
+
export const DEFAULT_INBOUND_MAIL_ADMIN_ROLES = [ROLES.TenantAdmin, ROLES.SystemAdmin] as const;
|
|
22
|
+
|
|
23
|
+
function validatedRoles(optionName: string, roles: readonly string[]): readonly string[] {
|
|
24
|
+
if (roles.length === 0) {
|
|
25
|
+
throw new Error(`createInboundMailFoundationFeature: ${optionName} must not be empty.`);
|
|
26
|
+
}
|
|
27
|
+
if (roles.some((role) => role.trim() === "")) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`createInboundMailFoundationFeature: ${optionName} must not contain empty role names.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (roles.includes("anonymous")) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`createInboundMailFoundationFeature: ${optionName} must not contain "anonymous": mail is only readable by members of the tenant.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return [...roles];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handlers the supervisor, the account health, the OAuth callback and the refresh job dispatch as the
|
|
41
|
+
// system user (SystemAdmin). Every new system dispatch must target one of these; a test scans the
|
|
42
|
+
// sources for it.
|
|
43
|
+
export const SYSTEM_DISPATCHED_MAIL_HANDLERS: readonly string[] = [
|
|
44
|
+
InboundMailFoundationHandlers.ingestMessage,
|
|
45
|
+
InboundMailFoundationHandlers.connectAccount,
|
|
46
|
+
InboundMailFoundationHandlers.updateAccount,
|
|
47
|
+
InboundMailFoundationHandlers.reactivateAccount,
|
|
48
|
+
InboundMailFoundationHandlers.markCredentialStored,
|
|
49
|
+
InboundMailFoundationHandlers.attachMessageContent,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// An app whose adminRoles/memberRoles leave SystemAdmin out would see every system dispatch refused.
|
|
53
|
+
// Only handler access lists use this; isMailAdmin and screens keep the configured roles.
|
|
54
|
+
export function accessRolesOf(handlerQn: string, roles: readonly string[]): string[] {
|
|
55
|
+
return SYSTEM_DISPATCHED_MAIL_HANDLERS.includes(handlerQn)
|
|
56
|
+
? [...new Set([...roles, ROLES.SystemAdmin])]
|
|
57
|
+
: [...roles];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveInboundMailRoles(options: InboundMailRoleOptions = {}): InboundMailRoles {
|
|
61
|
+
const userRoles = validatedRoles(
|
|
62
|
+
"userRoles",
|
|
63
|
+
options.userRoles ?? DEFAULT_INBOUND_MAIL_USER_ROLES,
|
|
64
|
+
);
|
|
65
|
+
const adminRoles = validatedRoles(
|
|
66
|
+
"adminRoles",
|
|
67
|
+
options.adminRoles ?? DEFAULT_INBOUND_MAIL_ADMIN_ROLES,
|
|
68
|
+
);
|
|
69
|
+
return { userRoles, adminRoles, memberRoles: [...new Set([...adminRoles, ...userRoles])] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// dedupeOptions accept primitives only: equal role sets collapse, differing ones fail at boot.
|
|
73
|
+
export function roleDedupeOptions(roles: InboundMailRoles): Readonly<Record<string, string>> {
|
|
74
|
+
return {
|
|
75
|
+
userRoles: [...roles.userRoles].sort().join(","),
|
|
76
|
+
adminRoles: [...roles.adminRoles].sort().join(","),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
81
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isInboundMailRoles(value: unknown): value is InboundMailRoles {
|
|
85
|
+
return (
|
|
86
|
+
isPlainObject(value) &&
|
|
87
|
+
isStringArray(value["userRoles"]) &&
|
|
88
|
+
isStringArray(value["adminRoles"]) &&
|
|
89
|
+
isStringArray(value["memberRoles"])
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function inboundMailRolesOfFeature(feature: FeatureDefinition): InboundMailRoles | undefined {
|
|
94
|
+
const exported = feature.exports;
|
|
95
|
+
return isPlainObject(exported) && isInboundMailRoles(exported["roles"])
|
|
96
|
+
? exported["roles"]
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readInboundMailRoles(registry: Registry): InboundMailRoles {
|
|
101
|
+
const feature = registry.features.get(INBOUND_MAIL_FOUNDATION_FEATURE);
|
|
102
|
+
return (feature && inboundMailRolesOfFeature(feature)) ?? resolveInboundMailRoles();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sameRoleSet(a: readonly string[], b: readonly string[]): boolean {
|
|
106
|
+
return a.length === b.length && a.every((role) => b.includes(role));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Provider features register forms whose dispatches the foundation's handlers answer, so roles
|
|
110
|
+
// that differ from the foundation's would only surface as 403s at runtime.
|
|
111
|
+
export function assertRolesMatchFoundation(
|
|
112
|
+
featureName: string,
|
|
113
|
+
roles: InboundMailRoles,
|
|
114
|
+
features: readonly FeatureDefinition[],
|
|
115
|
+
): void {
|
|
116
|
+
const foundation = features.find((feature) => feature.name === INBOUND_MAIL_FOUNDATION_FEATURE);
|
|
117
|
+
const foundationRoles = foundation && inboundMailRolesOfFeature(foundation);
|
|
118
|
+
if (
|
|
119
|
+
foundationRoles !== undefined &&
|
|
120
|
+
!(
|
|
121
|
+
sameRoleSet(foundationRoles.userRoles, roles.userRoles) &&
|
|
122
|
+
sameRoleSet(foundationRoles.adminRoles, roles.adminRoles)
|
|
123
|
+
)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${featureName}: role options differ from ${INBOUND_MAIL_FOUNDATION_FEATURE} ` +
|
|
127
|
+
`(userRoles [${roles.userRoles.join(", ")}] vs [${foundationRoles.userRoles.join(", ")}], ` +
|
|
128
|
+
`adminRoles [${roles.adminRoles.join(", ")}] vs [${foundationRoles.adminRoles.join(", ")}]). ` +
|
|
129
|
+
`Pass the same userRoles/adminRoles to ${featureName} and ${INBOUND_MAIL_FOUNDATION_FEATURE}.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|