@cosmicdrift/kumiko-samples 0.393.0 → 0.394.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +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 +30 -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 +75 -49
- 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-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +87 -38
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +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,73 @@
|
|
|
1
|
+
// Client feature for inbound-mail-foundation. The mail account screens offer "Reactivate" for an OAuth
|
|
2
|
+
// mailbox as a clientAction: signing in again is a browser redirect into the same connect route and
|
|
3
|
+
// callback that created the mailbox, which a writeHandler cannot do.
|
|
4
|
+
|
|
5
|
+
import type { ClientActionHandler, ClientActionOutcome } from "@cosmicdrift/kumiko-renderer";
|
|
6
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
|
|
7
|
+
import {
|
|
8
|
+
INBOUND_MAIL_CONNECT_PATH,
|
|
9
|
+
INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
10
|
+
INBOUND_MAIL_RECONNECT_OAUTH_ACTION,
|
|
11
|
+
} from "./constants.ts";
|
|
12
|
+
|
|
13
|
+
export type InboundMailFoundationClientOptions = {
|
|
14
|
+
/** Mount path of the connect route, when the app moved it (`connectPath` of
|
|
15
|
+
* createInboundMailConnectRoutes). Default "/api/inbound-mail/connect". */
|
|
16
|
+
readonly connectPath?: string;
|
|
17
|
+
/** Leaves the app for the provider's sign-in page. Default: `location.assign`. */
|
|
18
|
+
readonly navigate?: (url: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const INVALID_RECONNECT_ROW = "inboundMail.accounts.reconnect.invalidRow";
|
|
22
|
+
|
|
23
|
+
function textField(payload: Readonly<Record<string, unknown>>, field: string): string | undefined {
|
|
24
|
+
const value = payload[field];
|
|
25
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The callback trusts only the signed state, which the connect route builds from the session and these
|
|
29
|
+
// parameters; scope "user" makes the signed-in caller the owner, so the route re-checks ownership.
|
|
30
|
+
export function oauthReconnectUrl(
|
|
31
|
+
connectPath: string,
|
|
32
|
+
row: { readonly provider: string; readonly address: string; readonly ownerUserId?: string },
|
|
33
|
+
): string {
|
|
34
|
+
const query = new URLSearchParams({
|
|
35
|
+
provider: row.provider,
|
|
36
|
+
scope: row.ownerUserId === undefined ? "shared" : "user",
|
|
37
|
+
mailbox: row.address,
|
|
38
|
+
});
|
|
39
|
+
return `${connectPath}?${query.toString()}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function reconnectOauthHandler(
|
|
43
|
+
connectPath: string,
|
|
44
|
+
navigate: (url: string) => void,
|
|
45
|
+
): ClientActionHandler {
|
|
46
|
+
return async (payload): Promise<ClientActionOutcome> => {
|
|
47
|
+
const provider = textField(payload, "provider");
|
|
48
|
+
const address = textField(payload, "address");
|
|
49
|
+
if (provider === undefined || address === undefined) {
|
|
50
|
+
return { ok: false, message: INVALID_RECONNECT_ROW };
|
|
51
|
+
}
|
|
52
|
+
const ownerUserId = textField(payload, "ownerUserId");
|
|
53
|
+
navigate(
|
|
54
|
+
oauthReconnectUrl(connectPath, { provider, address, ...(ownerUserId && { ownerUserId }) }),
|
|
55
|
+
);
|
|
56
|
+
return { ok: true };
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function inboundMailFoundationClient(
|
|
61
|
+
options: InboundMailFoundationClientOptions = {},
|
|
62
|
+
): ClientFeatureDefinition {
|
|
63
|
+
const navigate = options.navigate ?? ((url: string) => globalThis.location.assign(url));
|
|
64
|
+
return {
|
|
65
|
+
name: INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
66
|
+
clientActions: {
|
|
67
|
+
[INBOUND_MAIL_RECONNECT_OAUTH_ACTION]: reconnectOauthHandler(
|
|
68
|
+
options.connectPath ?? INBOUND_MAIL_CONNECT_PATH,
|
|
69
|
+
navigate,
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Constants the browser client shares with the server feature; the server constants re-export them.
|
|
2
|
+
export const INBOUND_MAIL_FOUNDATION_FEATURE = "inbound-mail-foundation" as const;
|
|
3
|
+
|
|
4
|
+
/** Default mount path of the OAuth connect route; the web client builds the reconnect link from it. */
|
|
5
|
+
export const INBOUND_MAIL_CONNECT_PATH = "/api/inbound-mail/connect";
|
|
6
|
+
/** Client action of the OAuth "Reactivate" row action: sends the browser to the OAuth connect route. */
|
|
7
|
+
export const INBOUND_MAIL_RECONNECT_OAUTH_ACTION = `${INBOUND_MAIL_FOUNDATION_FEATURE}:reconnect-oauth`;
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.394.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "The IMAP provider reads text, HTML and attachments",
|
|
6
|
+
"detail": "The provider parses full mails. Inline images are skipped, forwarded message/rfc822 mails are unpacked up to depth 3, and the limits are 25 MB per attachment, 50 MB per mail and 50 attachments; everything skipped is reported in skippedAttachments with its reason. The new plugin method fetchContent reloads the content of a mail by provider id."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.394.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "IMAP and SMTP hosts given as IP addresses connect over TLS",
|
|
12
|
+
"detail": "imapflow passes servername false for an IP literal, which Bun's TLS rejects, so such mailboxes could not connect. The IMAP client now sets an undefined servername; nodemailer already omits it for SMTP."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.394.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "The IMAP cursor follows UIDs and skips known mails",
|
|
18
|
+
"detail": "lastUid is derived from the fetched UIDs, so a quarantined or already known mail at the end of a full batch no longer holds the cursor back or gets fetched again; the envelope is read so a mail the parser throws on still gets its header entry. A forwarded mail that cannot be parsed is kept as an attachment instead of failing the whole mail."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.394.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "Members connect a personal IMAP mailbox from \"My mail accounts\"",
|
|
24
|
+
"detail": "The new write handler inbound-provider-imap:write:connect-my-mailbox (every mail member role, not exposed to the agent, 10 calls per user in 10 minutes) takes the connect-mailbox fields without trustedAuthservId, checks the IMAP and SMTP login first and creates a mailbox owned by the caller. A disconnected personal mailbox of the caller with the same address is reactivated under its old account id; an address that belongs to another user or to a shared mailbox is refused with the usual conflict answer. connect-account stays admin-only; the handler creates the personal account through the system identity with the owner taken from the caller session, never from the payload. The dormant screen my-mail-account-connect-imap (exported as MY_IMAP_CONNECT_SCREEN_ID) is reached from the new \"Connect mailbox\" toolbar action of my-mail-accounts; it has no sender-check field, that policy stays with the admin form. New keys in all locale packages."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.394.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "The IMAP watch quarantines an unparseable mail right away",
|
|
30
|
+
"detail": "A mail the MIME parser cannot handle used to be dropped by the live watch and reached the quarantine only with the next reconciliation poll, up to five minutes later. The provider watch handler now passes such mails to `handlers.onMessages(messages, rejected)` and the supervisor quarantines them through the same ingest path as the poll. `onMessages` takes an optional second argument, so existing providers are unaffected. A failed account health update after a quarantine now logs the error code of the rejected write."
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"version": "0.393.0",
|
|
4
34
|
"type": "improvement",
|
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
// leaves neither behind.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
type AddressMatch,
|
|
8
|
+
findMailboxByAddress,
|
|
7
9
|
InboundAuthError,
|
|
10
|
+
InboundMailAccountStatuses,
|
|
8
11
|
InboundMailAuthMethods,
|
|
9
12
|
InboundMailFoundationHandlers,
|
|
13
|
+
type InboundMailRoles,
|
|
10
14
|
inboundCredentialSecretKey,
|
|
11
15
|
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
12
16
|
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
17
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
13
18
|
import {
|
|
14
19
|
type BeforeTransactionResult,
|
|
20
|
+
createSystemUser,
|
|
15
21
|
defineWriteHandler,
|
|
22
|
+
type HandlerContext,
|
|
16
23
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
17
24
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
25
|
+
import type { WriteResult } from "@cosmicdrift/kumiko-types/handlers";
|
|
18
26
|
import * as z from "zod";
|
|
19
27
|
import { IMAP_CONNECT_ERRORS, IMAP_PROVIDER_KEY, SMTP_CONNECT_ERRORS } from "./constants.ts";
|
|
20
28
|
import {
|
|
@@ -36,12 +44,22 @@ export const connectMailboxSchema = imapCredentialDocumentSchema
|
|
|
36
44
|
password: z.string().min(1).max(1000),
|
|
37
45
|
smtpHost: z.union([z.literal(""), smtpHostSchema]).nullish(),
|
|
38
46
|
smtpPort: z.number().int().min(1).max(65535),
|
|
47
|
+
trustedAuthservId: z.string().trim().max(255).nullish(),
|
|
39
48
|
});
|
|
40
49
|
type ConnectMailboxLoginFields = Pick<
|
|
41
50
|
z.infer<typeof connectMailboxSchema>,
|
|
42
51
|
"host" | "port" | "user" | "password" | "smtpHost" | "smtpPort"
|
|
43
52
|
>;
|
|
44
53
|
|
|
54
|
+
function isReopenableByThisForm(existing: AddressMatch, ownerUserId: string | null): boolean {
|
|
55
|
+
return (
|
|
56
|
+
existing.status === InboundMailAccountStatuses.disconnected &&
|
|
57
|
+
existing.provider === IMAP_PROVIDER_KEY &&
|
|
58
|
+
existing.authMethod === InboundMailAuthMethods.password &&
|
|
59
|
+
existing.ownerUserId === ownerUserId
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
const connectedAccountSchema = z.object({ accountId: z.string() });
|
|
46
64
|
|
|
47
65
|
// Matched on the mapped error text, which also carries the host, so only markers no host name
|
|
@@ -52,9 +70,6 @@ const TLS_FAILURE = /certificate|self[- ]signed|wrong[ _]version[ _]number|EPROT
|
|
|
52
70
|
// A fixed preview: any slice of the JSON document would expose the host or the user.
|
|
53
71
|
export const CREDENTIAL_PREVIEW = "IMAP login";
|
|
54
72
|
|
|
55
|
-
const SECRET_TRANSACTION_REASON =
|
|
56
|
-
"the credential secret is written on the request transaction so a failed account connect rolls it back too";
|
|
57
|
-
|
|
58
73
|
type ConnectFailure = keyof typeof IMAP_CONNECT_ERRORS;
|
|
59
74
|
|
|
60
75
|
// Most hosters name the outgoing server like the incoming one, or with smtp. for imap.
|
|
@@ -122,46 +137,114 @@ export async function checkMailboxLogin(
|
|
|
122
137
|
return { isSuccess: true, data: null };
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
export const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
export const SECRET_TRANSACTION_REASON =
|
|
141
|
+
"the credential secret is written on the request transaction so a failed account connect rolls it back too";
|
|
142
|
+
|
|
143
|
+
type ConnectedMailbox = {
|
|
144
|
+
readonly displayName: string;
|
|
145
|
+
readonly address: string;
|
|
146
|
+
readonly credentialDocument: ImapCredentialDocument;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Shared by the admin form (shared mailbox) and the member form (personal mailbox, owner = caller).
|
|
150
|
+
// A disconnected password mailbox of the same scope and owner is brought back under its old account id,
|
|
151
|
+
// so cursor, seen anchors and threads survive; anything else keeps connect-account's conflict answer.
|
|
152
|
+
export async function connectPasswordMailbox(
|
|
153
|
+
handlerName: string,
|
|
154
|
+
event: { readonly user: { readonly id: string; readonly tenantId: string } },
|
|
155
|
+
ctx: HandlerContext,
|
|
156
|
+
mailbox: ConnectedMailbox,
|
|
157
|
+
scope: "shared" | "user",
|
|
158
|
+
): Promise<WriteResult<{ accountId: string }>> {
|
|
159
|
+
const { displayName, address, credentialDocument } = mailbox;
|
|
160
|
+
const ownerUserId = scope === "user" ? event.user.id : null;
|
|
161
|
+
const existing = await findMailboxByAddress(ctx, event.user.tenantId, address);
|
|
162
|
+
const reopened = existing !== undefined && isReopenableByThisForm(existing, ownerUserId);
|
|
163
|
+
let accountId: string;
|
|
164
|
+
if (reopened) {
|
|
165
|
+
accountId = existing.id;
|
|
166
|
+
} else {
|
|
167
|
+
const accountFields = {
|
|
142
168
|
provider: IMAP_PROVIDER_KEY,
|
|
143
169
|
authMethod: InboundMailAuthMethods.password,
|
|
144
170
|
displayName,
|
|
145
171
|
address,
|
|
146
|
-
scope
|
|
147
|
-
}
|
|
172
|
+
scope,
|
|
173
|
+
};
|
|
174
|
+
// connect-account is admin-only, so a member's personal mailbox goes through the system identity
|
|
175
|
+
// with the owner taken from this session, like the OAuth callback does.
|
|
176
|
+
const connected =
|
|
177
|
+
scope === "user"
|
|
178
|
+
? await ctx.dispatch.writeAs(
|
|
179
|
+
createSystemUser(ctx.user.tenantId, [ROLES.SystemAdmin]),
|
|
180
|
+
InboundMailFoundationHandlers.connectAccount,
|
|
181
|
+
{ ...accountFields, ownerUserIdOverride: event.user.id },
|
|
182
|
+
)
|
|
183
|
+
: await ctx.dispatch.write(InboundMailFoundationHandlers.connectAccount, accountFields);
|
|
148
184
|
if (!connected.isSuccess) return connected;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
accountId = connectedAccountSchema.parse(connected.data).accountId;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const secrets = requireSecretsContext(
|
|
189
|
+
{ secrets: ctx.services.secrets, user: ctx.user },
|
|
190
|
+
handlerName,
|
|
191
|
+
).withRunner(ctx.db.unsafeRaw());
|
|
192
|
+
await secrets.set(
|
|
193
|
+
event.user.tenantId,
|
|
194
|
+
inboundCredentialSecretKey(accountId),
|
|
195
|
+
JSON.stringify(credentialDocument),
|
|
196
|
+
{
|
|
197
|
+
redact: () => CREDENTIAL_PREVIEW,
|
|
198
|
+
hint: `IMAP credentials for inbound mail account ${accountId}`,
|
|
199
|
+
updatedBy: event.user.id,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
if (reopened) {
|
|
203
|
+
// Secret first: reactivating a disconnected account requires the credential to exist.
|
|
204
|
+
const reactivated = await ctx.dispatch.write(InboundMailFoundationHandlers.reactivateAccount, {
|
|
205
|
+
accountId,
|
|
206
|
+
});
|
|
207
|
+
if (!reactivated.isSuccess) return reactivated;
|
|
208
|
+
}
|
|
209
|
+
return { isSuccess: true, data: { accountId } };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const createConnectMailboxHandler = ({ adminRoles }: InboundMailRoles) =>
|
|
213
|
+
defineWriteHandler({
|
|
214
|
+
name: "connect-mailbox",
|
|
215
|
+
description:
|
|
216
|
+
"Connects a shared IMAP mailbox with a password over implicit TLS: checks the IMAP login and the SMTP login of the outgoing server (given, or derived from the IMAP host) first, then creates the mail account and stores both servers, user and password as its credential secret in one step.",
|
|
217
|
+
// The payload carries the plaintext password: as a tool call it would reach the model provider.
|
|
218
|
+
agent: { expose: false },
|
|
219
|
+
schema: connectMailboxSchema,
|
|
220
|
+
access: { roles: [...adminRoles] },
|
|
221
|
+
// Each call opens a connection to a host the caller picks; the cap keeps the form from being
|
|
222
|
+
// used to scan ports or to guess passwords at someone else's mail server.
|
|
223
|
+
rateLimit: { per: "tenant+handler", limit: 10, windowSeconds: 600 },
|
|
224
|
+
escapeHatch: { grants: ["unsafeRaw"], reason: SECRET_TRANSACTION_REASON },
|
|
225
|
+
beforeTransaction: (event, ctx) =>
|
|
226
|
+
checkMailboxLogin(credentialDocumentOf(event.payload), ctx.obs.log),
|
|
227
|
+
handler: async (event, ctx) => {
|
|
228
|
+
const { displayName, address, trustedAuthservId } = event.payload;
|
|
229
|
+
const connected = await connectPasswordMailbox(
|
|
230
|
+
"connect-mailbox",
|
|
231
|
+
event,
|
|
232
|
+
ctx,
|
|
233
|
+
{ displayName, address, credentialDocument: credentialDocumentOf(event.payload) },
|
|
234
|
+
"shared",
|
|
235
|
+
);
|
|
236
|
+
if (!connected.isSuccess) return connected;
|
|
237
|
+
const { accountId } = connected.data;
|
|
238
|
+
if (trustedAuthservId) {
|
|
239
|
+
const trusted = await ctx.dispatch.write(
|
|
240
|
+
InboundMailFoundationHandlers.setTrustedAuthservId,
|
|
241
|
+
{
|
|
242
|
+
accountId,
|
|
243
|
+
trustedAuthservId,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
if (!trusted.isSuccess) return trusted;
|
|
247
|
+
}
|
|
248
|
+
return { isSuccess: true, data: { accountId } };
|
|
249
|
+
},
|
|
250
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// connect-my-mailbox: a member connects a personal password IMAP mailbox (owner = the caller).
|
|
2
|
+
// Same login check and secret write as connect-mailbox; the sender-check policy is tenant-wide and
|
|
3
|
+
// stays with the admin form.
|
|
4
|
+
|
|
5
|
+
import type { InboundMailRoles } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
6
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import {
|
|
8
|
+
checkMailboxLogin,
|
|
9
|
+
connectMailboxSchema,
|
|
10
|
+
connectPasswordMailbox,
|
|
11
|
+
credentialDocumentOf,
|
|
12
|
+
SECRET_TRANSACTION_REASON,
|
|
13
|
+
} from "./connect-mailbox.ts";
|
|
14
|
+
|
|
15
|
+
const connectMyMailboxSchema = connectMailboxSchema.omit({ trustedAuthservId: true });
|
|
16
|
+
|
|
17
|
+
const MY_MAILBOX_ESCAPE_REASON = `${SECRET_TRANSACTION_REASON}; the account is created with writeAs(system) on connect-account, which is admin-only, and the owner comes from the caller's session`;
|
|
18
|
+
|
|
19
|
+
export const createConnectMyMailboxHandler = ({ memberRoles }: InboundMailRoles) =>
|
|
20
|
+
defineWriteHandler({
|
|
21
|
+
name: "connect-my-mailbox",
|
|
22
|
+
description:
|
|
23
|
+
"Connects a personal IMAP mailbox of the caller with a password over implicit TLS: checks the IMAP login and the SMTP login of the outgoing server first, then creates the mail account owned by the caller and stores both servers, user and password as its credential secret in one step. A disconnected personal mailbox of the caller with the same address is reactivated under its old account id.",
|
|
24
|
+
// The payload carries the plaintext password: as a tool call it would reach the model provider.
|
|
25
|
+
agent: { expose: false },
|
|
26
|
+
schema: connectMyMailboxSchema,
|
|
27
|
+
access: { roles: [...memberRoles] },
|
|
28
|
+
// Each call opens a connection to a host the caller picks; per user, because any member may call it.
|
|
29
|
+
rateLimit: { per: "user", limit: 10, windowSeconds: 600 },
|
|
30
|
+
// connect-account is admin-only; the personal mailbox is created through the system identity. The owner
|
|
31
|
+
// is always event.user.id from the session, never a payload field.
|
|
32
|
+
escapeHatch: { grants: ["unsafeRaw", "systemIdentity"], reason: MY_MAILBOX_ESCAPE_REASON },
|
|
33
|
+
beforeTransaction: (event, ctx) =>
|
|
34
|
+
checkMailboxLogin(credentialDocumentOf(event.payload), ctx.obs.log),
|
|
35
|
+
handler: async (event, ctx) => {
|
|
36
|
+
const { displayName, address } = event.payload;
|
|
37
|
+
return connectPasswordMailbox(
|
|
38
|
+
"connect-my-mailbox",
|
|
39
|
+
event,
|
|
40
|
+
ctx,
|
|
41
|
+
{ displayName, address, credentialDocument: credentialDocumentOf(event.payload) },
|
|
42
|
+
"user",
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export const FEATURE_NAME = "inbound-provider-imap";
|
|
2
|
-
export
|
|
2
|
+
export { IMAP_PROVIDER_KEY } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
3
3
|
|
|
4
4
|
export const ImapHandlers = {
|
|
5
5
|
connectMailbox: "inbound-provider-imap:write:connect-mailbox",
|
|
6
|
+
connectMyMailbox: "inbound-provider-imap:write:connect-my-mailbox",
|
|
6
7
|
reconnectMailbox: "inbound-provider-imap:write:reconnect-mailbox",
|
|
7
8
|
} as const;
|
|
8
9
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
InboundAuthError,
|
|
24
|
+
InboundCredentialNotStoredError,
|
|
24
25
|
type InboundMailContext,
|
|
25
26
|
inboundCredentialSecretKey,
|
|
26
27
|
type MailAccountRecord,
|
|
@@ -82,7 +83,7 @@ export async function readCredentialDocument(
|
|
|
82
83
|
);
|
|
83
84
|
const secret = await secrets.get(account.tenantId, inboundCredentialSecretKey(account.id));
|
|
84
85
|
if (!secret) {
|
|
85
|
-
throw new
|
|
86
|
+
throw new InboundCredentialNotStoredError(
|
|
86
87
|
`${FEATURE_NAME}: no credential document in secret slot for account ${account.id} — ` +
|
|
87
88
|
`store the IMAP connection JSON under ${inboundCredentialSecretKey(account.id)} first`,
|
|
88
89
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RawInboundMessage,
|
|
3
|
+
RejectedInboundMessage,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
5
|
+
import type { ImapFlow } from "imapflow";
|
|
6
|
+
import { coerceDate, parseOrReject } from "./parse-message.ts";
|
|
7
|
+
|
|
8
|
+
export async function downloadBatch(
|
|
9
|
+
client: ImapFlow,
|
|
10
|
+
batch: readonly number[],
|
|
11
|
+
uidValidity: string,
|
|
12
|
+
trustedAuthservId: string | null,
|
|
13
|
+
): Promise<{
|
|
14
|
+
readonly messages: RawInboundMessage[];
|
|
15
|
+
readonly rejected: RejectedInboundMessage[];
|
|
16
|
+
}> {
|
|
17
|
+
const messages: RawInboundMessage[] = [];
|
|
18
|
+
const rejected: RejectedInboundMessage[] = [];
|
|
19
|
+
if (batch.length === 0) return { messages, rejected };
|
|
20
|
+
for await (const msg of client.fetch(
|
|
21
|
+
batch.join(","),
|
|
22
|
+
{ uid: true, internalDate: true, source: true, envelope: true },
|
|
23
|
+
{ uid: true },
|
|
24
|
+
)) {
|
|
25
|
+
if (!msg.source) continue;
|
|
26
|
+
const parsed = await parseOrReject({
|
|
27
|
+
source: msg.source,
|
|
28
|
+
uid: msg.uid,
|
|
29
|
+
uidValidity,
|
|
30
|
+
internalDate: coerceDate(msg.internalDate ?? undefined),
|
|
31
|
+
envelope: msg.envelope,
|
|
32
|
+
trustedAuthservId,
|
|
33
|
+
});
|
|
34
|
+
if ("message" in parsed) messages.push(parsed.message);
|
|
35
|
+
else rejected.push(parsed.rejected);
|
|
36
|
+
}
|
|
37
|
+
return { messages, rejected };
|
|
38
|
+
}
|