Text::JSContact - Convert between vCard and JSContact DESCRIPTION Text::JSContact provides bidirectional conversion between vCard (RFC 6350) and JSContact (RFC 9553), following the mapping rules in RFC 9555 and the extensions in RFC 9554. Cards are JSContact version 2.0 (RFC 9982), where uid is optional: a vCard without UID gives a Card without uid. The module handles both standard vCard properties and widely-used vendor extensions, particularly those from Apple's Contacts/AddressBook framework. FUNCTIONS vcard_to_jscontact($vcard_string) Parse a vCard string and return a JSContact Card hashref (version "2.0"; no uid unless the vCard has a UID, per RFC 9982). jscontact_to_vcard($card_hashref) Convert a JSContact Card hashref to a vCard string (version 4.0). The same Card always gives the same bytes. jscontact_to_vcard($card_hashref, original => $vcard_string) Write the Card as an edit of $vcard_string, the vCard it was read from (as fetched from the server: bytes or characters). The Card is compared with what $vcard_string itself reads as, and only the entries that differ are rewritten, in the vCard's own version (a VERSION:3.0 card stays a valid 3.0 card); every other line, including everything JSContact cannot represent, is kept byte for byte. An unchanged Card gives $vcard_string back. The caller does not need the Card it first read, and may change anything in the Card before writing it (add a uid, say): keys that are not Card properties are ignored. If $vcard_string cannot be parsed, the Card is written as a new vCard, as without "original". Returns a character string, like the one-argument form. This is the recommended way to update a contact via CardDAV. patch_vcard($original_vcard_string, $old_card, $new_card) The older form of the same patch: applies the changes between $old_card and $new_card to the original vCard. Returns $original_vcard_string itself when $old_card and $new_card do not differ; otherwise the patched vCard, as bytes if the original was bytes and as characters if it was characters. $original_vcard_string - the raw vCard text as fetched from the server $old_card - the JSContact Card parsed from that vCard $new_card - the modified JSContact Card to write back APPLE VCARD EXTENSIONS Apple's Contacts framework (macOS/iOS) uses several non-standard vCard properties and parameters. These are not part of any RFC but are widely used by Apple clients, iCloud, and third-party applications that need to interoperate with the Apple ecosystem. There is no formal Apple specification for these extensions. Our implementation is based on reverse-engineering Apple-exported vCards and community documentation, including: - iOS full vCard examples from cozy/cozy-vcard test suite https://github.com/cozy/cozy-vcard/blob/master/test/ios-full.vcf - X-ABLabel discussion and behavior analysis https://github.com/mstilkerich/rcmcarddav/issues/310 https://github.com/FossifyOrg/Contacts/issues/187 - Apple group contact format (X-ADDRESSBOOKSERVER-KIND/MEMBER) https://github.com/nextcloud/server/issues/9369 - RFC 9554 standardization of SOCIALPROFILE (replacing X-SOCIALPROFILE) https://datatracker.ietf.org/doc/rfc9554/ - X-ABCROP-RECTANGLE format analysis https://gitlab.com/CardBook/CardBook/-/issues/283 Policy: Reading Apple Extensions When parsing vCards, we recognize and convert the following Apple extensions to their JSContact equivalents: X-ABLabel -> label field on the associated property Parsed from itemN.X-ABLabel grouped with the labeled property. Apple standard labels like _$!!$_ are unwrapped to just "HomePage". X-ADDRESSBOOKSERVER-KIND -> kind (e.g. "group") X-ADDRESSBOOKSERVER-MEMBER -> members (Apple's vCard 3.0 equivalent of the standard KIND + MEMBER properties) X-ABRELATEDNAMES -> relatedTo, with relation type derived from the associated X-ABLabel. Mappings: Mother/Father/Parent -> parent Brother/Sister -> sibling Child -> child Friend -> friend Spouse/Partner -> spouse Assistant/Manager -> colleague X-ABDATE -> anniversaries, with kind derived from X-ABLabel. "Anniversary" label maps to kind "wedding". X-ABADR -> addresses[].countryCode (ISO country code, grouped with the associated ADR property) X-PHONETIC-FIRST-NAME -> name.phoneticComponents (kind: given) X-PHONETIC-MIDDLE-NAME -> name.phoneticComponents (kind: given2) X-PHONETIC-LAST-NAME -> name.phoneticComponents (kind: surname) X-SOCIALPROFILE -> onlineServices (with service from TYPE param and user from x-user param) X-AIM, X-ICQ, X-MSN, X-YAHOO, X-JABBER, X-SKYPE, X-TWITTER, X-GOOGLE-TALK -> onlineServices (legacy IM properties) Policy: Writing Apple Extensions When generating vCards, we write standard vCard 4.0 properties. Apple extensions are generated in the following cases: - When a JSContact property has a "label" field, we generate a grouped itemN.X-ABLabel property alongside the main property. This ensures custom labels survive round-trips through Apple clients. - Standard vCard 4.0 equivalents are used where they exist: KIND instead of X-ADDRESSBOOKSERVER-KIND MEMBER instead of X-ADDRESSBOOKSERVER-MEMBER RELATED instead of X-ABRELATEDNAMES ANNIVERSARY instead of X-ABDATE SOCIALPROFILE instead of X-SOCIALPROFILE (per RFC 9554) - Legacy X-service IM properties (X-AIM, X-SKYPE, etc.) are written as IMPP properties with SERVICE-TYPE parameter. The intent is that vCards generated by this module are valid vCard 4.0 that Apple clients can read, while vCards from Apple clients are fully parsed without data loss. Preserving Unknown Properties (patching) When updating a contact via CardDAV, a naive approach of converting JSContact -> vCard and PUT-ing the result will lose any vCard properties that JSContact does not model. This includes vendor extensions, custom X-properties, and any standard properties we don't yet handle. jscontact_to_vcard(..., original => ...) and patch_vcard() avoid that by patching the original vCard line by line: - a line for an entry the change does not touch keeps its bytes, its group, its folding and its place, as does every line JSContact cannot represent; - a changed entry is written over its own line, keeping its name and group as written, every parameter the reader does not interpret and every TYPE value it does not map; - a removed entry takes its line with it, and its group's X-ABLabel, X-ABADR, GEO and TZ once nothing else is left in the group; - a new entry is added after the lines of the same kind, with a new itemN group (one the original does not use) if it has a label. In a vCard 3.0 card, the lines patched in are vCard 3.0: no PROP-ID, TYPE=pref rather than PREF=1, ENCODING=b for inline data, Apple's X-SERVICE-TYPE, X-USER and X-ADDRESSBOOKSERVER-KIND/MEMBER, and X- forms of the other vCard 4 parameters. Limits: without PROP-IDs (every vCard 3.0 card, many 4.0 ones) the ids of a Card's entries are their positions, so adding or removing an entry renumbers the entries read after it. Phonetic names are read from X-PHONETIC-* but not written, so a change to them is not patched. SPECIFICATIONS RFC 6350 - vCard Format Specification RFC 9553 - JSContact: A JSON Representation of Contact Data RFC 9554 - vCard Format Extensions for JSContact RFC 9555 - JSContact: Converting from and to vCard AUTHOR Bron Gondwana LICENSE Copyright 2026 Fastmail Pty Ltd. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself; that is, either the GNU General Public License as published by the Free Software Foundation (version 1, or at your option any later version), or the Artistic License. See http://dev.perl.org/licenses/ and the LICENSE file included with this distribution for more information.