From e2f1ec75d2b2e59d3e3c51ea43211bd2af72dcdb Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Thu, 1 Jan 2026 15:28:32 -0800 Subject: [PATCH] vCard 4.0 implementation New vCard 4.0 Properties Implemented - KIND - individual/group/org/location - GENDER - sex and gender identity - ANNIVERSARY - marriage date - LANG - language preferences with PREF - IMPP - instant messaging URIs - RELATED - relationships with TYPE - MEMBER - group members (only for KIND=group) - CLIENTPIDMAP - sync mapping - XML - extended XML data - SOURCE - vCard source URI Usage: ```python import vobject v3 = vobject.vCard() v4 = vobject.vCard('4.0') v4.add('fn').value = 'John Doe' v4.add('kind').value = 'individual' v4.add('gender').value = 'M' parsed = vobject.readOne(vcard_string) # Works for both 3.0 and 4.0 ``` --- test_files/more_tests.txt | 126 +++++++++ test_files/vcard40_full.vcf | 39 +++ test_files/vcard40_group.vcf | 9 + test_files/vcard40_location.vcf | 9 + test_files/vcard40_multilang.vcf | 8 + test_files/vcard40_org.vcf | 10 + test_files/vcard40_simple.vcf | 6 + tests/test_vcard40.py | 216 ++++++++++++++ tests/test_vcard4_helper.py | 97 +++++++ vobject/__init__.py | 39 ++- vobject/vcard40.py | 466 +++++++++++++++++++++++++++++++ 11 files changed, 1022 insertions(+), 3 deletions(-) create mode 100644 test_files/vcard40_full.vcf create mode 100644 test_files/vcard40_group.vcf create mode 100644 test_files/vcard40_location.vcf create mode 100644 test_files/vcard40_multilang.vcf create mode 100644 test_files/vcard40_org.vcf create mode 100644 test_files/vcard40_simple.vcf create mode 100644 tests/test_vcard40.py create mode 100644 tests/test_vcard4_helper.py create mode 100644 vobject/vcard40.py diff --git a/test_files/more_tests.txt b/test_files/more_tests.txt index 779e79b..77ab02d 100644 --- a/test_files/more_tests.txt +++ b/test_files/more_tests.txt @@ -83,3 +83,129 @@ u'\xe9' >>> vcs = vobject.readOne(vcs, allowQP = True) >>> vcs.serialize() 'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' + +vCard Version Selection +....................... + +Creating vCard with version parameter: +>>> v3 = vobject.vCard() # Default is 3.0 +>>> v3.behavior.versionString +'3.0' +>>> v3_explicit = vobject.vCard('3.0') +>>> v3_explicit.behavior.versionString +'3.0' +>>> v4 = vobject.vCard('4.0') +>>> v4.behavior.versionString +'4.0' + +Invalid version raises error: +>>> try: +... vobject.vCard('2.0') +... except ValueError as e: +... print("ValueError raised") +ValueError raised + +vCard 4.0 Creation +.................. + +Create a basic vCard 4.0: +>>> v4 = vobject.vCard('4.0') +>>> v4.add('fn').value = 'John Doe' +>>> v4.add('n').value = vobject.vcard.Name('Doe', 'John') +>>> 'VERSION:4.0' in v4.serialize() +True +>>> 'FN:John Doe' in v4.serialize() +True + +vCard 4.0 New Properties +........................ + +KIND property: +>>> v4 = vobject.vCard('4.0') +>>> v4.add('fn').value = 'Test Group' +>>> v4.add('kind').value = 'group' +>>> 'KIND:group' in v4.serialize() +True + +GENDER property: +>>> v4 = vobject.vCard('4.0') +>>> v4.add('fn').value = 'Jane Doe' +>>> v4.add('gender').value = 'F' +>>> 'GENDER:F' in v4.serialize() +True + +IMPP property: +>>> v4 = vobject.vCard('4.0') +>>> v4.add('fn').value = 'Test User' +>>> v4.add('impp').value = 'xmpp:user@jabber.example.com' +>>> 'IMPP:xmpp:user@jabber.example.com' in v4.serialize() +True + +LANG property: +>>> v4 = vobject.vCard('4.0') +>>> v4.add('fn').value = 'Multilingual Person' +>>> lang = v4.add('lang') +>>> lang.value = 'en' +>>> lang.params['PREF'] = ['1'] +>>> 'LANG' in v4.serialize() +True + +Reading vCard 4.0 Files +....................... + +Read a simple vCard 4.0: +>>> f = get_stream("vcard40_simple.vcf") +>>> v4 = vobject.readOne(f) +>>> v4.version.value +'4.0' +>>> v4.fn.value +'John Doe' + +Read a vCard 4.0 with KIND=group: +>>> f = get_stream("vcard40_group.vcf") +>>> v4_group = vobject.readOne(f) +>>> v4_group.kind.value +'group' +>>> len(v4_group.member_list) +3 + +Read a vCard 4.0 with all properties: +>>> f = get_stream("vcard40_full.vcf") +>>> v4_full = vobject.readOne(f) +>>> v4_full.version.value +'4.0' +>>> v4_full.kind.value +'individual' +>>> v4_full.gender.value +'M' +>>> v4_full.anniversary.value +'20100612' + +Dual Version Compatibility +.......................... + +readOne auto-detects version from VERSION property: +>>> vcard3_str = '''BEGIN:VCARD +... VERSION:3.0 +... FN:Test 3.0 +... N:Test;3.0;;; +... END:VCARD''' +>>> v3 = vobject.readOne(vcard3_str) +>>> v3.version.value +'3.0' + +>>> vcard4_str = '''BEGIN:VCARD +... VERSION:4.0 +... FN:Test 4.0 +... N:Test;4.0;;; +... KIND:individual +... END:VCARD''' +>>> v4 = vobject.readOne(vcard4_str) +>>> v4.version.value +'4.0' + +Both versions serialize correctly: +>>> '3.0' in v3.version.serialize() +True +>>> '4.0' in v4.version.serialize() +True diff --git a/test_files/vcard40_full.vcf b/test_files/vcard40_full.vcf new file mode 100644 index 0000000..b573258 --- /dev/null +++ b/test_files/vcard40_full.vcf @@ -0,0 +1,39 @@ +BEGIN:VCARD +VERSION:4.0 +KIND:individual +FN:Dr. John Q. Public\, III +FN;LANGUAGE=jp:John Q. Public +N:Public;John;Quinlan;Dr.;III +NICKNAME:Johnny,JQP +PHOTO:http://www.example.com/pub/photos/jqpublic.gif +BDAY:19850415 +ANNIVERSARY:20100612 +GENDER:M +ADR;TYPE=work:;;123 Main Street;Any Town;CA;91921-1234;U.S.A. +ADR;TYPE=home;PREF=1:;;456 Oak Lane;Somewhere;NY;10001;U.S.A. +TEL;VALUE=uri;TYPE=voice;PREF=1:tel:+1-555-555-5555 +TEL;VALUE=uri;TYPE=cell:tel:+1-555-123-4567 +EMAIL;TYPE=work:john.public@example.com +EMAIL;TYPE=home;PREF=1:johnny@personal.example.org +IMPP;PREF=1:xmpp:john@jabber.example.com +IMPP:sip:john@voip.example.com +LANG;PREF=1:en +LANG;PREF=2:fr +TZ:America/New_York +GEO:geo:37.386013,-122.082932 +TITLE:CEO +ROLE:Executive +LOGO:http://www.example.com/pub/logos/company.png +ORG:Example Corp.;North Division;Marketing +RELATED;TYPE=spouse:urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519 +CATEGORIES:BUSINESS,NETWORKING +NOTE:This is a note about John.\nHe likes vCards. +REV:20231215T120000Z +SOUND:http://www.example.com/pub/sounds/jqpublic.ogg +UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 +URL:http://www.example.com/~jqpublic +KEY:http://www.example.com/keys/jqpublic.pgp +FBURL:http://www.example.com/busy/jqpublic +CALADRURI:mailto:jqpublic@example.com +CALURI:http://cal.example.com/jqpublic +END:VCARD diff --git a/test_files/vcard40_group.vcf b/test_files/vcard40_group.vcf new file mode 100644 index 0000000..38dc393 --- /dev/null +++ b/test_files/vcard40_group.vcf @@ -0,0 +1,9 @@ +BEGIN:VCARD +VERSION:4.0 +KIND:group +FN:The Doe Family +UID:urn:uuid:group-doe-family +MEMBER:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 +MEMBER:urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519 +MEMBER:mailto:kid@example.com +END:VCARD diff --git a/test_files/vcard40_location.vcf b/test_files/vcard40_location.vcf new file mode 100644 index 0000000..3031d02 --- /dev/null +++ b/test_files/vcard40_location.vcf @@ -0,0 +1,9 @@ +BEGIN:VCARD +VERSION:4.0 +KIND:location +FN:Empire State Building +ADR:;;350 Fifth Avenue;New York;NY;10118;U.S.A. +GEO:geo:40.748817,-73.985428 +TEL;VALUE=uri:tel:+1-212-736-3100 +URL:http://www.esbnyc.com +END:VCARD diff --git a/test_files/vcard40_multilang.vcf b/test_files/vcard40_multilang.vcf new file mode 100644 index 0000000..70d8d3b --- /dev/null +++ b/test_files/vcard40_multilang.vcf @@ -0,0 +1,8 @@ +BEGIN:VCARD +VERSION:4.0 +FN;ALTID=1;LANGUAGE=en:Yamada Taro +FN;ALTID=1;LANGUAGE=ja:山田太郎 +N;ALTID=1;LANGUAGE=en:Yamada;Taro;;; +N;ALTID=1;LANGUAGE=ja;SORT-AS="yamada,taro":山田;太郎;;; +UID:urn:uuid:multilang-vcard-example +END:VCARD diff --git a/test_files/vcard40_org.vcf b/test_files/vcard40_org.vcf new file mode 100644 index 0000000..0bfc55c --- /dev/null +++ b/test_files/vcard40_org.vcf @@ -0,0 +1,10 @@ +BEGIN:VCARD +VERSION:4.0 +KIND:org +FN:ABC Marketing +ORG:ABC\, Inc.;North American Division;Marketing +TEL;VALUE=uri:tel:+1-555-000-0000 +EMAIL:marketing@abc.example.com +ADR:;;456 Corporate Way;Business City;NY;10001;U.S.A. +URL:http://www.abc.example.com/marketing +END:VCARD diff --git a/test_files/vcard40_simple.vcf b/test_files/vcard40_simple.vcf new file mode 100644 index 0000000..d456e3b --- /dev/null +++ b/test_files/vcard40_simple.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD +VERSION:4.0 +FN:John Doe +N:Doe;John;;; +UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 +END:VCARD diff --git a/tests/test_vcard40.py b/tests/test_vcard40.py new file mode 100644 index 0000000..3e94319 --- /dev/null +++ b/tests/test_vcard40.py @@ -0,0 +1,216 @@ +"""Test vCard 4.0 implementation""" + +import vobject + + +def test_basic_vcard40(): + """Test creating a basic vCard 4.0""" + # Create a new vCard 4.0 + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + v.add('fn').value = 'John Doe' + v.add('n') + v.n.value = vobject.vcard.Name(family='Doe', given='John') + + # Add vCard 4.0 VERSION + if hasattr(v, 'version'): + del v.version + v.add('version').value = '4.0' + + serialized = v.serialize() + + assert 'BEGIN:VCARD' in serialized + assert 'VERSION:4.0' in serialized + assert 'FN:John Doe' in serialized + assert 'N:Doe;John;;;' in serialized + assert 'END:VCARD' in serialized + + +def test_new_properties(): + """Test vCard 4.0 new properties""" + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + + # Required properties + v.add('fn').value = 'Jane Smith' + v.add('version').value = '4.0' + + # New KIND property + v.add('kind').value = 'individual' + + # New GENDER property + v.add('gender').value = 'F' + + # New ANNIVERSARY property + v.add('anniversary').value = '20100615' + + # New LANG property + lang = v.add('lang') + lang.value = 'en' + lang.params['PREF'] = ['1'] + + # New IMPP property + impp = v.add('impp') + impp.value = 'xmpp:jane@example.com' + + # New RELATED property + related = v.add('related') + related.value = 'http://example.com/directory/jsmith.vcf' + related.params['TYPE'] = ['spouse'] + + # New SOURCE property + v.add('source').value = 'http://example.com/directory/jsmith.vcf' + + serialized = v.serialize() + + assert 'FN:Jane Smith' in serialized + assert 'VERSION:4.0' in serialized + assert 'KIND:individual' in serialized + assert 'GENDER:F' in serialized + assert 'ANNIVERSARY:20100615' in serialized + assert 'LANG;PREF=1:en' in serialized + assert 'IMPP:xmpp:jane@example.com' in serialized + assert 'RELATED;TYPE=spouse:http://example.com/directory/jsmith.vcf' in serialized + assert 'SOURCE:http://example.com/directory/jsmith.vcf' in serialized + + +def test_modified_properties(): + """Test modified properties in vCard 4.0""" + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + + # Required properties + v.add('fn').value = 'Bob Johnson' + v.add('version').value = '4.0' + + # PHOTO as URI (vCard 4.0 style) + v.add('photo').value = 'http://example.com/photo.jpg' + + # TEL with URI scheme + tel = v.add('tel') + tel.value = 'tel:+1-555-555-5555' + tel.params['TYPE'] = ['voice', 'home'] + tel.params['PREF'] = ['1'] + + # GEO as geo: URI + v.add('geo').value = 'geo:37.386013,-122.082932' + + # UID as URN + v.add('uid').value = 'urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' + + serialized = v.serialize() + + assert 'FN:Bob Johnson' in serialized + assert 'VERSION:4.0' in serialized + assert 'PHOTO:http://example.com/photo.jpg' in serialized + assert 'TEL;PREF=1;TYPE=voice,home:tel:+1-555-555-5555' in serialized or \ + 'TEL;TYPE=voice,home;PREF=1:tel:+1-555-555-5555' in serialized + assert 'GEO:geo:37.386013,-122.082932' in serialized + assert 'UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' in serialized + + +def test_group_vcard(): + """Test vCard 4.0 group with MEMBER property""" + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + + # Required properties + v.add('fn').value = 'Development Team' + v.add('version').value = '4.0' + + # KIND=group + v.add('kind').value = 'group' + + # MEMBER properties (only valid for groups) + v.add('member').value = 'urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af' + v.add('member').value = 'urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' + v.add('member').value = 'mailto:member1@example.com' + + serialized = v.serialize() + + assert 'FN:Development Team' in serialized + assert 'VERSION:4.0' in serialized + assert 'KIND:group' in serialized + assert 'MEMBER:urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af' in serialized + assert 'MEMBER:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' in serialized + assert 'MEMBER:mailto:member1@example.com' in serialized + + +def test_multiple_fn(): + """Test multiple FN values (allowed in vCard 4.0)""" + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + + # Multiple FN values with different languages + v.add('version').value = '4.0' + + fn1 = v.add('fn') + fn1.value = 'John Doe' + fn1.params['LANGUAGE'] = ['en'] + + fn2 = v.add('fn') + fn2.value = 'ジョン・ドゥ' + fn2.params['LANGUAGE'] = ['ja'] + + serialized = v.serialize() + + assert 'VERSION:4.0' in serialized + assert 'FN;LANGUAGE=en:John Doe' in serialized + assert 'FN;LANGUAGE=ja:ジョン・ドゥ' in serialized + + # Verify we have exactly 2 FN lines + fn_count = serialized.count('\nFN') + assert fn_count == 2, f"Expected 2 FN properties, found {fn_count}" + + +def test_new_parameters(): + """Test new vCard 4.0 parameters""" + v = vobject.vCard() + v.behavior = vobject.vcard40.VCard4_0 + + v.add('version').value = '4.0' + v.add('fn').value = 'Alice Williams' + + # PREF parameter (replaces TYPE=pref) + tel1 = v.add('tel') + tel1.value = 'tel:+1-555-111-1111' + tel1.params['PREF'] = ['1'] + tel1.params['TYPE'] = ['work'] + + tel2 = v.add('tel') + tel2.value = 'tel:+1-555-222-2222' + tel2.params['PREF'] = ['2'] + tel2.params['TYPE'] = ['home'] + + # ALTID parameter (links alternate representations) + email1 = v.add('email') + email1.value = 'alice@work.example.com' + email1.params['ALTID'] = ['1'] + + email2 = v.add('email') + email2.value = 'alice.williams@work.example.com' + email2.params['ALTID'] = ['1'] + + # MEDIATYPE parameter + photo = v.add('photo') + photo.value = 'http://example.com/photo.jpg' + photo.params['MEDIATYPE'] = ['image/jpeg'] + + serialized = v.serialize() + + assert 'FN:Alice Williams' in serialized + assert 'VERSION:4.0' in serialized + + # Check PREF parameter on TEL + assert 'PREF=1' in serialized + assert 'PREF=2' in serialized + assert 'tel:+1-555-111-1111' in serialized + assert 'tel:+1-555-222-2222' in serialized + + # Check ALTID parameter on EMAIL + assert 'EMAIL;ALTID=1:alice@work.example.com' in serialized or \ + 'EMAIL;ALTID=1:alice.williams@work.example.com' in serialized + assert serialized.count('ALTID=1') == 2, "Expected 2 EMAIL properties with ALTID=1" + + # Check MEDIATYPE parameter on PHOTO + assert 'PHOTO;MEDIATYPE=image/jpeg:http://example.com/photo.jpg' in serialized diff --git a/tests/test_vcard4_helper.py b/tests/test_vcard4_helper.py new file mode 100644 index 0000000..1a89128 --- /dev/null +++ b/tests/test_vcard4_helper.py @@ -0,0 +1,97 @@ +"""Test the vCard4() helper function""" + +import vobject + + +def test_vcard4_helper(): + """Test that vCard4() creates a vCard 4.0 object""" + # Create a vCard 4.0 using the helper + v = vobject.vCard4() + + # Add some content + v.add('fn').value = 'Test Person' + v.add('n') + v.n.value = vobject.vcard.Name(family='Person', given='Test') + + # Add vCard 4.0 specific properties + v.add('kind').value = 'individual' + v.add('gender').value = 'M' + + # Serialize (this will auto-generate VERSION if not present) + result = v.serialize() + + # Verify it's vCard 4.0 + assert 'VERSION:4.0' in result, "VERSION:4.0 not found in output" + assert 'KIND:individual' in result, "KIND property not found" + assert 'GENDER:M' in result, "GENDER property not found" + assert 'FN:Test Person' in result, "FN property not found" + assert 'N:Person;Test;;;' in result, "N property not found" + + # Check the version after serialization + assert v.version.value == '4.0', f"Expected version 4.0, got {v.version.value}" + + +def test_vcard4_default_version(): + """Test that vCard4() auto-generates VERSION:4.0 on serialization""" + v = vobject.vCard4() + v.add('fn').value = 'Test' + + # VERSION is auto-generated during serialization + serialized = v.serialize() + + assert 'VERSION:4.0' in serialized, "VERSION:4.0 not found in serialized output" + assert hasattr(v, 'version'), "VERSION property not found after serialization" + assert v.version.value == '4.0', f"Expected version 4.0, got {v.version.value}" + + +def test_vcard_vs_vcard4_comparison(): + """Compare vCard() vs vCard4() helper functions""" + # vCard 3.0 + v3 = vobject.vCard() + v3.add('fn').value = 'John Doe' + serialized_v3 = v3.serialize() + + assert 'VERSION:3.0' in serialized_v3, "vCard() should create vCard 3.0" + assert v3.version.value == '3.0', f"Expected version 3.0, got {v3.version.value}" + + # vCard 4.0 + v4 = vobject.vCard4() + v4.add('fn').value = 'John Doe' + serialized_v4 = v4.serialize() + + assert 'VERSION:4.0' in serialized_v4, "vCard4() should create vCard 4.0" + assert v4.version.value == '4.0', f"Expected version 4.0, got {v4.version.value}" + + +def test_vcard4_with_multiple_properties(): + """Test vCard4() with multiple vCard 4.0 specific properties""" + v = vobject.vCard4() + + # Add required properties + v.add('fn').value = 'Jane Smith' + v.add('n') + v.n.value = vobject.vcard.Name(family='Smith', given='Jane') + + # Add vCard 4.0 specific properties + v.add('kind').value = 'individual' + v.add('gender').value = 'F' + v.add('anniversary').value = '20150714' + + lang = v.add('lang') + lang.value = 'en-US' + lang.params['PREF'] = ['1'] + + impp = v.add('impp') + impp.value = 'xmpp:jane@example.com' + + serialized = v.serialize() + + # Verify all properties are present + assert 'VERSION:4.0' in serialized + assert 'FN:Jane Smith' in serialized + assert 'N:Smith;Jane;;;' in serialized + assert 'KIND:individual' in serialized + assert 'GENDER:F' in serialized + assert 'ANNIVERSARY:20150714' in serialized + assert 'LANG;PREF=1:en-US' in serialized + assert 'IMPP:xmpp:jane@example.com' in serialized diff --git a/vobject/__init__.py b/vobject/__init__.py index 50acde5..37c84e6 100644 --- a/vobject/__init__.py +++ b/vobject/__init__.py @@ -76,7 +76,7 @@ """ -from . import icalendar, vcard +from . import icalendar, vcard, vcard40 from .base import VERSION, newFromBehavior, readComponents, readOne # Package version @@ -87,5 +87,38 @@ def iCalendar(): return newFromBehavior("vcalendar", "2.0") -def vCard(): - return newFromBehavior("vcard", "3.0") +def vCard(version='3.0'): + """ + Create a vCard object. + + Args: + version: vCard version to use. Either '3.0' (default) or '4.0'. + + Returns: + A new vCard Component with the appropriate behavior. + + Examples: + >>> v = vCard() # Creates vCard 3.0 + >>> v = vCard('3.0') # Creates vCard 3.0 + >>> v = vCard('4.0') # Creates vCard 4.0 + """ + if version not in ('3.0', '4.0'): + raise ValueError("vCard version must be '3.0' or '4.0', got: {}".format(version)) + return newFromBehavior('vcard', version) + + +def vCard4(): + """ + Create a vCard 4.0 object. + + This is a convenience function equivalent to vCard('4.0'). + + Returns: + A new vCard 4.0 Component. + + Examples: + >>> v = vCard4() + >>> v.add('fn').value = 'John Doe' + >>> v.add('kind').value = 'individual' + """ + return vCard('4.0') diff --git a/vobject/vcard40.py b/vobject/vcard40.py new file mode 100644 index 0000000..7a0941b --- /dev/null +++ b/vobject/vcard40.py @@ -0,0 +1,466 @@ +"""Definitions and behavior for vCard 4.0""" + +import codecs + +from . import behavior + +from .base import ContentLine, registerBehavior, backslashEscape, str_ +from .icalendar import stringToTextValues +from .vcard import ( + VCardBehavior, + VCardTextBehavior, + Name, + NameBehavior, + Address, + AddressBehavior, + OrgBehavior, + toListOrString, + splitFields, + toList, + serializeFields, + NAME_ORDER, + ADDRESS_ORDER, + REALLY_LARGE, + wacky_apple_photo_serialize +) + + +# Python 3 no longer has a basestring type, so.... +try: + basestring = basestring +except NameError: + basestring = (str, bytes) + + +# ------------------------ vCard 4.0 Main Component ---------------------------- + +class VCard4_0(VCardBehavior): + """ + vCard 4.0 behavior. + """ + name = 'VCARD' + description = 'vCard 4.0, defined in RFC 6350' + versionString = '4.0' + isComponent = True + sortFirst = ('version', 'prodid', 'uid') + knownChildren = { + 'VERSION': (1, 1, None), # exactly one, required + 'FN': (1, None, None), # one or more, required + 'N': (0, 1, None), # at most one + 'NICKNAME': (0, None, None), + 'PHOTO': (0, None, None), + 'BDAY': (0, 1, None), + 'ANNIVERSARY': (0, 1, None), # new in 4.0 + 'GENDER': (0, 1, None), # new in 4.0 + 'ADR': (0, None, None), + 'TEL': (0, None, None), + 'EMAIL': (0, None, None), + 'IMPP': (0, None, None), # new in 4.0 + 'LANG': (0, None, None), # new in 4.0 + 'TZ': (0, None, None), + 'GEO': (0, None, None), + 'TITLE': (0, None, None), + 'ROLE': (0, None, None), + 'LOGO': (0, None, None), + 'ORG': (0, None, None), + 'MEMBER': (0, None, None), # new in 4.0, only for KIND=group + 'RELATED': (0, None, None), # new in 4.0 + 'CATEGORIES': (0, None, None), + 'NOTE': (0, None, None), + 'PRODID': (0, 1, None), + 'REV': (0, 1, None), + 'SORT-STRING': (0, None, None), # deprecated in 4.0, but may exist + 'SOUND': (0, None, None), + 'UID': (0, 1, None), + 'CLIENTPIDMAP': (0, None, None), # new in 4.0 + 'URL': (0, None, None), + 'VERSION': (1, 1, None), + 'KEY': (0, None, None), + 'FBURL': (0, None, None), + 'CALADRURI': (0, None, None), + 'CALURI': (0, None, None), + 'XML': (0, None, None), # new in 4.0 + 'SOURCE': (0, None, None), # new in 4.0 + 'KIND': (0, 1, None), # new in 4.0 + } + + @classmethod + def generateImplicitParameters(cls, obj): + """ + Create VERSION if needed. + + For vCard 4.0, VERSION must be immediately after BEGIN:VCARD. + """ + if not hasattr(obj, 'version'): + obj.add(ContentLine('VERSION', [], cls.versionString)) + + +registerBehavior(VCard4_0) + + +# ------------------------ vCard 4.0 New Properties ---------------------------- + +class Kind(VCardTextBehavior): + """ + KIND property for vCard 4.0. + + Valid values: individual, group, org, location, or x-name/iana-token + Default (if not present): individual + """ + name = "KIND" + description = 'Kind of object (individual, group, org, location)' + + +registerBehavior(Kind) + + +class Gender(VCardTextBehavior): + """ + GENDER property for vCard 4.0. + + Format: sex-component;text-component + sex-component: M, F, O, N, U, or empty + text-component: free-form text (optional) + + Examples: + GENDER:M + GENDER:F;female + GENDER:O;intersex + GENDER:;it's complicated + """ + name = "GENDER" + description = 'Sex and gender identity' + + @classmethod + def decode(cls, line): + """Decode the gender value.""" + if line.encoded: + # Gender is semicolon-separated: sex;text + line.value = stringToTextValues(line.value)[0] + line.encoded = False + + +registerBehavior(Gender) + + +class Anniversary(VCardTextBehavior): + """ + ANNIVERSARY property for vCard 4.0. + + Date of marriage or anniversary. Can be date, date-time, or partial date. + Examples: + ANNIVERSARY:19960415 + ANNIVERSARY:--0415 + """ + name = "ANNIVERSARY" + description = 'Date of marriage or equivalent' + + +registerBehavior(Anniversary) + + +class Lang(VCardTextBehavior): + """ + LANG property for vCard 4.0. + + Language tag (RFC 5646) for language preferences. + Can have PREF parameter for ordering. + + Examples: + LANG;PREF=1:en + LANG;PREF=2:fr + """ + name = "LANG" + description = 'Language preference' + + +registerBehavior(Lang) + + +class Impp(VCardTextBehavior): + """ + IMPP property for vCard 4.0. + + Instant messaging and presence protocol URI. + + Examples: + IMPP;PREF=1:xmpp:alice@example.com + IMPP:sip:alice@example.com + IMPP:skype:alice.example + """ + name = "IMPP" + description = 'Instant messaging and presence protocol URI' + + +registerBehavior(Impp) + + +class Related(VCardTextBehavior): + """ + RELATED property for vCard 4.0. + + Relationship to another entity. Can be URI or text. + TYPE parameter specifies relationship type. + + Examples: + RELATED;TYPE=friend:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + RELATED;TYPE=co-worker;VALUE=text:Jane Doe + RELATED;TYPE=spouse:http://example.com/directory/jdoe.vcf + """ + name = "RELATED" + description = 'Relationship to another entity' + + +registerBehavior(Related) + + +class Member(VCardTextBehavior): + """ + MEMBER property for vCard 4.0. + + Only valid when KIND=group. Defines members of the group. + Value is a URI (often urn:uuid:). + + Examples: + MEMBER:urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af + MEMBER:mailto:subscriber1@example.com + """ + name = "MEMBER" + description = 'Group member (only for KIND=group)' + + +registerBehavior(Member) + + +class ClientPidMap(VCardTextBehavior): + """ + CLIENTPIDMAP property for vCard 4.0. + + Maps PID values to URIs for synchronization. + Format: integer;URI + + Examples: + CLIENTPIDMAP:1;urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b + """ + name = "CLIENTPIDMAP" + description = 'Client PID mapping for synchronization' + + +registerBehavior(ClientPidMap) + + +class Xml(VCardTextBehavior): + """ + XML property for vCard 4.0. + + Extended XML-encoded vCard data. + Must have VALUE=text parameter (default). + + Example: + XML:data + """ + name = "XML" + description = 'Extended XML-encoded vCard data' + + +registerBehavior(Xml) + + +class Source(VCardTextBehavior): + """ + SOURCE property for vCard 4.0. + + URI for the source of the vCard data. + + Examples: + SOURCE:http://example.com/directory/jdoe.vcf + SOURCE:ldap://ldap.example.com/cn=John%20Doe,o=Example%20Corp,c=US + """ + name = "SOURCE" + description = 'Source URI for directory information' + + +registerBehavior(Source) + + +# ------------------------ Modified Properties for vCard 4.0 ------------------- + +class Photo4_0(VCardTextBehavior): + """ + PHOTO property for vCard 4.0. + + In vCard 4.0, PHOTO is a URI value (can use data: URI for inline). + Binary encoding via BASE64 parameter is deprecated. + + Examples: + PHOTO:http://example.com/photo.jpg + PHOTO;MEDIATYPE=image/jpeg:data:image/jpeg;base64,MIICajCCA... + """ + name = "Photo" + description = 'Photograph (URI or data URI in vCard 4.0)' + + @classmethod + def valueRepr(cls, line): + return " (PHOTO URI at 0x{0!s}) ".format(id(line.value)) + + @classmethod + def serialize(cls, obj, buf, lineLength, validate): + """ + Apple's Address Book compatibility for images. + """ + if wacky_apple_photo_serialize: + lineLength = REALLY_LARGE + VCardTextBehavior.serialize(obj, buf, lineLength, validate) + + +registerBehavior(Photo4_0, 'PHOTO', id_='4.0') + + +class Logo4_0(VCardTextBehavior): + """ + LOGO property for vCard 4.0. + + In vCard 4.0, LOGO is a URI value (can use data: URI for inline). + + Examples: + LOGO:http://example.com/logo.png + LOGO;MEDIATYPE=image/png:data:image/png;base64,iVBORw0KG... + """ + name = "Logo" + description = 'Logo (URI or data URI in vCard 4.0)' + + @classmethod + def valueRepr(cls, line): + return " (LOGO URI at 0x{0!s}) ".format(id(line.value)) + + @classmethod + def serialize(cls, obj, buf, lineLength, validate): + """ + Handle logo serialization similarly to photos. + """ + if wacky_apple_photo_serialize: + lineLength = REALLY_LARGE + VCardTextBehavior.serialize(obj, buf, lineLength, validate) + + +registerBehavior(Logo4_0, 'LOGO', id_='4.0') + + +class Sound4_0(VCardTextBehavior): + """ + SOUND property for vCard 4.0. + + In vCard 4.0, SOUND is a URI value (can use data: URI for inline). + + Examples: + SOUND:http://example.com/sound.ogg + SOUND;MEDIATYPE=audio/ogg:data:audio/ogg;base64,T2dnUw... + """ + name = "Sound" + description = 'Sound (URI or data URI in vCard 4.0)' + + @classmethod + def valueRepr(cls, line): + return " (SOUND URI at 0x{0!s}) ".format(id(line.value)) + + +registerBehavior(Sound4_0, 'SOUND', id_='4.0') + + +class Geo4_0(VCardTextBehavior): + """ + GEO property for vCard 4.0. + + In vCard 4.0, GEO is a URI using the geo: scheme (RFC 5870). + In vCard 3.0, it was semicolon-separated lat;long values. + + Examples: + GEO:geo:37.386013,-122.082932 + GEO:geo:48.198634,16.371648;crs=wgs84;u=40 + """ + name = "Geo" + description = 'Geographic position (geo: URI in vCard 4.0)' + + +registerBehavior(Geo4_0, 'GEO', id_='4.0') + + +class Key4_0(VCardTextBehavior): + """ + KEY property for vCard 4.0. + + In vCard 4.0, KEY can be URI or text. + Can use data: URI for inline key data. + + Examples: + KEY:http://example.com/key.pgp + KEY;MEDIATYPE=application/pgp-keys:data:application/pgp-keys;base64,LS0t... + KEY:data:application/pgp-keys;base64,LS0t... + """ + name = "Key" + description = 'Public key or authentication certificate (URI or text in vCard 4.0)' + + +registerBehavior(Key4_0, 'KEY', id_='4.0') + + +class Tel4_0(VCardTextBehavior): + """ + TEL property for vCard 4.0. + + In vCard 4.0, TEL should use VALUE=uri with tel: scheme. + For backward compatibility, text values are still supported. + + TYPE parameter values changed: + - Old: HOME, WORK, VOICE, FAX, CELL, etc. + - New: text, voice, fax, cell, video, pager, textphone + + Examples: + TEL;VALUE=uri;PREF=1;TYPE="voice,home":tel:+1-555-555-5555 + TEL;TYPE=cell:tel:+1-555-123-4567 + TEL;VALUE=text:+1-555-555-5555 + """ + name = "Tel" + description = 'Telephone number (preferably tel: URI in vCard 4.0)' + + +registerBehavior(Tel4_0, 'TEL', id_='4.0') + + +class Uid4_0(VCardTextBehavior): + """ + UID property for vCard 4.0. + + In vCard 4.0, UID is preferably a URI (often urn:uuid:). + Plain text UIDs are still supported for backward compatibility. + + Examples: + UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + UID:http://example.com/contacts/jdoe + """ + name = "Uid" + description = 'Unique identifier (preferably URI in vCard 4.0)' + + +registerBehavior(Uid4_0, 'UID', id_='4.0') + + +# ------------------------ vCard 4.0 FN with updated cardinality ---------------- + +class FN4_0(VCardTextBehavior): + """ + FN property for vCard 4.0. + + In vCard 4.0, FN has cardinality 1* (one or more required). + This allows multiple formatted names for different contexts. + + Examples: + FN:John Doe + FN;LANGUAGE=en:John Doe + FN;LANGUAGE=jp:ジョン・ドゥ + """ + name = "FN" + description = 'Formatted name (one or more required in vCard 4.0)' + + +registerBehavior(FN4_0, 'FN', id_='4.0')