@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,125 +1,219 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IMAP_CONNECT_SCREEN_ID,
|
|
3
3
|
IMAP_RECONNECT_SCREEN_ID,
|
|
4
|
+
type InboundMailRoles,
|
|
4
5
|
MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
6
|
+
MY_IMAP_CONNECT_SCREEN_ID,
|
|
7
|
+
MY_IMAP_RECONNECT_SCREEN_ID,
|
|
8
|
+
MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
5
9
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
6
10
|
import { i18nKey } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
11
|
import type { ActionFormScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
8
12
|
import { ImapHandlers, ImapQueries, SMTP_SUBMISSION_PORT } from "./constants.ts";
|
|
9
13
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
required: true,
|
|
37
|
-
min: 1,
|
|
38
|
-
max: 65535,
|
|
39
|
-
default: SMTP_SUBMISSION_PORT,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
layout: {
|
|
43
|
-
sections: [
|
|
44
|
-
// The name-based icon guess reads "address" as a postal address.
|
|
45
|
-
{ fields: ["displayName", { field: "address", icon: "mail" }] },
|
|
46
|
-
{
|
|
47
|
-
title: i18nKey("inboundMail.imap.connect.server"),
|
|
48
|
-
fields: ["host", "port", "user", "password"],
|
|
14
|
+
// The admin form connects a shared mailbox, the member form a personal one. The member form carries
|
|
15
|
+
// no sender-check field because that policy is tenant-wide and only admins set it.
|
|
16
|
+
function createConnectScreen(variant: {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly handler: string;
|
|
19
|
+
readonly roles: readonly string[];
|
|
20
|
+
readonly returnTarget: string;
|
|
21
|
+
readonly withSenderCheck: boolean;
|
|
22
|
+
}): ActionFormScreenDefinition {
|
|
23
|
+
return {
|
|
24
|
+
id: variant.id,
|
|
25
|
+
type: "actionForm",
|
|
26
|
+
agent: { expose: false },
|
|
27
|
+
handler: variant.handler,
|
|
28
|
+
fields: {
|
|
29
|
+
displayName: { type: "text", required: true, maxLength: 200 },
|
|
30
|
+
address: { type: "text", required: true, maxLength: 320, format: "email" },
|
|
31
|
+
host: { type: "text", required: true, maxLength: 255 },
|
|
32
|
+
port: {
|
|
33
|
+
type: "number",
|
|
34
|
+
integer: true,
|
|
35
|
+
grouping: false,
|
|
36
|
+
required: true,
|
|
37
|
+
min: 1,
|
|
38
|
+
max: 65535,
|
|
39
|
+
default: 993,
|
|
49
40
|
},
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
user: { type: "text", required: true, maxLength: 320 },
|
|
42
|
+
password: { type: "text", required: true, format: "password", sensitive: true },
|
|
43
|
+
smtpHost: { type: "text", maxLength: 255 },
|
|
44
|
+
smtpPort: {
|
|
45
|
+
type: "number",
|
|
46
|
+
integer: true,
|
|
47
|
+
grouping: false,
|
|
48
|
+
required: true,
|
|
49
|
+
min: 1,
|
|
50
|
+
max: 65535,
|
|
51
|
+
default: SMTP_SUBMISSION_PORT,
|
|
54
52
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
...(variant.withSenderCheck && { trustedAuthservId: { type: "text", maxLength: 255 } }),
|
|
54
|
+
},
|
|
55
|
+
layout: {
|
|
56
|
+
sections: [
|
|
57
|
+
// The name-based icon guess reads "address" as a postal address.
|
|
58
|
+
{ fields: ["displayName", { field: "address", icon: "mail" }] },
|
|
59
|
+
{
|
|
60
|
+
title: i18nKey("inboundMail.imap.connect.server"),
|
|
61
|
+
fields: ["host", "port", "user", "password"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: i18nKey("inboundMail.imap.connect.outgoing"),
|
|
65
|
+
description: i18nKey("inboundMail.imap.connect.outgoingHint"),
|
|
66
|
+
fields: ["smtpHost", "smtpPort"],
|
|
67
|
+
},
|
|
68
|
+
...(variant.withSenderCheck
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
title: i18nKey("inboundMail.imap.connect.senderCheck"),
|
|
72
|
+
description: i18nKey("inboundMail.imap.connect.senderCheckHint"),
|
|
73
|
+
fields: ["trustedAuthservId"],
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
submitLabel: i18nKey("inboundMail.imap.connect.submit"),
|
|
80
|
+
redirect: variant.returnTarget,
|
|
81
|
+
cancelTarget: variant.returnTarget,
|
|
82
|
+
access: { roles: [...variant.roles] },
|
|
83
|
+
dormant: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Reached from the toolbar of the foundation's mail account list, never from the nav.
|
|
88
|
+
export const createImapConnectScreen = ({
|
|
89
|
+
adminRoles,
|
|
90
|
+
}: InboundMailRoles): ActionFormScreenDefinition =>
|
|
91
|
+
createConnectScreen({
|
|
92
|
+
id: IMAP_CONNECT_SCREEN_ID,
|
|
93
|
+
handler: ImapHandlers.connectMailbox,
|
|
94
|
+
roles: adminRoles,
|
|
95
|
+
returnTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
96
|
+
withSenderCheck: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Reached from the toolbar of "My mail accounts".
|
|
100
|
+
export const createMyImapConnectScreen = ({
|
|
101
|
+
memberRoles,
|
|
102
|
+
}: InboundMailRoles): ActionFormScreenDefinition =>
|
|
103
|
+
createConnectScreen({
|
|
104
|
+
id: MY_IMAP_CONNECT_SCREEN_ID,
|
|
105
|
+
handler: ImapHandlers.connectMyMailbox,
|
|
106
|
+
roles: memberRoles,
|
|
107
|
+
returnTarget: MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
108
|
+
withSenderCheck: false,
|
|
109
|
+
});
|
|
63
110
|
|
|
64
111
|
// Reached from the "Reconnect" row action of an account with a sign-in error. The record id comes
|
|
65
112
|
// from the path; the load query returns the stored server settings, never the password.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
required: true,
|
|
93
|
-
min: 1,
|
|
94
|
-
max: 65535,
|
|
95
|
-
default: SMTP_SUBMISSION_PORT,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
layout: {
|
|
99
|
-
sections: [
|
|
100
|
-
{
|
|
101
|
-
title: i18nKey("inboundMail.imap.connect.server"),
|
|
102
|
-
description: i18nKey("inboundMail.imap.reconnect.hint"),
|
|
103
|
-
// The id only travels in the payload; the admin recognises the mailbox by its name.
|
|
104
|
-
fields: [
|
|
105
|
-
{ field: "accountId", visible: false },
|
|
106
|
-
{ field: "displayName", readOnly: true, submit: false },
|
|
107
|
-
"host",
|
|
108
|
-
"port",
|
|
109
|
-
"user",
|
|
110
|
-
"password",
|
|
111
|
-
],
|
|
113
|
+
// The admin and the owner variant share this form; the owner's carries no sender-check field
|
|
114
|
+
// because that policy is tenant-wide and only admins set it.
|
|
115
|
+
function createReconnectScreen(variant: {
|
|
116
|
+
readonly id: string;
|
|
117
|
+
readonly roles: readonly string[];
|
|
118
|
+
readonly returnTarget: string;
|
|
119
|
+
readonly withSenderCheck: boolean;
|
|
120
|
+
}): ActionFormScreenDefinition {
|
|
121
|
+
return {
|
|
122
|
+
id: variant.id,
|
|
123
|
+
type: "actionForm",
|
|
124
|
+
agent: { expose: false },
|
|
125
|
+
handler: ImapHandlers.reconnectMailbox,
|
|
126
|
+
load: { query: ImapQueries.reconnectPrefill, idParam: "accountId" },
|
|
127
|
+
fields: {
|
|
128
|
+
accountId: { type: "text", required: true },
|
|
129
|
+
displayName: { type: "text", maxLength: 200 },
|
|
130
|
+
host: { type: "text", required: true, maxLength: 255 },
|
|
131
|
+
port: {
|
|
132
|
+
type: "number",
|
|
133
|
+
integer: true,
|
|
134
|
+
grouping: false,
|
|
135
|
+
required: true,
|
|
136
|
+
min: 1,
|
|
137
|
+
max: 65535,
|
|
138
|
+
default: 993,
|
|
112
139
|
},
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
user: { type: "text", required: true, maxLength: 320 },
|
|
141
|
+
password: { type: "text", required: true, format: "password", sensitive: true },
|
|
142
|
+
smtpHost: { type: "text", maxLength: 255 },
|
|
143
|
+
smtpPort: {
|
|
144
|
+
type: "number",
|
|
145
|
+
integer: true,
|
|
146
|
+
grouping: false,
|
|
147
|
+
required: true,
|
|
148
|
+
min: 1,
|
|
149
|
+
max: 65535,
|
|
150
|
+
default: SMTP_SUBMISSION_PORT,
|
|
117
151
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
...(variant.withSenderCheck && { trustedAuthservId: { type: "text", maxLength: 255 } }),
|
|
153
|
+
},
|
|
154
|
+
layout: {
|
|
155
|
+
sections: [
|
|
156
|
+
{
|
|
157
|
+
title: i18nKey("inboundMail.imap.connect.server"),
|
|
158
|
+
description: i18nKey("inboundMail.imap.reconnect.hint"),
|
|
159
|
+
// The id only travels in the payload; the user recognises the mailbox by its name.
|
|
160
|
+
fields: [
|
|
161
|
+
{ field: "accountId", visible: false },
|
|
162
|
+
{ field: "displayName", readOnly: true, submit: false },
|
|
163
|
+
"host",
|
|
164
|
+
"port",
|
|
165
|
+
"user",
|
|
166
|
+
"password",
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
title: i18nKey("inboundMail.imap.connect.outgoing"),
|
|
171
|
+
description: i18nKey("inboundMail.imap.connect.outgoingHint"),
|
|
172
|
+
fields: ["smtpHost", "smtpPort"],
|
|
173
|
+
},
|
|
174
|
+
...(variant.withSenderCheck
|
|
175
|
+
? [
|
|
176
|
+
{
|
|
177
|
+
title: i18nKey("inboundMail.imap.connect.senderCheck"),
|
|
178
|
+
description: i18nKey("inboundMail.imap.connect.senderCheckHint"),
|
|
179
|
+
fields: ["trustedAuthservId"],
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
submitLabel: i18nKey("inboundMail.imap.reconnect.submit"),
|
|
186
|
+
redirect: variant.returnTarget,
|
|
187
|
+
cancelTarget: variant.returnTarget,
|
|
188
|
+
access: { roles: [...variant.roles] },
|
|
189
|
+
dormant: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const createImapReconnectScreen = ({
|
|
194
|
+
adminRoles,
|
|
195
|
+
}: InboundMailRoles): ActionFormScreenDefinition =>
|
|
196
|
+
createReconnectScreen({
|
|
197
|
+
id: IMAP_RECONNECT_SCREEN_ID,
|
|
198
|
+
roles: adminRoles,
|
|
199
|
+
returnTarget: MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
200
|
+
withSenderCheck: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Reached from the "Reactivate" row action of "My mail accounts".
|
|
204
|
+
export const createMyImapReconnectScreen = ({
|
|
205
|
+
memberRoles,
|
|
206
|
+
}: InboundMailRoles): ActionFormScreenDefinition =>
|
|
207
|
+
createReconnectScreen({
|
|
208
|
+
id: MY_IMAP_RECONNECT_SCREEN_ID,
|
|
209
|
+
roles: memberRoles,
|
|
210
|
+
returnTarget: MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
211
|
+
withSenderCheck: false,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export const createImapScreens = (roles: InboundMailRoles): ActionFormScreenDefinition[] => [
|
|
215
|
+
createImapConnectScreen(roles),
|
|
216
|
+
createImapReconnectScreen(roles),
|
|
217
|
+
createMyImapConnectScreen(roles),
|
|
218
|
+
createMyImapReconnectScreen(roles),
|
|
219
|
+
];
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
type MailAccountRecord,
|
|
10
10
|
MailSendNotConfiguredError,
|
|
11
11
|
MailSendRejectedError,
|
|
12
|
+
type OutboundAttachment,
|
|
12
13
|
type OutboundMailMessage,
|
|
13
|
-
type
|
|
14
|
+
type ProviderSentMail,
|
|
14
15
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
15
16
|
import MailComposer from "nodemailer/lib/mail-composer";
|
|
16
17
|
import { FEATURE_NAME } from "./constants.ts";
|
|
@@ -37,7 +38,8 @@ export async function sendFromImapAccount(
|
|
|
37
38
|
ctx: InboundMailContext,
|
|
38
39
|
account: MailAccountRecord,
|
|
39
40
|
message: OutboundMailMessage,
|
|
40
|
-
|
|
41
|
+
attachments: readonly OutboundAttachment[],
|
|
42
|
+
): Promise<ProviderSentMail> {
|
|
41
43
|
const doc = await readCredentialDocument(ctx, account);
|
|
42
44
|
if (!doc.smtpHost || !doc.password) {
|
|
43
45
|
throw new MailSendNotConfiguredError(
|
|
@@ -55,6 +57,13 @@ export async function sendFromImapAccount(
|
|
|
55
57
|
...(message.html !== undefined && { html: message.html }),
|
|
56
58
|
...(message.inReplyTo && { inReplyTo: message.inReplyTo }),
|
|
57
59
|
...(message.references && { references: [...message.references] }),
|
|
60
|
+
...(attachments.length > 0 && {
|
|
61
|
+
attachments: attachments.map((attachment) => ({
|
|
62
|
+
filename: attachment.fileName,
|
|
63
|
+
contentType: attachment.mimeType,
|
|
64
|
+
content: Buffer.from(attachment.data),
|
|
65
|
+
})),
|
|
66
|
+
}),
|
|
58
67
|
}).compile();
|
|
59
68
|
const raw = await mime.build();
|
|
60
69
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromDomainOf,
|
|
3
|
+
parseSenderAuthentication,
|
|
4
|
+
type SenderAuthentication,
|
|
5
|
+
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
6
|
+
import type { ParsedMail } from "mailparser";
|
|
7
|
+
|
|
8
|
+
const HEADER_NAME = "authentication-results";
|
|
9
|
+
|
|
10
|
+
// headerLines keeps every occurrence in file order, topmost first. Unlike `headers`, which merges
|
|
11
|
+
// repeated headers, the order is what the trust rule depends on.
|
|
12
|
+
function authenticationResultValues(parsed: ParsedMail): string[] {
|
|
13
|
+
return parsed.headerLines
|
|
14
|
+
.filter((header) => header.key === HEADER_NAME)
|
|
15
|
+
.map((header) => header.line.slice(header.line.indexOf(":") + 1));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A From header with addresses of several domains cannot be bound to one DMARC verdict.
|
|
19
|
+
function singleFromDomain(parsed: ParsedMail): string | null {
|
|
20
|
+
const domains = new Set(
|
|
21
|
+
(parsed.from?.value ?? []).map((address) => fromDomainOf(address.address ?? "")),
|
|
22
|
+
);
|
|
23
|
+
const [only] = domains;
|
|
24
|
+
return domains.size === 1 && only ? only : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function senderAuthenticationOf(
|
|
28
|
+
parsed: ParsedMail,
|
|
29
|
+
trustedAuthservId: string | null,
|
|
30
|
+
): SenderAuthentication | null {
|
|
31
|
+
return parseSenderAuthentication(
|
|
32
|
+
authenticationResultValues(parsed),
|
|
33
|
+
trustedAuthservId,
|
|
34
|
+
singleFromDomain(parsed),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InboundMailContext,
|
|
3
|
+
MailAccountRecord,
|
|
4
|
+
RawInboundMessage,
|
|
5
|
+
RejectedInboundMessage,
|
|
6
|
+
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
7
|
+
import type { ImapFlow } from "imapflow";
|
|
8
|
+
import { readCredentialDocument } from "./credential-document.ts";
|
|
9
|
+
import { createImapClient, IMAP_MAILBOX, imapMailHostGuard, mapImapError } from "./imap-client.ts";
|
|
10
|
+
import { coerceDate, parseOrReject } from "./parse-message.ts";
|
|
11
|
+
|
|
12
|
+
// IMAP IDLE (imapflow idles automatically while the mailbox is open
|
|
13
|
+
// and emits 'exists' for new messages).
|
|
14
|
+
|
|
15
|
+
export async function watchMailbox(
|
|
16
|
+
ctx: InboundMailContext,
|
|
17
|
+
account: MailAccountRecord,
|
|
18
|
+
handlers: {
|
|
19
|
+
readonly onMessages: (
|
|
20
|
+
msgs: readonly RawInboundMessage[],
|
|
21
|
+
rejected?: readonly RejectedInboundMessage[],
|
|
22
|
+
) => Promise<void>;
|
|
23
|
+
readonly onError: (err: unknown) => void;
|
|
24
|
+
},
|
|
25
|
+
): Promise<() => Promise<void>> {
|
|
26
|
+
const doc = await readCredentialDocument(ctx, account);
|
|
27
|
+
const client: ImapFlow = await createImapClient(doc, imapMailHostGuard());
|
|
28
|
+
let stopped = false;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await client.connect();
|
|
32
|
+
const mailbox = await client.mailboxOpen(IMAP_MAILBOX);
|
|
33
|
+
const uidValidity = String(mailbox.uidValidity);
|
|
34
|
+
// uidNext is the next UID to be assigned, so everything from here is
|
|
35
|
+
// "new".
|
|
36
|
+
let nextUid = Number(mailbox.uidNext ?? 1);
|
|
37
|
+
|
|
38
|
+
const drainNew = async () => {
|
|
39
|
+
const messages: RawInboundMessage[] = [];
|
|
40
|
+
const rejected: RejectedInboundMessage[] = [];
|
|
41
|
+
let maxSeen = nextUid - 1;
|
|
42
|
+
for await (const msg of client.fetch(
|
|
43
|
+
`${nextUid}:*`,
|
|
44
|
+
{ uid: true, internalDate: true, source: true, envelope: true },
|
|
45
|
+
{ uid: true },
|
|
46
|
+
)) {
|
|
47
|
+
// `N:*` gotcha (see fetch): filter out the previous last message.
|
|
48
|
+
if (msg.uid < nextUid || !msg.source) continue;
|
|
49
|
+
const parsed = await parseOrReject({
|
|
50
|
+
source: msg.source,
|
|
51
|
+
envelope: msg.envelope,
|
|
52
|
+
uid: msg.uid,
|
|
53
|
+
uidValidity,
|
|
54
|
+
internalDate: coerceDate(msg.internalDate ?? undefined),
|
|
55
|
+
trustedAuthservId: account.trustedAuthservId ?? null,
|
|
56
|
+
});
|
|
57
|
+
if ("message" in parsed) messages.push(parsed.message);
|
|
58
|
+
else rejected.push(parsed.rejected);
|
|
59
|
+
if (msg.uid > maxSeen) maxSeen = msg.uid;
|
|
60
|
+
}
|
|
61
|
+
nextUid = maxSeen + 1;
|
|
62
|
+
if (messages.length > 0 || rejected.length > 0) await handlers.onMessages(messages, rejected);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
client.on("exists", () => {
|
|
66
|
+
// skip: ignore delayed exists events after the watcher has stopped.
|
|
67
|
+
if (stopped) return;
|
|
68
|
+
void drainNew().catch((err) => {
|
|
69
|
+
if (!stopped) handlers.onError(mapImapError(err, doc.host));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const die = (err: unknown) => {
|
|
73
|
+
// skip: already stopped or terminated; fire onError only once.
|
|
74
|
+
if (stopped) return;
|
|
75
|
+
stopped = true;
|
|
76
|
+
handlers.onError(mapImapError(err, doc.host));
|
|
77
|
+
};
|
|
78
|
+
client.on("error", die);
|
|
79
|
+
client.on("close", () => die(new Error("connection closed")));
|
|
80
|
+
|
|
81
|
+
// IDLE loop: 'exists' fires ONLY while client.idle() is running
|
|
82
|
+
// (verified empirically against greenmail). Each intervening command
|
|
83
|
+
// (drainNew-fetch) ends idle(), so the loop re-enters idle() until
|
|
84
|
+
// stop() closes the connection.
|
|
85
|
+
void (async () => {
|
|
86
|
+
while (!stopped) {
|
|
87
|
+
try {
|
|
88
|
+
await client.idle();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (!stopped) die(err);
|
|
91
|
+
// skip: the idle loop ends; die() has informed the supervisor (or stop()
|
|
92
|
+
// was the cause).
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
return async () => {
|
|
99
|
+
stopped = true;
|
|
100
|
+
client.removeAllListeners("exists");
|
|
101
|
+
client.removeAllListeners("error");
|
|
102
|
+
client.removeAllListeners("close");
|
|
103
|
+
await client.logout().catch(() => {});
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
await client.logout().catch(() => {});
|
|
107
|
+
throw mapImapError(err, doc.host);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
InboundAuthError,
|
|
16
|
+
type InboundContentFetchResult,
|
|
16
17
|
type InboundFetchResult,
|
|
17
18
|
type InboundMailContext,
|
|
18
19
|
type InboundMailProviderPlugin,
|
|
19
20
|
type MailAccountRecord,
|
|
20
21
|
type RawInboundMessage,
|
|
22
|
+
type RejectedInboundMessage,
|
|
21
23
|
type SyncCursorPayload,
|
|
22
24
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
23
25
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
@@ -36,11 +38,21 @@ type WatchHandlers = {
|
|
|
36
38
|
readonly onError: (err: unknown) => void;
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
type LogEntry =
|
|
42
|
+
| { readonly message: RawInboundMessage }
|
|
43
|
+
| { readonly rejected: RejectedInboundMessage };
|
|
44
|
+
|
|
39
45
|
type AccountState = {
|
|
40
|
-
readonly log:
|
|
46
|
+
readonly log: LogEntry[];
|
|
47
|
+
// What fetchContent serves; a message that is not in here counts as gone from the server.
|
|
48
|
+
readonly fullContent: Map<string, RawInboundMessage>;
|
|
49
|
+
readonly contentFetches: string[][];
|
|
41
50
|
watch: WatchHandlers | null;
|
|
51
|
+
watchedTrustedAuthservId: string | null | undefined;
|
|
42
52
|
nextFetchError: unknown;
|
|
43
53
|
nextVerifyError: unknown;
|
|
54
|
+
hangingFetch: Promise<void> | null;
|
|
55
|
+
lastFetchSignal: AbortSignal | undefined;
|
|
44
56
|
credentialSecretKey: string | null;
|
|
45
57
|
};
|
|
46
58
|
|
|
@@ -51,9 +63,14 @@ function stateFor(accountId: string): AccountState {
|
|
|
51
63
|
if (!state) {
|
|
52
64
|
state = {
|
|
53
65
|
log: [],
|
|
66
|
+
fullContent: new Map(),
|
|
67
|
+
contentFetches: [],
|
|
54
68
|
watch: null,
|
|
69
|
+
watchedTrustedAuthservId: undefined,
|
|
55
70
|
nextFetchError: null,
|
|
56
71
|
nextVerifyError: null,
|
|
72
|
+
hangingFetch: null,
|
|
73
|
+
lastFetchSignal: undefined,
|
|
57
74
|
credentialSecretKey: null,
|
|
58
75
|
};
|
|
59
76
|
accounts.set(accountId, state);
|
|
@@ -66,10 +83,25 @@ function stateFor(accountId: string): AccountState {
|
|
|
66
83
|
* cursor — the foundation's deduplication makes this idempotent. */
|
|
67
84
|
export async function seedInboundMessage(accountId: string, msg: RawInboundMessage): Promise<void> {
|
|
68
85
|
const state = stateFor(accountId);
|
|
69
|
-
state.log.push(msg);
|
|
86
|
+
state.log.push({ message: msg });
|
|
70
87
|
if (state.watch) await state.watch.onMessages([msg]);
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
/** Test helper: a mail the provider cannot parse; fetch reports it as rejected and moves on. */
|
|
91
|
+
export function seedRejectedInboundMessage(accountId: string, providerMessageId: string): void {
|
|
92
|
+
stateFor(accountId).log.push({ rejected: { providerMessageId, reason: "unparseable" } });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Test helper: the full version of a message that fetchContent serves later. */
|
|
96
|
+
export function setInboundFullContent(accountId: string, msg: RawInboundMessage): void {
|
|
97
|
+
stateFor(accountId).fullContent.set(msg.providerMessageId, msg);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Test helper: the provider message ids of every fetchContent call so far. */
|
|
101
|
+
export function contentFetchesOf(accountId: string): readonly (readonly string[])[] {
|
|
102
|
+
return stateFor(accountId).contentFetches;
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
/** Test helper: injects a one-shot error into the next fetch (e.g.
|
|
74
106
|
* InboundAuthError or InboundCursorInvalidError). */
|
|
75
107
|
export function failNextFetchWith(accountId: string, err: unknown): void {
|
|
@@ -81,6 +113,25 @@ export function failNextVerifyWith(accountId: string, err: unknown): void {
|
|
|
81
113
|
stateFor(accountId).nextVerifyError = err;
|
|
82
114
|
}
|
|
83
115
|
|
|
116
|
+
/** Test helper: fetches of the account do not return until `release()`, whatever the abort signal
|
|
117
|
+
* says, like a provider that ignores cancellation on a dead connection. */
|
|
118
|
+
export function hangFetchesOf(accountId: string): { readonly release: () => void } {
|
|
119
|
+
const state = stateFor(accountId);
|
|
120
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
121
|
+
state.hangingFetch = promise;
|
|
122
|
+
return {
|
|
123
|
+
release: () => {
|
|
124
|
+
state.hangingFetch = null;
|
|
125
|
+
resolve();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Test helper: the abort signal the last fetch of the account received. */
|
|
131
|
+
export function lastFetchSignalOf(accountId: string): AbortSignal | undefined {
|
|
132
|
+
return stateFor(accountId).lastFetchSignal;
|
|
133
|
+
}
|
|
134
|
+
|
|
84
135
|
/** Test helper: every fetch reads this tenant secret first, like a real
|
|
85
136
|
* provider reads its stored credentials; a missing secret is an auth error. */
|
|
86
137
|
export function requireCredentialSecretOnFetch(accountId: string, key: string): void {
|
|
@@ -116,6 +167,11 @@ export function isWatching(accountId: string): boolean {
|
|
|
116
167
|
return accounts.get(accountId)?.watch != null;
|
|
117
168
|
}
|
|
118
169
|
|
|
170
|
+
/** Test helper: the trustedAuthservId of the account the last watch was started with. */
|
|
171
|
+
export function watchedTrustedAuthservIdOf(accountId: string): string | null | undefined {
|
|
172
|
+
return accounts.get(accountId)?.watchedTrustedAuthservId;
|
|
173
|
+
}
|
|
174
|
+
|
|
119
175
|
/** Test-Helper: kompletter State-Reset (Test-Isolation — Mock-States
|
|
120
176
|
* nie zwischen Tests teilen). */
|
|
121
177
|
export function resetInboundInMemory(): void {
|
|
@@ -142,6 +198,8 @@ export const inMemoryInboundMailPlugin: InboundMailProviderPlugin = {
|
|
|
142
198
|
},
|
|
143
199
|
fetch: async (ctx, account, cursor, opts): Promise<InboundFetchResult> => {
|
|
144
200
|
const state = stateFor(account.id);
|
|
201
|
+
state.lastFetchSignal = opts.signal;
|
|
202
|
+
if (state.hangingFetch) await state.hangingFetch;
|
|
145
203
|
if (state.nextFetchError) {
|
|
146
204
|
const err = state.nextFetchError;
|
|
147
205
|
state.nextFetchError = null;
|
|
@@ -153,15 +211,29 @@ export const inMemoryInboundMailPlugin: InboundMailProviderPlugin = {
|
|
|
153
211
|
const offset = offsetOf(cursor);
|
|
154
212
|
const slice = state.log.slice(offset, offset + opts.maxMessages);
|
|
155
213
|
const nextOffset = offset + slice.length;
|
|
214
|
+
const rejected = slice.flatMap((entry) => ("rejected" in entry ? [entry.rejected] : []));
|
|
156
215
|
return {
|
|
157
|
-
messages: slice,
|
|
216
|
+
messages: slice.flatMap((entry) => ("message" in entry ? [entry.message] : [])),
|
|
217
|
+
...(rejected.length > 0 && { rejected }),
|
|
158
218
|
nextCursor: { offset: nextOffset },
|
|
159
219
|
hasMore: nextOffset < state.log.length,
|
|
160
220
|
};
|
|
161
221
|
},
|
|
222
|
+
fetchContent: async (_ctx, account, providerMessageIds): Promise<InboundContentFetchResult> => {
|
|
223
|
+
const state = stateFor(account.id);
|
|
224
|
+
state.contentFetches.push([...providerMessageIds]);
|
|
225
|
+
const messages = providerMessageIds.flatMap((id) => state.fullContent.get(id) ?? []);
|
|
226
|
+
const found = new Set(messages.map((m) => m.providerMessageId));
|
|
227
|
+
return {
|
|
228
|
+
messages,
|
|
229
|
+
missing: providerMessageIds.filter((id) => !found.has(id)),
|
|
230
|
+
rejected: [],
|
|
231
|
+
};
|
|
232
|
+
},
|
|
162
233
|
watch: async (_ctx, account, handlers) => {
|
|
163
234
|
const state = stateFor(account.id);
|
|
164
235
|
state.watch = handlers;
|
|
236
|
+
state.watchedTrustedAuthservId = account.trustedAuthservId ?? null;
|
|
165
237
|
return async () => {
|
|
166
238
|
if (state.watch === handlers) state.watch = null;
|
|
167
239
|
};
|