Contacts as vCard 4: export and import .vcf, through one mapping the DAV plugin can reuse #660
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#660
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the maintainer's list: “Preferable export/import format of contact details is vCard 4. A step closer to official DAV plugin.” The core cannot read or write a vCard. The DAV plugin can, for eight properties, in a copy of its own. This issue puts one mapping in the core, used by a file export and import here and offered on the plugin surface so the plugin deletes its copy and the two cannot disagree.
What exists today
src/forvcard,.vcfandtext/vcardfinds nothing.Contact(jobs/models.py:604-640): company (optional), department,name(one field, 200),role, oneemail,notes; telephone numbers (core/models.py:267, kinds mobile, work, home, switchboard, fax, other; the number as typed, anormalisedinternational form, one primary), postal addresses (:486; kinds home, work, postal, other; street, postcode, municipality, region, a two-letter country, checked against the country's rules since #306) and web links (:638: social, repository, website, and aservicekey into the registry #305 added). A company has a name, website, careers page, location and notes, and no phone, address or email (#329, open). The person's own details are assembled for a CV bycontact_details(documents/rendering.py:151) under the CV's print choices (#308).core/phones.pyreads a number against its country's numbering plan (check,:881, overphonenumberslite), keeps the international form (normalise,:1044), refuses what cannot exist and stores what it cannot read as typed, with a warning (:13-45).postulo-dav/src/postulo_dav/text.py:1-8writes vCard 4.0 by hand:card_to_text(:155) andtext_to_card(:177) handleFN,N,ORG,TITLE,EMAIL, oneTEL,URL,NOTE,UID,REV. It guessesNby splitting the name at its last space, drops the department, always writesVERSION:4.0, and keeps its ownfold,unfoldandescape(:22-60). Its open issues design the rest:KIND:orgcompany cards,ORG:Company;Unitfor a department,ADRas postal addresses, patching a card and never rebuilding it, and a vCard 3 fallback where the book does not list 4.0 (postulo/postulo-dav#8, #9, #10); the pairing key is collection plusUID, never rewritten (#7); a lone carriage return in a name adds properties to the card (#27); an extendedREVis not read (#25).Contact,get_or_create_companyand the primary number and link only; #328 (open) adds companies, departments, contacts and their rows; #329 adds a company's phones, addresses and email; #327 the pairing page.resume/candidate.py) and the spreadsheet import's preview; #659 factors the shared part.Checked against
mainatf09b6010b.Proposal
FN(§6.2.1)N(§6.2.2) is optional in 4.0 and is not invented fromFNORG:Company;Unit(§6.6.4)TITLE(§6.6.1)EMAIL;TYPE=work(§6.4.2)TEL;VALUE=uri;TYPE=…:tel:+…(§6.4.1)cell, workwork, homehome, switchboardwork,voice, faxfax; the primaryPREF=1; the international form, else as typedADR;TYPE=…;CC=PT(§6.3.1)CCfrom RFC 8605 §3.1 beside the country name, in English asphones.country_namehas itURL(§6.7.8)SOCIALPROFILE(RFC 9554) is readNOTE(§6.7.2)KIND:orgcard (§6.1.4):FN,ORG,URLfor the website,NOTEFN,Nfrom the account's first and last name,EMAIL, primaryTELandADR,URLs,TITLEfrom the headline,PRONOUNS(RFC 9554)VERSION:4.0,PRODID,UID,REVAs #679, #681 and #682 land, their values join the mapping:
BDAYandBIRTHPLACE, andGENDER, on the person's own card, andIMPPon every contact's and the person's card, each under the print rule of its own issue. Nothing is written for a field before its issue exists.text/vcard; charset=utf-8,.vcf: one contact, a company's contacts, all contacts, a company card, the person's own card, from the pages where each lives..vcf(3.0 and 4.0 read; 2.1 refused with a sentence) through the review page: each card shown with what it would become and what is not kept (PHOTO,LOGO,KEY,BDAY,X-properties), none ticked by default, since an address book is not a job search and nothing from it should arrive unchosen. Chosen cards become a contact under the companyORGnames, matched by name and never merged silently, or a company forKIND:org; an existing contact is never overwritten.PHOTOorSOURCEaddress is never fetched.text.py.Decisions
N. Options: write it split from the name (as the plugin does); omit it; ask the person. Recommended: omit. A name field has no given and family part, and Portuguese and many other names do not split at the last space. The person's own card, which has first and last names, writes it.UID. A contact has no stored UID, and a pairing needs one that is never rewritten (postulo-dav#7). Recommended: one function derives a stableurn:uuid:from the instance and the contact for file export; the plugin keeps the UID already stored on its link; an importedUIDis used only to spot a repeat inside the file.Prior art
Looked for under the rule of #653, 2026-10-02:
py-vobject/vobject(pushed 2026-08-18); needs pytz, six and python-dateutil, none of them inuv.lock; no OSV advisory. Its documentation says it supports vCard 2.1 and 3.0 and notes 4.0 “has not been universally adopted”; its README says vCard 3.0 files are supported; three requests for vCard 4.0 are open (#29, #113, #124) and its ROADMAP lists “Robust vCard 4.0 support” unticked; there is no 1.0.0 on PyPI. Reject: it does not do the format asked for.text.py,applications/ical.py). The weakness is the copies:ical.pyfixed content-line injection in #218 and the plugin's ownescapenever received it (#27). Recommended: hand-written, in one module the plugin imports; the library check is recorded here, not skipped. Revisit when a maintained library ships vCard 4.0 (watch vobject's #124).Builds on / Needed by
Builds on postulo/postulo-dav#7, #8, #9, #10, #25, #27; #328, #329, #327, #297, #304, #305, #308 and #653, #659. Needed by postulo-dav#8, #9 and #10.
Done when
tests/test_vcard.py(new): each row of the table;Nnever invented;ORG:Acme;Legal;KIND:org; folding at 75 octets never inside a character; 3.0 and 4.0 read; a lone\r,\nor other control character in a name, note, email or URL adds no property (the test postulo-dav#27 asks for, at the source); a round trip loses nothing the table says is kept.tests/security/test_vcard.py(new): a hostile file is bounded in size, cards and line length; aPHOTO;VALUE=urimakes no request (postulo.plugins.httpis patched to fail); nothing names another account's record.tests/e2e/test_vcard.py(new): export a contact, import it into a second account through the review page with nothing ticked, then ticked. New names onpostulo.plugins.apiget their line in Writing a plugin (tests/test_wiki_surface.py) and a test intests/test_plugin_surface.py; new pages jointests/test_page_coverage.pyandtests/security/test_isolation_sweep.py.From the list of improvements of 2026-10-02.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#660).