feat(mail): add governed JMAP mailbox sync and search
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { ActionToolbar, FormGrid, ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailImapFolderMappings, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailImapFolderMappings, type MailImapFolderListResponse, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
|
||||
import { ActionToolbar, FormGrid, ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailImapFolderMappings, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailImapFolderMappings, type MailImapFolderListResponse, type MailJmapTransportSettings, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
|
||||
import { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
updateMailProfilePolicy,
|
||||
testImapSettings,
|
||||
testMailProfileImap,
|
||||
testMailProfileJmap,
|
||||
testMailProfileSmtp,
|
||||
testSmtpSettings,
|
||||
unlinkMailServerCredential,
|
||||
@@ -106,6 +107,15 @@ type ProfileDraft = {
|
||||
imapSentFolder: string;
|
||||
imapFolderMappings: MailImapFolderMappings;
|
||||
imapTimeout: string;
|
||||
jmapSessionUrl: string;
|
||||
jmapAccountId: string;
|
||||
jmapAuthScheme: "bearer" | "basic";
|
||||
jmapAllowedApiOrigins: string;
|
||||
jmapTimeout: string;
|
||||
jmapMaxResponseBytes: string;
|
||||
jmapMaxBodyValueBytes: string;
|
||||
jmapUsername: string;
|
||||
jmapToken: string;
|
||||
};
|
||||
|
||||
type MailProfileScopeManagerProps = {
|
||||
@@ -163,6 +173,7 @@ const MAIL_PROFILE_WORKFLOW_DOCUMENTATION = {
|
||||
const patternLabels: Record<MailProfilePatternKey, string> = {
|
||||
smtp_hosts: "i18n:govoplan-mail.smtp_hostnames.36eb51d8",
|
||||
imap_hosts: "i18n:govoplan-mail.imap_hostnames.ac9c1d78",
|
||||
jmap_hosts: "JMAP hostnames",
|
||||
envelope_senders: "i18n:govoplan-mail.envelope_senders.269065cd",
|
||||
from_headers: "i18n:govoplan-mail.from_headers.b3ea473b",
|
||||
recipient_domains: "i18n:govoplan-mail.recipient_domains.cb9b7b44"
|
||||
@@ -402,7 +413,11 @@ export function MailProfileScopeManager({
|
||||
} else if (editingTarget.kind === "server") {
|
||||
const serverPayload = {
|
||||
name: draft.serverName.trim(),
|
||||
config: editingTarget.protocol === "smtp" ? smtpServerPayload(draft) : imapServerPayload(draft),
|
||||
config: editingTarget.protocol === "smtp"
|
||||
? smtpServerPayload(draft)
|
||||
: editingTarget.protocol === "imap"
|
||||
? imapServerPayload(draft)
|
||||
: jmapServerPayload(draft),
|
||||
inherit_to_lower_scopes: draft.serverInheritToLowerScopes,
|
||||
is_default: draft.serverIsDefault,
|
||||
is_active: draft.serverIsActive
|
||||
@@ -526,7 +541,7 @@ export function MailProfileScopeManager({
|
||||
header: "i18n:govoplan-mail.transport.c10d76c9",
|
||||
width: "minmax(220px, 1fr)",
|
||||
render: (row) => row.kind === "profile" ?
|
||||
<span>{transportLabel(row.profile.smtp)}{row.profile.imap ? i18nMessage("i18n:govoplan-mail.value.48afe802", { value0: transportLabel(row.profile.imap) }) : ""}</span> :
|
||||
<span>{profileTransportLabels(row.profile).join(" · ")}</span> :
|
||||
row.kind === "server" ?
|
||||
<TransportCell server={row.server} /> :
|
||||
<span>{String(row.credential.public_data?.username || "No username")}</span>
|
||||
@@ -660,6 +675,7 @@ export function MailProfileScopeManager({
|
||||
{ id: "edit", label: i18nMessage("i18n:govoplan-mail.edit_value.fad75899", { value0: row.profile.name }), icon: <Pencil size={16} />, disabled: Boolean(profileMutationBlocker), disabledReason: profileMutationBlocker, onClick: () => openEdit(row.profile) },
|
||||
{ id: "add-smtp", label: "Add SMTP server", icon: <Plus size={16} />, disabled: Boolean(profileMutationBlocker), disabledReason: profileMutationBlocker, onClick: () => openCreateServer(row.profile, "smtp") },
|
||||
{ id: "add-imap", label: "Add IMAP server", icon: <Plus size={16} />, disabled: Boolean(profileMutationBlocker), disabledReason: profileMutationBlocker, onClick: () => openCreateServer(row.profile, "imap") },
|
||||
{ id: "add-jmap", label: "Add JMAP server", icon: <Plus size={16} />, disabled: Boolean(profileMutationBlocker), disabledReason: profileMutationBlocker, onClick: () => openCreateServer(row.profile, "jmap") },
|
||||
{ id: "deactivate", label: i18nMessage("i18n:govoplan-mail.deactivate_value.a276a667", { value0: row.profile.name }), icon: <Trash2 size={16} />, variant: "danger", disabled: Boolean(deactivationBlocker), disabledReason: deactivationBlocker, onClick: () => setDeactivating(row.profile) }
|
||||
]} />;
|
||||
|
||||
@@ -1166,8 +1182,9 @@ function ProfileForm({
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [jmapTestResult, setJmapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [imapFolderResult, setImapFolderResult] = useState<MailImapFolderListResponse | null>(null);
|
||||
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null);
|
||||
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "jmap" | "folders" | null>(null);
|
||||
|
||||
const disabled = busy || !canWrite;
|
||||
const credentialDisabled = disabled || !canManageCredentials;
|
||||
@@ -1205,6 +1222,13 @@ function ProfileForm({
|
||||
&& selectedServerId
|
||||
&& !draft.imapPassword
|
||||
);
|
||||
const useSavedJmapTest = Boolean(
|
||||
existingProfile
|
||||
&& (editTarget.kind === "server" || editTarget.kind === "credentials")
|
||||
&& editTarget.protocol === "jmap"
|
||||
&& selectedServerId
|
||||
&& !draft.jmapToken
|
||||
);
|
||||
const creatingCredential = editTarget.kind === "credentials" && !editTarget.credentialId;
|
||||
const creatingNewCredential = creatingCredential && !reuseCredentialId;
|
||||
const moduleReferenceOptions = useMemo<ReferenceOption[]>(
|
||||
@@ -1249,8 +1273,9 @@ function ProfileForm({
|
||||
);
|
||||
const policyMessages = useMemo(() => validateMailPolicy(effectivePolicy, {
|
||||
smtpHost: draft.smtpHost,
|
||||
imapHost: draft.imapHost
|
||||
}), [draft.imapHost, draft.smtpHost, effectivePolicy]);
|
||||
imapHost: draft.imapHost,
|
||||
jmapHost: jmapHostname(draft.jmapSessionUrl)
|
||||
}), [draft.imapHost, draft.jmapSessionUrl, draft.smtpHost, effectivePolicy]);
|
||||
const profileFormMutationBlocker = mailProfileMutationDisabledReason(canWrite, busy);
|
||||
const smtpTestBlocker = profileFormMutationBlocker
|
||||
|| (!draft.smtpHost.trim() ? "Enter an SMTP server hostname before testing the connection." : "");
|
||||
@@ -1260,6 +1285,7 @@ function ProfileForm({
|
||||
useEffect(() => {
|
||||
setSmtpTestResult(null);
|
||||
setImapTestResult(null);
|
||||
setJmapTestResult(null);
|
||||
setImapFolderResult(null);
|
||||
setMailActionState(null);
|
||||
}, [editing, editTarget]);
|
||||
@@ -1346,6 +1372,24 @@ function ProfileForm({
|
||||
}
|
||||
}
|
||||
|
||||
async function runJmapTest() {
|
||||
if (!existingProfile || !selectedServerId) return;
|
||||
setMailActionState("jmap");
|
||||
setJmapTestResult(null);
|
||||
try {
|
||||
setJmapTestResult(await testMailProfileJmap(
|
||||
settings,
|
||||
existingProfile.id,
|
||||
selectedServerId,
|
||||
selectedCredentialId
|
||||
));
|
||||
} catch (err) {
|
||||
setJmapTestResult({ ok: false, protocol: "jmap", message: errorMessage(err), details: {} });
|
||||
} finally {
|
||||
setMailActionState(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mail-profile-form">
|
||||
{!canManageCredentials && (
|
||||
@@ -1359,7 +1403,7 @@ function ProfileForm({
|
||||
details: "Profile metadata and server configuration are separate from encrypted credential authority.",
|
||||
requiredAction: "Ask a Mail credential administrator to create, replace, or link the required credential.",
|
||||
actor: "Mail credential administrator",
|
||||
target: "The credential row beneath the relevant SMTP or IMAP server"
|
||||
target: "The credential row beneath the relevant SMTP, IMAP, or JMAP server"
|
||||
}}
|
||||
documentation={MAIL_PROFILE_DOCUMENTATION} />
|
||||
}
|
||||
@@ -1411,6 +1455,43 @@ function ProfileForm({
|
||||
</FormGrid>
|
||||
}
|
||||
|
||||
{editTarget.kind === "server" && editTarget.protocol === "jmap" &&
|
||||
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||
<FormField label="JMAP Session URL" help="Use the authenticated RFC 8620 Session resource URL; credentials must not be embedded in the URL." documentation={MAIL_PROFILE_DOCUMENTATION}>
|
||||
<input value={draft.jmapSessionUrl} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapSessionUrl: event.target.value })} placeholder="https://mail.example.org/.well-known/jmap" />
|
||||
</FormField>
|
||||
<FormField label="Account id" help="Leave blank to use the primary JMAP Mail account. Configure it when the credential exposes multiple mail accounts.">
|
||||
<input value={draft.jmapAccountId} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapAccountId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Authentication scheme">
|
||||
<select value={draft.jmapAuthScheme} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapAuthScheme: event.target.value as "bearer" | "basic" })}>
|
||||
<option value="bearer">Bearer access token</option>
|
||||
<option value="basic">Basic username and password</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Allowed API origins" help="Optional comma-separated origins for an apiUrl hosted on a different origin than the Session resource. Cross-origin endpoints fail closed unless listed.">
|
||||
<textarea rows={2} value={draft.jmapAllowedApiOrigins} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapAllowedApiOrigins: event.target.value })} placeholder="https://api.example.org" />
|
||||
</FormField>
|
||||
<FormField label="Timeout (seconds)">
|
||||
<input type="number" min="1" max="120" value={draft.jmapTimeout} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapTimeout: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Maximum response bytes">
|
||||
<input type="number" min="65536" max="26214400" value={draft.jmapMaxResponseBytes} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapMaxResponseBytes: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Maximum body value bytes">
|
||||
<input type="number" min="1024" max="5242880" value={draft.jmapMaxBodyValueBytes} disabled={disabled} onChange={(event) => setDraft({ ...draft, jmapMaxBodyValueBytes: event.target.value })} />
|
||||
</FormField>
|
||||
{editTarget.serverId &&
|
||||
<div className="mail-server-field-span">
|
||||
<Button onClick={() => void runJmapTest()} disabled={disabled || mailActionState !== null || (!useSavedJmapTest && !selectedCredentialId)} disabledReason={!useSavedJmapTest && !selectedCredentialId ? "Save or select a JMAP credential before testing." : undefined}>
|
||||
{mailActionState === "jmap" ? "Testing JMAP…" : "Test saved JMAP"}
|
||||
</Button>
|
||||
<MailServerActionResult result={jmapTestResult} />
|
||||
</div>
|
||||
}
|
||||
</FormGrid>
|
||||
}
|
||||
|
||||
{editTarget.kind === "credentials" &&
|
||||
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||
{creatingCredential &&
|
||||
@@ -1469,6 +1550,18 @@ function ProfileForm({
|
||||
onChange={(checked) => setDraft({ ...draft, credentialInheritToLowerScopes: checked })}
|
||||
label="Visible to lower scopes" />
|
||||
</div>
|
||||
{editTarget.protocol === "jmap" &&
|
||||
<>
|
||||
{draft.jmapAuthScheme === "basic" &&
|
||||
<FormField label="JMAP username">
|
||||
<input value={draft.jmapUsername} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, jmapUsername: event.target.value })} autoComplete="username" />
|
||||
</FormField>
|
||||
}
|
||||
<FormField label={draft.jmapAuthScheme === "bearer" ? "JMAP access token" : "JMAP password"} help={selectedCredential?.secret_configured ? "A secret is saved. Leave blank to retain it." : "The secret is encrypted in the governed credential envelope."}>
|
||||
<input type="password" value={draft.jmapToken} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, jmapToken: event.target.value })} autoComplete="new-password" />
|
||||
</FormField>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{reuseCredentialId &&
|
||||
@@ -1574,6 +1667,7 @@ function MailCredentialTreeCell({ credential }: {credential: MailCredentialEnvel
|
||||
}
|
||||
|
||||
function ProfileTransportSummary({ profile }: {profile: MailServerProfile;}) {
|
||||
const jmapServer = preferredProfileServer(profile, "jmap");
|
||||
return (
|
||||
<div className="mail-profile-transport-summary">
|
||||
<div>
|
||||
@@ -1586,6 +1680,11 @@ function ProfileTransportSummary({ profile }: {profile: MailServerProfile;}) {
|
||||
<strong>{transportLabel(profile.imap)}</strong>
|
||||
<small>{profile.imap ? mailCredentialConfigured(profile, "imap") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16" : "i18n:govoplan-mail.not_configured.811931bb"}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>JMAP server</span>
|
||||
<strong>{transportLabel(jmapServer?.config)}</strong>
|
||||
<small>{jmapServer ? jmapServer.credentials.some((credential) => credential.secret_configured) ? "Credential saved" : "No saved credential" : "i18n:govoplan-mail.not_configured.811931bb"}</small>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
@@ -1607,6 +1706,19 @@ function mailCredentialConfigured(profile: MailServerProfile, protocol: "smtp" |
|
||||
return protocol === "smtp" ? profile.smtp_password_configured : profile.imap_password_configured;
|
||||
}
|
||||
|
||||
function preferredProfileServer(profile: MailServerProfile, protocol: MailProfileProtocol): MailServerEndpoint | null {
|
||||
const servers = (profile.servers ?? []).filter((server) => server.protocol === protocol && server.is_active);
|
||||
return servers.find((server) => server.is_default) ?? servers[0] ?? null;
|
||||
}
|
||||
|
||||
function profileTransportLabels(profile: MailServerProfile): string[] {
|
||||
const labels = [`SMTP · ${transportLabel(profile.smtp)}`];
|
||||
if (profile.imap) labels.push(`IMAP · ${transportLabel(profile.imap)}`);
|
||||
const jmap = preferredProfileServer(profile, "jmap");
|
||||
if (jmap) labels.push(`JMAP · ${transportLabel(jmap.config)}`);
|
||||
return labels;
|
||||
}
|
||||
|
||||
function credentialAvailabilityLabel(credential: MailCredentialEnvelope): string {
|
||||
const modules = credential.allowed_modules.length > 0 ? credential.allowed_modules.join(", ") : "all modules";
|
||||
const servers = credential.allowed_server_refs.length > 0
|
||||
@@ -1669,7 +1781,16 @@ function emptyProfileDraft(): ProfileDraft {
|
||||
imapPassword: "",
|
||||
imapSentFolder: "auto",
|
||||
imapFolderMappings: {},
|
||||
imapTimeout: "30"
|
||||
imapTimeout: "30",
|
||||
jmapSessionUrl: "",
|
||||
jmapAccountId: "",
|
||||
jmapAuthScheme: "bearer",
|
||||
jmapAllowedApiOrigins: "",
|
||||
jmapTimeout: "20",
|
||||
jmapMaxResponseBytes: String(5 * 1024 * 1024),
|
||||
jmapMaxBodyValueBytes: String(1024 * 1024),
|
||||
jmapUsername: "",
|
||||
jmapToken: ""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1688,6 +1809,7 @@ function profileToDraft(profile: MailServerProfile, target: MailProfileEditTarge
|
||||
: undefined;
|
||||
const smtpConfig = targetServer?.protocol === "smtp" ? targetServer.config : profile.smtp;
|
||||
const imapConfig = targetServer?.protocol === "imap" ? targetServer.config : profile.imap;
|
||||
const jmapConfig = targetServer?.protocol === "jmap" ? targetServer.config as MailJmapTransportSettings : undefined;
|
||||
const targetUsername = stringValue(targetCredential?.public_data?.username);
|
||||
return {
|
||||
name: profile.name,
|
||||
@@ -1725,7 +1847,16 @@ function profileToDraft(profile: MailServerProfile, target: MailProfileEditTarge
|
||||
...(imapConfig?.folder_mappings ?? {}),
|
||||
...(!imapConfig?.folder_mappings?.sent && imapConfig?.sent_folder && imapConfig.sent_folder !== "auto" ? { sent: imapConfig.sent_folder } : {})
|
||||
}),
|
||||
imapTimeout: stringValue(imapConfig?.timeout_seconds ?? 30)
|
||||
imapTimeout: stringValue(imapConfig?.timeout_seconds ?? 30),
|
||||
jmapSessionUrl: stringValue(jmapConfig?.session_url),
|
||||
jmapAccountId: stringValue(jmapConfig?.account_id),
|
||||
jmapAuthScheme: jmapConfig?.auth_scheme === "basic" ? "basic" : "bearer",
|
||||
jmapAllowedApiOrigins: (jmapConfig?.allowed_api_origins ?? []).join(", "),
|
||||
jmapTimeout: stringValue(jmapConfig?.timeout_seconds ?? 20),
|
||||
jmapMaxResponseBytes: stringValue(jmapConfig?.max_response_bytes ?? 5 * 1024 * 1024),
|
||||
jmapMaxBodyValueBytes: stringValue(jmapConfig?.max_body_value_bytes ?? 1024 * 1024),
|
||||
jmapUsername: targetServer?.protocol === "jmap" && targetCredential ? targetUsername : "",
|
||||
jmapToken: ""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1758,8 +1889,8 @@ function updateProfilePayload(
|
||||
}
|
||||
|
||||
function credentialPayload(draft: ProfileDraft, protocol: MailProfileProtocol, preserveBlankPassword: boolean) {
|
||||
const username = protocol === "smtp" ? draft.smtpUsername : draft.imapUsername;
|
||||
const password = protocol === "smtp" ? draft.smtpPassword : draft.imapPassword;
|
||||
const username = protocol === "smtp" ? draft.smtpUsername : protocol === "imap" ? draft.imapUsername : draft.jmapUsername;
|
||||
const password = protocol === "smtp" ? draft.smtpPassword : protocol === "imap" ? draft.imapPassword : draft.jmapToken;
|
||||
const allowedModules = commaValues(draft.credentialAllowedModules);
|
||||
const allowedServerRefs = commaValues(draft.credentialAllowedServerRefs);
|
||||
const payload: {
|
||||
@@ -1775,7 +1906,7 @@ function credentialPayload(draft: ProfileDraft, protocol: MailProfileProtocol, p
|
||||
} = {
|
||||
name: draft.credentialName.trim(),
|
||||
description: mailTextOrNull(draft.credentialDescription),
|
||||
credential_kind: "username_password",
|
||||
credential_kind: protocol === "jmap" && draft.jmapAuthScheme === "bearer" ? "bearer_token" : "username_password",
|
||||
username: mailTextOrNull(username),
|
||||
inherit_to_lower_scopes: draft.credentialInheritToLowerScopes,
|
||||
allowed_modules: allowedModules.length > 0 ? allowedModules : ["mail"],
|
||||
@@ -1796,13 +1927,13 @@ function profileEditorCanSave(
|
||||
if (editing === "new") return Boolean(draft.name.trim() && draft.smtpHost.trim());
|
||||
if (target.kind === "profile") return Boolean(draft.name.trim());
|
||||
if (target.kind === "server") {
|
||||
const host = target.protocol === "smtp" ? draft.smtpHost : draft.imapHost;
|
||||
const host = target.protocol === "smtp" ? draft.smtpHost : target.protocol === "imap" ? draft.imapHost : draft.jmapSessionUrl;
|
||||
return Boolean(draft.serverName.trim() && host.trim());
|
||||
}
|
||||
if (target.kind === "credentials") {
|
||||
if (!target.serverId) return false;
|
||||
if (!target.credentialId && reuseCredentialId) return true;
|
||||
const password = target.protocol === "smtp" ? draft.smtpPassword : draft.imapPassword;
|
||||
const password = target.protocol === "smtp" ? draft.smtpPassword : target.protocol === "imap" ? draft.imapPassword : draft.jmapToken;
|
||||
return Boolean(draft.credentialName.trim() && (target.credentialId || password));
|
||||
}
|
||||
return false;
|
||||
@@ -1906,6 +2037,18 @@ function imapServerPayload(draft: ProfileDraft): MailImapTestPayload {
|
||||
);
|
||||
}
|
||||
|
||||
function jmapServerPayload(draft: ProfileDraft): MailJmapTransportSettings {
|
||||
return {
|
||||
session_url: draft.jmapSessionUrl.trim(),
|
||||
account_id: mailTextOrNull(draft.jmapAccountId),
|
||||
auth_scheme: draft.jmapAuthScheme,
|
||||
timeout_seconds: boundedInteger(draft.jmapTimeout, 20, 1, 120),
|
||||
max_response_bytes: boundedInteger(draft.jmapMaxResponseBytes, 5 * 1024 * 1024, 64 * 1024, 25 * 1024 * 1024),
|
||||
max_body_value_bytes: boundedInteger(draft.jmapMaxBodyValueBytes, 1024 * 1024, 1024, 5 * 1024 * 1024),
|
||||
allowed_api_origins: commaValues(draft.jmapAllowedApiOrigins)
|
||||
};
|
||||
}
|
||||
|
||||
function profileCredentialsPayload(draft: ProfileDraft, preserveBlankPassword: boolean) {
|
||||
return {
|
||||
smtp: mailTransportCredentialsPayload(draft.smtpUsername, draft.smtpPassword, preserveBlankPassword),
|
||||
@@ -2171,8 +2314,9 @@ function mailPolicySourcePath(scopeType: MailProfileScope): string[] {
|
||||
return ["i18n:govoplan-mail.system.bc0792d8", "i18n:govoplan-mail.tenant.3ca93c78", "i18n:govoplan-mail.owner_policy.1e8df143", "i18n:govoplan-mail.campaign.69390e16"];
|
||||
}
|
||||
|
||||
function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | null | undefined): string {
|
||||
function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | MailJmapTransportSettings | null | undefined): string {
|
||||
if (!transport) return "i18n:govoplan-mail.not_configured.811931bb";
|
||||
if ("session_url" in transport) return transport.session_url || "No Session URL";
|
||||
const host = transport.host || "i18n:govoplan-mail.no_host.4c710d7d";
|
||||
const port = transport.port ? `:${transport.port}` : "";
|
||||
return `${host}${port}`;
|
||||
@@ -2204,6 +2348,20 @@ function commaValues(value: string): string[] {
|
||||
)];
|
||||
}
|
||||
|
||||
function boundedInteger(value: string, fallback: number, minimum: number, maximum: number): number {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.min(maximum, Math.max(minimum, parsed));
|
||||
}
|
||||
|
||||
function jmapHostname(value: string): string {
|
||||
try {
|
||||
return new URL(value).hostname;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user