@cosmicdrift/kumiko-samples 0.393.0 → 0.395.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +61 -136
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/watch-mailbox.ts +109 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/changes.json +7 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
- package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
- package/packages/bundled-features/src/managed-pages/table.ts +6 -2
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
- package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/README.md +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
- package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
- package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
- package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
- package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
- package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
- package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
- package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +8 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RawInboundMessage,
|
|
3
|
+
RejectedInboundMessage,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
5
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
|
+
import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
|
|
7
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
8
|
+
import type { MessageEnvelopeObject } from "imapflow";
|
|
9
|
+
import { type AddressObject, type ParsedMail, simpleParser } from "mailparser";
|
|
10
|
+
import { extractMailContent } from "./mime-content.ts";
|
|
11
|
+
import { senderAuthenticationOf } from "./sender-auth-headers.ts";
|
|
12
|
+
|
|
13
|
+
const SNIPPET_MAX = 300;
|
|
14
|
+
|
|
15
|
+
const log = createFallbackLogger("inbound-provider-imap");
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Message-Normalisierung → RawInboundMessage
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
function stripAngles(id: string): string {
|
|
22
|
+
return id.replace(/^<|>$/g, "").trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Normalizes the References chain: mailparser returns string | string[]
|
|
26
|
+
* | undefined; when References is missing, In-Reply-To provides the thread
|
|
27
|
+
* anchor. */
|
|
28
|
+
export function normalizeReferences(parsed: {
|
|
29
|
+
references?: string | string[];
|
|
30
|
+
inReplyTo?: string;
|
|
31
|
+
}): string[] {
|
|
32
|
+
const raw = parsed.references;
|
|
33
|
+
const list = raw === undefined ? [] : Array.isArray(raw) ? raw : raw.split(/\s+/);
|
|
34
|
+
const refs = list.map(stripAngles).filter((r) => r.length > 0);
|
|
35
|
+
if (refs.length === 0 && parsed.inReplyTo) {
|
|
36
|
+
const inReplyTo = stripAngles(parsed.inReplyTo);
|
|
37
|
+
if (inReplyTo) return [inReplyTo];
|
|
38
|
+
}
|
|
39
|
+
return refs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addressList(a: AddressObject | AddressObject[] | undefined): string[] {
|
|
43
|
+
if (!a) return [];
|
|
44
|
+
const arr = Array.isArray(a) ? a : [a];
|
|
45
|
+
return arr.map((x) => x.text).filter((t): t is string => Boolean(t));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** imapflow provides internalDate as either a Date or string depending on
|
|
49
|
+
* the code path. The string form is deliberately discarded (no date parsing
|
|
50
|
+
* in feature code, no-date-api); toRawInboundMessage then falls back to the
|
|
51
|
+
* mailparser Date header, which is always a real Date. */
|
|
52
|
+
export function coerceDate(d: Date | string | undefined): Date | undefined {
|
|
53
|
+
return d instanceof Date ? d : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** providerMessageId: a UID is unique only within a UIDVALIDITY; together,
|
|
57
|
+
* they form the stable idempotency anchor. */
|
|
58
|
+
export function buildProviderMessageId(uidValidity: string, uid: number): string {
|
|
59
|
+
return `${uidValidity}:${uid}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function toRawInboundMessage(args: {
|
|
63
|
+
readonly source: Uint8Array;
|
|
64
|
+
readonly uid: number;
|
|
65
|
+
readonly uidValidity: string;
|
|
66
|
+
readonly internalDate: Date | undefined;
|
|
67
|
+
/** IMAP envelope; lets a mail that cannot be parsed still show sender and subject. */
|
|
68
|
+
readonly envelope?: MessageEnvelopeObject | undefined;
|
|
69
|
+
/** authserv-id of the mailbox's own server; without it senderAuthentication is null. */
|
|
70
|
+
readonly trustedAuthservId?: string | null;
|
|
71
|
+
}): Promise<RawInboundMessage> {
|
|
72
|
+
const parsed: ParsedMail = await simpleParser(Buffer.from(args.source));
|
|
73
|
+
const messageIdHeader = parsed.messageId ? stripAngles(parsed.messageId) : null;
|
|
74
|
+
const receivedAt = args.internalDate ?? parsed.date;
|
|
75
|
+
const content = await extractMailContent(parsed);
|
|
76
|
+
const text = parsed.text?.trim() ?? "";
|
|
77
|
+
return {
|
|
78
|
+
providerMessageId: buildProviderMessageId(args.uidValidity, args.uid),
|
|
79
|
+
messageIdHeader: messageIdHeader && messageIdHeader.length > 0 ? messageIdHeader : null,
|
|
80
|
+
providerThreadId: null,
|
|
81
|
+
references: normalizeReferences(parsed),
|
|
82
|
+
from: parsed.from?.text ?? "",
|
|
83
|
+
to: addressList(parsed.to),
|
|
84
|
+
cc: addressList(parsed.cc),
|
|
85
|
+
subject: parsed.subject ?? "",
|
|
86
|
+
snippet: text.slice(0, SNIPPET_MAX),
|
|
87
|
+
// Lib-boundary bridge (Date from imapflow/mailparser) → Temporal;
|
|
88
|
+
// when no date exists, Epoch is the honest marker.
|
|
89
|
+
receivedAtIso: (receivedAt
|
|
90
|
+
? legacyDateToInstant(receivedAt)
|
|
91
|
+
: Temporal.Instant.fromEpochMilliseconds(0)
|
|
92
|
+
).toString(),
|
|
93
|
+
text: content.text,
|
|
94
|
+
...(content.html !== null && { html: content.html }),
|
|
95
|
+
attachments: content.attachments,
|
|
96
|
+
skippedAttachments: content.skipped,
|
|
97
|
+
senderAuthentication: senderAuthenticationOf(parsed, args.trustedAuthservId ?? null),
|
|
98
|
+
rawMime: args.source,
|
|
99
|
+
scope: "inbox",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function envelopeAddresses(list: MessageEnvelopeObject["to"]): string[] {
|
|
104
|
+
return (list ?? []).map((a) => a.address ?? "").filter((a) => a !== "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function headersOfEnvelope(
|
|
108
|
+
envelope: MessageEnvelopeObject,
|
|
109
|
+
internalDate: Date | undefined,
|
|
110
|
+
): NonNullable<RejectedInboundMessage["headers"]> {
|
|
111
|
+
const sender = envelope.from?.[0];
|
|
112
|
+
const receivedAt = internalDate ?? coerceDate(envelope.date);
|
|
113
|
+
return {
|
|
114
|
+
from: sender?.address ?? "",
|
|
115
|
+
to: envelopeAddresses(envelope.to),
|
|
116
|
+
cc: envelopeAddresses(envelope.cc),
|
|
117
|
+
subject: envelope.subject ?? "",
|
|
118
|
+
messageIdHeader: envelope.messageId ? stripAngles(envelope.messageId) : null,
|
|
119
|
+
receivedAtIso: receivedAt ? legacyDateToInstant(receivedAt).toString() : "",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ParsedOrRejected =
|
|
124
|
+
| { readonly message: RawInboundMessage }
|
|
125
|
+
| { readonly rejected: RejectedInboundMessage };
|
|
126
|
+
|
|
127
|
+
/** A mail the MIME parser cannot handle must not stop the mailbox: it is reported as rejected so the
|
|
128
|
+
* foundation quarantines it and the cursor moves on. */
|
|
129
|
+
export async function parseOrReject(
|
|
130
|
+
args: Parameters<typeof toRawInboundMessage>[0],
|
|
131
|
+
): Promise<ParsedOrRejected> {
|
|
132
|
+
try {
|
|
133
|
+
return { message: await toRawInboundMessage(args) };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Class name only: parser messages can quote header content.
|
|
136
|
+
log.warn("mail could not be parsed", {
|
|
137
|
+
uid: args.uid,
|
|
138
|
+
error: err instanceof Error ? err.name : typeof err,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
rejected: {
|
|
142
|
+
providerMessageId: buildProviderMessageId(args.uidValidity, args.uid),
|
|
143
|
+
reason: "unparseable",
|
|
144
|
+
rawMime: args.source,
|
|
145
|
+
...(args.envelope && { headers: headersOfEnvelope(args.envelope, args.internalDate) }),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
// Loads the mailbox a reconnect targets. The db is tenant-scoped, so another tenant's account id
|
|
2
|
-
// finds nothing.
|
|
2
|
+
// finds nothing. A disconnected mailbox is reconnectable on purpose: it keeps its id, threads and
|
|
3
|
+
// sync cursor, so reconnecting never re-imports history.
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
|
-
InboundMailAccountStatuses,
|
|
6
6
|
InboundMailAuthMethods,
|
|
7
7
|
mailAccountsProjectionTable,
|
|
8
8
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
9
9
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
10
10
|
import {
|
|
11
11
|
failNotFound,
|
|
12
|
-
failUnprocessable,
|
|
13
12
|
UnprocessableError,
|
|
14
13
|
type WriteFailure,
|
|
15
14
|
writeFailure,
|
|
@@ -21,7 +20,9 @@ type ReconnectAccountRow = {
|
|
|
21
20
|
readonly provider: string;
|
|
22
21
|
readonly authMethod: string;
|
|
23
22
|
readonly displayName: string | null;
|
|
23
|
+
readonly ownerUserId: string | null;
|
|
24
24
|
readonly status: string | null;
|
|
25
|
+
readonly trustedAuthservId: string | null;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export type ReconnectAccountLookup =
|
|
@@ -36,9 +37,6 @@ export async function findReconnectableAccount(
|
|
|
36
37
|
id: accountId,
|
|
37
38
|
});
|
|
38
39
|
if (!row) return failNotFound("mail-account", accountId);
|
|
39
|
-
if (row.status === InboundMailAccountStatuses.disconnected) {
|
|
40
|
-
return failUnprocessable("account_disconnected", { accountId });
|
|
41
|
-
}
|
|
42
40
|
if (row.provider !== IMAP_PROVIDER_KEY || row.authMethod !== InboundMailAuthMethods.password) {
|
|
43
41
|
const { reason, i18nKey } = IMAP_RECONNECT_ERRORS.notPasswordImap;
|
|
44
42
|
return writeFailure(new UnprocessableError(reason, { i18nKey, details: { accountId } }));
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
// reconnect-mailbox: new credentials for an existing password IMAP mailbox, typically after the
|
|
2
2
|
// account went to auth_error. The login is checked before the transaction opens; only then the
|
|
3
|
-
//
|
|
3
|
+
// credential secret of the same account is overwritten and the account goes back to active (also from disconnected), both
|
|
4
4
|
// on the request transaction, so a failure leaves status and secret as they were.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
InboundMailAccountStatuses,
|
|
8
7
|
InboundMailFoundationHandlers,
|
|
8
|
+
type InboundMailRoles,
|
|
9
9
|
inboundCredentialSecretKey,
|
|
10
|
+
isMailAdmin,
|
|
11
|
+
mayReactivateMailbox,
|
|
10
12
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
11
13
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
12
14
|
import {
|
|
13
15
|
type BeforeTransactionResult,
|
|
14
16
|
defineWriteHandler,
|
|
15
17
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
|
|
16
19
|
import * as z from "zod";
|
|
17
20
|
import {
|
|
18
21
|
CREDENTIAL_PREVIEW,
|
|
@@ -23,51 +26,78 @@ import {
|
|
|
23
26
|
import { findReconnectableAccount } from "./reconnect-account.ts";
|
|
24
27
|
|
|
25
28
|
const reconnectMailboxSchema = connectMailboxSchema
|
|
26
|
-
.pick({
|
|
29
|
+
.pick({
|
|
30
|
+
host: true,
|
|
31
|
+
port: true,
|
|
32
|
+
user: true,
|
|
33
|
+
password: true,
|
|
34
|
+
smtpHost: true,
|
|
35
|
+
smtpPort: true,
|
|
36
|
+
trustedAuthservId: true,
|
|
37
|
+
})
|
|
27
38
|
.extend({ accountId: z.uuid() });
|
|
28
39
|
|
|
29
40
|
const SECRET_TRANSACTION_REASON =
|
|
30
41
|
"the credential secret is written on the request transaction so a failed reconnect rolls the account status back too";
|
|
31
42
|
|
|
32
|
-
export const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
43
|
+
export const createReconnectMailboxHandler = ({ memberRoles, adminRoles }: InboundMailRoles) =>
|
|
44
|
+
defineWriteHandler({
|
|
45
|
+
name: "reconnect-mailbox",
|
|
46
|
+
description:
|
|
47
|
+
"Replaces the credentials of an existing password IMAP mailbox: checks the IMAP login and the SMTP login of the outgoing server first, then overwrites the credential secret of the same account and sets it back to active, keeping its threads and sync cursor.",
|
|
48
|
+
// The payload carries the plaintext password: as a tool call it would reach the model provider.
|
|
49
|
+
agent: { expose: false },
|
|
50
|
+
schema: reconnectMailboxSchema,
|
|
51
|
+
// Members: the owner re-arms a personal mailbox; the lookup gate keeps shared ones to admins.
|
|
52
|
+
access: { roles: [...memberRoles] },
|
|
53
|
+
// Same cap as connect: each call opens a connection to a host the caller picks.
|
|
54
|
+
rateLimit: { per: "tenant+handler", limit: 10, windowSeconds: 600 },
|
|
55
|
+
escapeHatch: { grants: ["unsafeRaw"], reason: SECRET_TRANSACTION_REASON },
|
|
56
|
+
beforeTransaction: async (event, ctx): Promise<BeforeTransactionResult<null>> => {
|
|
57
|
+
const lookup = await findReconnectableAccount(ctx.db, event.payload.accountId);
|
|
58
|
+
if (!lookup.isSuccess) return lookup;
|
|
59
|
+
// Before the login check, so a denied caller cannot make the server probe a host; same answer as a missing account.
|
|
60
|
+
if (!mayReactivateMailbox(lookup.row.ownerUserId, ctx.user, adminRoles)) {
|
|
61
|
+
return failNotFound("mail-account", event.payload.accountId);
|
|
62
|
+
}
|
|
63
|
+
return checkMailboxLogin(credentialDocumentOf(event.payload), ctx.obs.log);
|
|
64
|
+
},
|
|
65
|
+
handler: async (event, ctx) => {
|
|
66
|
+
const { accountId } = event.payload;
|
|
67
|
+
// Secret first: reactivate from disconnected requires the credential to exist.
|
|
68
|
+
const secrets = requireSecretsContext(
|
|
69
|
+
{ secrets: ctx.services.secrets, user: ctx.user },
|
|
70
|
+
"reconnect-mailbox",
|
|
71
|
+
).withRunner(ctx.db.unsafeRaw());
|
|
72
|
+
await secrets.set(
|
|
73
|
+
event.user.tenantId,
|
|
74
|
+
inboundCredentialSecretKey(accountId),
|
|
75
|
+
JSON.stringify(credentialDocumentOf(event.payload)),
|
|
76
|
+
{
|
|
77
|
+
redact: () => CREDENTIAL_PREVIEW,
|
|
78
|
+
hint: `IMAP credentials for inbound mail account ${accountId}`,
|
|
79
|
+
updatedBy: event.user.id,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
const reactivated = await ctx.dispatch.write(
|
|
83
|
+
InboundMailFoundationHandlers.reactivateAccount,
|
|
84
|
+
{
|
|
85
|
+
accountId,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (!reactivated.isSuccess) return reactivated;
|
|
89
|
+
// The sender-check policy is tenant-wide: only admins set it, the owner's form does not carry it.
|
|
90
|
+
if (isMailAdmin(event.user, adminRoles)) {
|
|
91
|
+
// The admin form shows the stored id, so an empty field means the admin cleared it.
|
|
92
|
+
const trusted = await ctx.dispatch.write(
|
|
93
|
+
InboundMailFoundationHandlers.setTrustedAuthservId,
|
|
94
|
+
{
|
|
95
|
+
accountId,
|
|
96
|
+
trustedAuthservId: event.payload.trustedAuthservId ?? null,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
if (!trusted.isSuccess) return trusted;
|
|
100
|
+
}
|
|
101
|
+
return { isSuccess: true, data: { accountId } };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Initial values of the reconnect form: the server settings of the stored credential, never the
|
|
2
2
|
// password. A missing or unreadable credential leaves the fields empty for the admin to fill in.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type InboundMailRoles,
|
|
6
|
+
inboundCredentialSecretKey,
|
|
7
|
+
mayReactivateMailbox,
|
|
8
|
+
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
5
9
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
6
10
|
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
11
|
import * as z from "zod";
|
|
@@ -11,18 +15,24 @@ import { findReconnectableAccount } from "./reconnect-account.ts";
|
|
|
11
15
|
|
|
12
16
|
const reconnectPrefillSchema = z.object({ accountId: z.uuid() }).strict();
|
|
13
17
|
|
|
14
|
-
export const
|
|
18
|
+
export const createReconnectPrefillQuery = ({
|
|
19
|
+
memberRoles,
|
|
20
|
+
adminRoles,
|
|
21
|
+
}: InboundMailRoles): QueryHandlerDef => ({
|
|
15
22
|
name: "reconnect-prefill",
|
|
16
23
|
schema: reconnectPrefillSchema,
|
|
17
|
-
access: { roles: [
|
|
24
|
+
access: { roles: [...memberRoles] },
|
|
18
25
|
// Only the reconnect form needs these settings; the agent has no write to use them with.
|
|
19
26
|
agent: { expose: false },
|
|
20
27
|
description:
|
|
21
|
-
"Returns the server settings (IMAP host and port, user name, SMTP host and port) stored for a password IMAP mailbox without its password, or null when the mailbox is not found
|
|
28
|
+
"Returns the server settings (IMAP host and port, user name, SMTP host and port, trusted authserv-id) stored for a password IMAP mailbox without its password, or null when the mailbox is not found or not a password IMAP mailbox; use it to prefill the reconnect form.",
|
|
22
29
|
handler: async (query, ctx) => {
|
|
23
30
|
const { accountId } = reconnectPrefillSchema.parse(query.payload);
|
|
24
31
|
const lookup = await findReconnectableAccount(ctx.db, accountId);
|
|
25
|
-
|
|
32
|
+
// Same null as for a missing account: a caller who may not reconnect it learns nothing about it.
|
|
33
|
+
if (!lookup.isSuccess || !mayReactivateMailbox(lookup.row.ownerUserId, ctx.user, adminRoles)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
const secrets = requireSecretsContext(
|
|
28
38
|
{ secrets: ctx.services.secrets, user: ctx.user },
|
|
@@ -31,8 +41,9 @@ export const reconnectPrefillQuery: QueryHandlerDef = {
|
|
|
31
41
|
const stored = await secrets.get(ctx.user.tenantId, inboundCredentialSecretKey(accountId));
|
|
32
42
|
const parsed = stored ? parseImapCredentialDocument(stored.reveal()) : undefined;
|
|
33
43
|
const displayName = lookup.row.displayName ?? "";
|
|
34
|
-
|
|
44
|
+
const trustedAuthservId = lookup.row.trustedAuthservId ?? "";
|
|
45
|
+
if (!parsed?.ok) return { accountId, displayName, trustedAuthservId };
|
|
35
46
|
const { host, port, user, smtpHost, smtpPort } = parsed.doc;
|
|
36
|
-
return { accountId, displayName, host, port, user, smtpHost, smtpPort };
|
|
47
|
+
return { accountId, displayName, host, port, user, smtpHost, smtpPort, trustedAuthservId };
|
|
37
48
|
},
|
|
38
|
-
};
|
|
49
|
+
});
|