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. 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. jscontact_to_vcard($card_hashref) Convert a JSContact Card hashref to a vCard string (version 4.0). patch_vcard($original_vcard_string, $old_card, $new_card) Apply changes to a vCard while preserving unrecognized properties and vendor extensions. Compares $old_card and $new_card to determine what changed, then applies only those changes to the original vCard string. This is the recommended way to update contacts via CardDAV, because it preserves Apple extensions, custom X-properties, and any other data in the vCard that JSContact does not model. $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 Returns a new vCard string with minimal changes applied. 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 (patch_vcard) 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. The patch_vcard() function solves this by: 1. Parsing the original vCard to identify all properties 2. Comparing old_card and new_card to find what actually changed 3. Modifying only the changed properties in the original vCard 4. Preserving everything else verbatim This is important for interoperability with Apple and other clients that store data in properties we may not understand. 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 Same as Perl itself (Artistic License 2.0)