fix: emit RFC 9553 name kinds and decode QUOTED-PRINTABLE in vCard import #224 #187

This commit is contained in:
Linus Rath
2026-04-26 02:47:19 +02:00
parent 9f8588eadc
commit aadf56c27b
3 changed files with 154 additions and 21 deletions
+10 -7
View File
@@ -147,7 +147,9 @@ export function ContactForm({ contact, addressBooks, allKeywords, onSave, onCanc
const t = useTranslations("contacts.form");
const isEditing = !!contact;
const findComponent = (kind: string) => contact?.name?.components?.find(c => c.kind === kind)?.value || "";
// Accept JSContact-standard kinds (RFC 9553) and legacy vCard-style aliases.
const findComponent = (...kinds: string[]) =>
contact?.name?.components?.find(c => kinds.includes(c.kind))?.value || "";
// Convert RFC 9553 AnniversaryDate to ISO date string for HTML date input
function anniversaryDateToString(date: AnniversaryDate): string {
@@ -210,11 +212,11 @@ export function ContactForm({ contact, addressBooks, allKeywords, onSave, onCanc
};
}
const [prefix, setPrefix] = useState(findComponent("prefix"));
const [prefix, setPrefix] = useState(findComponent("title", "prefix"));
const [givenName, setGivenName] = useState(findComponent("given"));
const [additionalName, setAdditionalName] = useState(findComponent("additional"));
const [additionalName, setAdditionalName] = useState(findComponent("given2", "additional", "middle"));
const [surname, setSurname] = useState(findComponent("surname"));
const [suffix, setSuffix] = useState(findComponent("suffix"));
const [suffix, setSuffix] = useState(findComponent("generation", "suffix"));
const [nickname, setNickname] = useState(
contact?.nicknames ? Object.values(contact.nicknames)[0]?.name || "" : ""
@@ -436,12 +438,13 @@ export function ContactForm({ contact, addressBooks, allKeywords, onSave, onCanc
phonesMap[`p${i}`] = obj;
});
// Emit JSContact-standard kinds (RFC 9553) so the JMAP server stores them losslessly.
const nameComponents = [];
if (prefix.trim()) nameComponents.push({ kind: "prefix" as const, value: prefix.trim() });
if (prefix.trim()) nameComponents.push({ kind: "title" as const, value: prefix.trim() });
if (givenName.trim()) nameComponents.push({ kind: "given" as const, value: givenName.trim() });
if (additionalName.trim()) nameComponents.push({ kind: "additional" as const, value: additionalName.trim() });
if (additionalName.trim()) nameComponents.push({ kind: "given2" as const, value: additionalName.trim() });
if (surname.trim()) nameComponents.push({ kind: "surname" as const, value: surname.trim() });
if (suffix.trim()) nameComponents.push({ kind: "suffix" as const, value: suffix.trim() });
if (suffix.trim()) nameComponents.push({ kind: "generation" as const, value: suffix.trim() });
const titlesMap: Record<string, { name: string; kind?: "title" | "role" }> = {};
if (jobTitle.trim()) titlesMap["t0"] = { name: jobTitle.trim(), kind: "title" };