fix(mail-webui): constrain focused profile updates

This commit is contained in:
2026-07-21 20:48:47 +02:00
parent 9f49591a98
commit 2e5921b3ff
3 changed files with 128 additions and 13 deletions

View File

@@ -23,6 +23,22 @@ export type MailProfileChildDescriptor = {
protocol: MailProfileProtocol;
};
export type MailProfileTransportCredentialsLike = {
username?: string | null;
password?: string | null;
};
export type MailProfileTargetedUpdateParts = {
profile: Record<string, unknown>;
smtp: Record<string, unknown>;
imap: Record<string, unknown> | null;
credentials: {
smtp: MailProfileTransportCredentialsLike;
imap: MailProfileTransportCredentialsLike;
};
clearImap: boolean;
};
export function mailProfileChildDescriptors(profile: MailProfileTreeProfileLike): MailProfileChildDescriptor[] {
const children: MailProfileChildDescriptor[] = [
{ kind: "server", id: `server:${profile.id}:smtp`, protocol: "smtp" },
@@ -58,3 +74,42 @@ export function mailProfileEditTargetShowsProfileFields(target: MailProfileEditT
export function mailProfileEditTargetShowsSettingsPanel(target: MailProfileEditTarget): boolean {
return target.kind !== "profile";
}
/**
* Keep the PATCH authority surface equal to the focused editor. Profile and
* server edits must not accidentally replay credential fields merely because
* the form draft contains their displayed values.
*/
export function mailProfileTargetedUpdatePayload(
target: MailProfileEditTarget,
parts: MailProfileTargetedUpdateParts
): Record<string, unknown> {
if (target.kind === "profile") return parts.profile;
if (target.kind === "server") {
if (target.protocol === "smtp") return { smtp: parts.smtp };
return parts.imap === null
? { imap: null, clear_imap: parts.clearImap }
: { imap: parts.imap };
}
if (target.kind === "credentials") {
return { credentials: { [target.protocol]: parts.credentials[target.protocol] } };
}
throw new Error("Create is not an update target");
}
/** Omit blank create credentials so profile-write remains independent. */
export function mailProfileCreateCredentialsPayload(
smtp: MailProfileTransportCredentialsLike,
imap: MailProfileTransportCredentialsLike
): {smtp?: MailProfileTransportCredentialsLike;imap?: MailProfileTransportCredentialsLike;} | undefined {
const populated = (value: MailProfileTransportCredentialsLike): MailProfileTransportCredentialsLike =>
Object.fromEntries(
Object.entries(value).filter(([, item]) => item !== null && item !== undefined && item !== "")
);
const smtpCredentials = populated(smtp);
const imapCredentials = populated(imap);
const result: {smtp?: MailProfileTransportCredentialsLike;imap?: MailProfileTransportCredentialsLike;} = {};
if (Object.keys(smtpCredentials).length > 0) result.smtp = smtpCredentials;
if (Object.keys(imapCredentials).length > 0) result.imap = imapCredentials;
return Object.keys(result).length > 0 ? result : undefined;
}