diff --git a/src/libicalvcard/vcardcomponent.c b/src/libicalvcard/vcardcomponent.c index 8f34de6d2..85d095172 100755 --- a/src/libicalvcard/vcardcomponent.c +++ b/src/libicalvcard/vcardcomponent.c @@ -17,12 +17,14 @@ #include "vcardparser.h" #include "vcardproperty_p.h" #include "vcardrestriction.h" +#include "vcardvalue.h" #include "icalerror.h" #include "icalmemory.h" #include #include #include +#include struct vcardcomponent_impl { @@ -843,6 +845,387 @@ void vcardcomponent_normalize(vcardcomponent *comp) comp->components = sorted_comps; } +static void comp_to_v4(vcardcomponent *impl) +{ + pvl_elem itr; + + for (itr = pvl_head(impl->properties); itr != 0; itr = pvl_next(itr)) { + vcardproperty *prop = (vcardproperty *) pvl_data(itr); + vcardproperty_kind pkind = vcardproperty_isa(prop); + vcardvalue *value = vcardproperty_get_value(prop); + vcardvalue_kind vkind = vcardvalue_isa(value); + vcardparameter *param; + vcardenumarray *types = NULL; + char *mediatype = NULL, *buf = NULL, *buf_ptr; + const char *data; + size_t size = 0; + + /* Remove TYPE=PREF and replace with PREF=1 (if no existing (PREF=) */ + param = vcardproperty_get_first_parameter(prop, VCARD_TYPE_PARAMETER); + if (param) { + vcardenumarray_element pref = { .val = VCARD_TYPE_PREF }; + ssize_t i; + + types = vcardparameter_get_type(param); + i = vcardenumarray_find(types, &pref); + if (i >= 0) { + vcardenumarray_remove_element_at(types, i); + if (!vcardenumarray_size(types)) { + vcardproperty_remove_parameter_by_ref(prop, param); + types = NULL; + } + + param = vcardproperty_get_first_parameter(prop, + VCARD_PREF_PARAMETER); + if (!param) { + param = vcardparameter_new_pref(1); + vcardproperty_add_parameter(prop, param); + } + } + } + + switch (pkind) { + case VCARD_VERSION_PROPERTY: + vcardproperty_set_version(prop, VCARD_VERSION_40); + break; + + case VCARD_GEO_PROPERTY: + if (vkind != VCARD_X_VALUE) { + vcardgeotype geo = vcardvalue_get_geo(value); + + if (!geo.uri) { + /* Convert STRUCTURED value kind to geo: URI */ + int n = snprintf(buf, size, "geo:%s,%s", + geo.coords.lat, geo.coords.lon); + + size = n + 1; + buf = icalmemory_new_buffer(size); + snprintf(buf, size, "geo:%s,%s", + geo.coords.lat, geo.coords.lon); + + geo.uri = buf; + geo.coords.lat = geo.coords.lon = NULL; + vcardvalue_set_geo(value, geo); + } + } + break; + + case VCARD_KEY_PROPERTY: + case VCARD_LOGO_PROPERTY: + case VCARD_PHOTO_PROPERTY: + case VCARD_SOUND_PROPERTY: + if (types) { + /* Replace TYPE=subtype with MEDIATYPE or data: + * + * XXX We assume that the first VCARD_TYPE_X is the subtype + */ + for (size_t i = 0; i < vcardenumarray_size(types); i++) { + const vcardenumarray_element *type = + vcardenumarray_element_at(types, i); + + if (type->xvalue) { + /* Copy and lowercase the mediatype */ + switch (pkind) { + case VCARD_LOGO_PROPERTY: + case VCARD_PHOTO_PROPERTY: + mediatype = icalmemory_strdup("image/"); + break; + case VCARD_SOUND_PROPERTY: + mediatype = icalmemory_strdup("audio/"); + break; + default: + mediatype = icalmemory_strdup("application/"); + break; + } + + size = strlen(mediatype); + buf_ptr = mediatype + size; + icalmemory_append_string(&mediatype, &buf_ptr, + &size, type->xvalue); + for (char *c = mediatype; (*c = tolower(*c)); c++); + + /* Remove this TYPE */ + vcardenumarray_remove_element_at(types, i); + if (!vcardenumarray_size(types)) { + vcardproperty_remove_parameter_by_ref(prop, param); + } + break; + } + } + } + + data = vcardvalue_get_uri(value); + if (data && !strchr(data, ':')) { + /* Convert base64-encoded TEXT value kind to data: URI */ + size += strlen(data) + 7; // "data:," + NUL + + param = vcardproperty_get_first_parameter(prop, + VCARD_ENCODING_PARAMETER); + if (param) { + size += 7; // ";base64 + } + + buf = icalmemory_new_buffer(size); + buf_ptr = buf; + + icalmemory_append_string(&buf, &buf_ptr, &size, "data:"); + if (mediatype) { + icalmemory_append_string(&buf, &buf_ptr, &size, mediatype); + icalmemory_free_buffer(mediatype); + } + if (param) { + icalmemory_append_string(&buf, &buf_ptr, &size, ";base64"); + vcardproperty_remove_parameter_by_ref(prop, param); + } + icalmemory_append_char(&buf, &buf_ptr, &size, ','); + icalmemory_append_string(&buf, &buf_ptr, &size, data); + + value->kind = VCARD_URI_VALUE; + vcardvalue_set_uri(value, buf); + } + else if (mediatype) { + param = vcardparameter_new_mediatype(mediatype); + vcardproperty_add_parameter(prop, param); + + icalmemory_free_buffer(mediatype); + } + break; + + case VCARD_UID_PROPERTY: + if (vkind == VCARD_TEXT_VALUE) { + unsigned t_low, t_mid, t_high, clock, node; + + /* Does it look like a RFC 4122 UUID? */ + data = vcardvalue_get_text(value); + if (data && strlen(data) == 36 && + sscanf(data, "%8X-%4X-%4X-%4X-%12X", + &t_low, &t_mid, &t_high, &clock, &node) == 5) { + /* Convert TEXT value kind to urn:uuid: URI */ + size = strlen(data) + 10; // "urn:uuid:" + NUL + buf = icalmemory_new_buffer(size); + buf_ptr = buf; + + icalmemory_append_string(&buf, &buf_ptr, &size, "urn:uuid:"); + icalmemory_append_string(&buf, &buf_ptr, &size, data); + + value->kind = VCARD_URI_VALUE; + vcardvalue_set_uri(value, buf); + } + } + break; + + default: + break; + } + + if (buf) icalmemory_free_buffer(buf); + } +} + +struct pref_prop { + vcardproperty *prop; + int level; +}; + +static void comp_to_v3(vcardcomponent *impl) +{ + struct pref_prop *pref_props[VCARD_NO_PROPERTY] = { 0 }; + vcardproperty_kind pkind; + pvl_elem itr; + + for (itr = pvl_head(impl->properties); itr != 0; itr = pvl_next(itr)) { + vcardproperty *prop = (vcardproperty *) pvl_data(itr); + vcardparameter *val_param = + vcardproperty_get_first_parameter(prop, VCARD_VALUE_PARAMETER); + vcardvalue *value = vcardproperty_get_value(prop); + vcardvalue_kind vkind = vcardvalue_isa(value); + vcardparameter *param; + char *subtype = NULL, *buf = NULL; + const char *mediatype, *uri; + + /* Find prop with lowest PREF= for each set of like properties */ + pkind = vcardproperty_isa(prop); + param = vcardproperty_get_first_parameter(prop, VCARD_PREF_PARAMETER); + if (param && pkind != VCARD_X_PROPERTY) { + int level = vcardparameter_get_pref(param); + struct pref_prop *pp = pref_props[pkind]; + + if (!pp) { + pp = icalmemory_new_buffer(sizeof(struct pref_prop)); + pp->prop = prop; + pp->level = level; + pref_props[pkind] = pp; + } + else if (level < pp->level) { + pp->prop = prop; + pp->level = level; + } + } + + /* Replace MEDIATYPE with TYPE= */ + param = vcardproperty_get_first_parameter(prop, + VCARD_MEDIATYPE_PARAMETER); + if (param) { + mediatype = vcardparameter_get_mediatype(param); + if ((subtype = strchr(mediatype, '/'))) { + /* Copy and uppercase the subtype */ + subtype = icalmemory_strdup(subtype + 1); + for (char *c = subtype; (*c = toupper(*c)); c++); + + /* Add TYPE parameter */ + vcardenumarray_element type = { .xvalue = subtype }; + vcardproperty_add_type_parameter(prop, &type); + + icalmemory_free_buffer(subtype); + } + + vcardproperty_remove_parameter_by_ref(prop, param); + } + + switch (pkind) { + case VCARD_VERSION_PROPERTY: + vcardproperty_set_version(prop, VCARD_VERSION_30); + break; + + case VCARD_GEO_PROPERTY: + if (vkind != VCARD_X_VALUE) { + vcardgeotype geo = vcardvalue_get_geo(value); + + if (geo.uri && !strncmp(geo.uri, "geo:", 4)) { + /* Convert geo: URI to STRUCTURED value kind */ + char *lon; + + buf = icalmemory_strdup(geo.uri); + geo.uri = NULL; + geo.coords.lat = buf + 4; + lon = strchr(buf + 4, ','); + if (lon) { + *lon++ = '\0'; + geo.coords.lon = lon; + } + + vcardvalue_set_geo(value, geo); + } + } + break; + + case VCARD_KEY_PROPERTY: + case VCARD_LOGO_PROPERTY: + case VCARD_PHOTO_PROPERTY: + case VCARD_SOUND_PROPERTY: + uri = vcardvalue_get_uri(value); + if (uri && !strncmp("data:", uri, 5)) { + /* Convert data: URI to base64-encoded TEXT value kind */ + char *base64, *data = NULL; + + buf = icalmemory_strdup(uri); + mediatype = buf + 5; + base64 = strstr(mediatype, ";base64,"); + + if (base64) { + vcardparameter *param = + vcardparameter_new_encoding(VCARD_ENCODING_B); + vcardproperty_add_parameter(prop, param); + + *base64 = '\0'; + data = base64 + 8; + } + else if ((data = strchr(mediatype, ','))) { + *data++ = '\0'; + } + + if ((subtype = strchr(mediatype, '/'))) { + /* Copy and uppercase the subtype */ + for (char *c = ++subtype; (*c = toupper(*c)); c++); + + /* Add TYPE parameter */ + vcardenumarray_element type = { .xvalue = subtype }; + vcardproperty_add_type_parameter(prop, &type); + } + + value->kind = VCARD_TEXT_VALUE; + vcardvalue_set_text(value, data ? data : ""); + } + break; + + case VCARD_TEL_PROPERTY: + uri = vcardvalue_get_uri(value); + if (uri && !strncmp(uri, "tel:", 4)) { + /* Convert tel: URI to TEXT value kind */ + buf = icalmemory_strdup(uri + 4); + + value->kind = VCARD_TEXT_VALUE; + vcardvalue_set_text(value, buf); + + if (val_param) { + vcardproperty_remove_parameter_by_ref(prop, val_param); + } + } + break; + + case VCARD_UID_PROPERTY: + uri = vcardvalue_get_uri(value); + if (uri && !strncmp(uri, "urn:uuid:", 9)) { + /* Convert urn:uuid: URI to TEXT value kind */ + buf = icalmemory_strdup(uri + 9); + + value->kind = VCARD_TEXT_VALUE; + vcardvalue_set_text(value, buf); + + if (val_param) { + vcardproperty_remove_parameter_by_ref(prop, val_param); + } + } + break; + + default: + break; + } + + if (buf) icalmemory_free_buffer(buf); + } + + /* Add TYPE=PREF for each most preferred property */ + for (pkind = 0; pkind < VCARD_NO_PROPERTY; pkind++) { + struct pref_prop *pp = pref_props[pkind]; + + if (pp) { + vcardenumarray_element type = { .val = VCARD_TYPE_PREF }; + + vcardproperty_add_type_parameter(pp->prop, &type); + icalmemory_free_buffer(pp); + } + } +} + +void vcardcomponent_transform(vcardcomponent *impl, vcardproperty_version version) +{ + pvl_elem itr; + vcardcomponent *c; + vcardcomponent_kind kind = vcardcomponent_isa(impl); + + icalerror_check_arg_rv((impl != 0), "component"); + icalerror_check_arg_rv((kind != VCARD_NO_COMPONENT), + "component kind is VCARD_NO_COMPONENT"); + + if (kind == VCARD_VCARD_COMPONENT) { + if (version == VCARD_VERSION_40) { + comp_to_v4(impl); + } + else { + comp_to_v3(impl); + } + + impl->version = version; + } + + for (itr = pvl_head(impl->components); itr != 0; itr = pvl_next(itr)) { + c = (vcardcomponent *) pvl_data(itr); + + vcardcomponent_transform(c, version); + } +} + /******************** Convenience routines **********************/ enum vcardproperty_version vcardcomponent_get_version(vcardcomponent *card) diff --git a/src/libicalvcard/vcardcomponent.h b/src/libicalvcard/vcardcomponent.h index 42915eb80..a7deb2b6c 100644 --- a/src/libicalvcard/vcardcomponent.h +++ b/src/libicalvcard/vcardcomponent.h @@ -164,6 +164,9 @@ LIBICAL_VCARD_EXPORT void vcardcomponent_convert_errors(vcardcomponent *card); */ LIBICAL_VCARD_EXPORT void vcardcomponent_normalize(vcardcomponent *card); +LIBICAL_VCARD_EXPORT void vcardcomponent_transform(vcardcomponent *impl, + vcardproperty_version version); + /******************** Convenience routines **********************/ LIBICAL_VCARD_EXPORT enum vcardproperty_version vcardcomponent_get_version(vcardcomponent *card); diff --git a/src/libicalvcard/vcardproperty.c b/src/libicalvcard/vcardproperty.c index 3f57da2e9..fc081a5d1 100644 --- a/src/libicalvcard/vcardproperty.c +++ b/src/libicalvcard/vcardproperty.c @@ -1131,3 +1131,21 @@ void vcardproperty_normalize(vcardproperty *prop) break; } } + +void vcardproperty_add_type_parameter(vcardproperty *prop, + vcardenumarray_element *type) +{ + vcardenumarray *types; + vcardparameter *param = + vcardproperty_get_first_parameter(prop, VCARD_TYPE_PARAMETER); + + if (param) { + types = vcardparameter_get_type(param); + } + else { + types = vcardenumarray_new(1); + param = vcardparameter_new_type(types); + vcardproperty_add_parameter(prop, param); + } + vcardenumarray_add(types, type); +} diff --git a/src/libicalvcard/vcardproperty.h b/src/libicalvcard/vcardproperty.h index d20128816..30e8bcafa 100644 --- a/src/libicalvcard/vcardproperty.h +++ b/src/libicalvcard/vcardproperty.h @@ -178,4 +178,7 @@ LIBICAL_VCARD_EXPORT void vcardproperty_normalize(vcardproperty *prop); LIBICAL_VCARD_EXPORT int vcardproperty_is_structured(vcardproperty_kind pkind); LIBICAL_VCARD_EXPORT int vcardproperty_is_multivalued(vcardproperty_kind pkind); +LIBICAL_VCARD_EXPORT void vcardproperty_add_type_parameter(vcardproperty *prop, + vcardenumarray_element *type); + #endif /*VCARDPROPERTY_H */ diff --git a/src/test/libicalvcard/vcard_test.c b/src/test/libicalvcard/vcard_test.c index 9fd3e9e8a..15c467baa 100644 --- a/src/test/libicalvcard/vcard_test.c +++ b/src/test/libicalvcard/vcard_test.c @@ -69,8 +69,8 @@ static void test_parse_file(const char *fname) "VERSION:4.0\r\n" "FN:Simon Perreault\r\n" "N:Perreault;Simon;;;ing. jr,M.Sc.\r\n" - "BDAY;VALUE=DATE:--0203\r\n" - "BDAY;VALUE=DATE:--0203\r\n" + "BDAY:--0203\r\n" + "BDAY:0000\r\n" "ANNIVERSARY;VALUE=TIMESTAMP:20090808T143000-0500\r\n" "GENDER:M;manly\r\n" "ADR;TYPE=WORK:;Suite D2-630;2875 Laurier;Quebec;QC;G1V 2M2;Canada\r\n" @@ -80,13 +80,13 @@ static void test_parse_file(const char *fname) "EMAIL;TYPE=WORK:simon.perreault@viagenie.ca\r\n" "LANG;PREF=2:en\r\n" "LANG;PREF=1:fr\r\n" - "TZ;VALUE=TEXT:-0500\r\n" + "TZ;VALUE=UTC-OFFSET:-05\r\n" "GEO;TYPE=WORK:geo:46.772673,-71.282945\r\n" "ORG;TYPE=WORK:Viagenie;Foo\r\n" "CATEGORIES:bar,foo\r\n" "NOTE;LANGUAGE=en;PID=1.0,3:Test vCard\r\n" "URL;TYPE=HOME:http://nomis80.org\r\n" - "KEY;VALUE=URI;TYPE=WORK:http://www.viagenie.ca/simon.perreault/simon.asc\r\n" + "KEY;TYPE=WORK:http://www.viagenie.ca/simon.perreault/simon.asc\r\n" "X-LIC-ERROR;X-LIC-ERRORTYPE=RESTRICTION-CHECK:Failed restrictions for \r\n" " BDAY property. Expected zero or one instances of the property and got 2\r\n" "END:VCARD\r\n" @@ -168,11 +168,18 @@ static void test_add_props(vcardcomponent *card) "VERSION:4.0\r\n" "FN:Mickey Mouse\r\n" "N:Mouse;Mickey;;;;;\r\n" - "BDAY;VALUE=DATE:19281118\r\n" + "PHOTO:data:image/jpeg;base64,ABCDEF\r\n" + "BDAY:--1118T03\r\n" "ADR:;;123 Main Street,Disney World;Orlando;FL;32836;USA;;;;;;;;;;;\r\n" + "TEL;VALUE=URI:tel:+1-888-555-1212\r\n" + "LANG;PREF=1:en\r\n" + "LANG;PREF=2:fr\r\n" "TZ;VALUE=UTC-OFFSET:-0230\r\n" + "GEO:geo:46.772673,-71.282945\r\n" + "LOGO;MEDIATYPE=image/png:https://example.com/logo.png\r\n" "CATEGORIES:aaa,zzz\r\n" "group1.NOTE;LANGUAGE=en;PID=1,3;SORT-AS=bar,foo;TYPE=WORK:Test vCard\r\n" + "UID;VALUE=TEXT:foo-bar\r\n" "END:VCARD\r\n"; /* Create and add NOTE property */ @@ -246,9 +253,9 @@ static void test_add_props(vcardcomponent *card) /* Create and add BDAY property */ vcardtimetype t = vcardtime_null_date(); - t.year = 1928; t.month = 11; t.day = 18; + t.hour = 3; prop = vcardproperty_new_bday(t); vcardcomponent_add_property(card, prop); @@ -257,6 +264,44 @@ static void test_add_props(vcardcomponent *card) prop = vcardproperty_new_tz(tz); vcardcomponent_add_property(card, prop); + /* Create and add GEO property */ + vcardgeotype geo = { .uri = "geo:46.772673,-71.282945" }; + prop = vcardproperty_new_geo(geo); + vcardcomponent_add_property(card, prop); + + /* Create and add PHOTO property */ + prop = vcardproperty_new_photo("data:image/jpeg;base64,ABCDEF"); + vcardcomponent_add_property(card, prop); + + /* Create and add LOGO property */ + prop = vcardproperty_vanew_logo("https://example.com/logo.png", + vcardparameter_new_mediatype("image/png"), + 0); + vcardcomponent_add_property(card, prop); + + /* Create and add UID property */ + prop = vcardproperty_vanew_uid("foo-bar", + vcardparameter_new_value(VCARD_VALUE_TEXT), + 0); + vcardcomponent_add_property(card, prop); + + /* Create and add TEL property */ + prop = vcardproperty_vanew_tel("tel:+1-888-555-1212", + vcardparameter_new_value(VCARD_VALUE_URI), + 0); + vcardcomponent_add_property(card, prop); + + /* Create and add LANG properties */ + prop = vcardproperty_vanew_lang("fr", + vcardparameter_new_pref(2), + 0); + vcardcomponent_add_property(card, prop); + + prop = vcardproperty_vanew_lang("en", + vcardparameter_new_pref(1), + 0); + vcardcomponent_add_property(card, prop); + vcardrestriction_check(card); vcardcomponent_normalize(card); assert_str_equals(want, vcardcomponent_as_vcard_string(card)); @@ -269,26 +314,57 @@ static void test_n_restriction(vcardcomponent *card) "BEGIN:VCARD\r\n" "VERSION:3.0\r\n" "FN:Mickey Mouse\r\n" - "BDAY;VALUE=DATE:19281118\r\n" + "PHOTO;ENCODING=B;TYPE=JPEG:ABCDEF\r\n" + "BDAY:00001118T030000\r\n" "ADR:;;123 Main Street,Disney World;Orlando;FL;32836;USA;;;;;;;;;;;\r\n" - "TZ;VALUE=UTC-OFFSET:-0230\r\n" + "TEL:+1-888-555-1212\r\n" + "LANG;PREF=1;TYPE=PREF:en\r\n" + "LANG;PREF=2:fr\r\n" + "TZ:-02:30\r\n" + "GEO:46.772673;-71.282945\r\n" + "LOGO;VALUE=URI;TYPE=PNG:https://example.com/logo.png\r\n" "CATEGORIES:aaa,zzz\r\n" "group1.NOTE;LANGUAGE=en;PID=1,3;SORT-AS=bar,foo;TYPE=WORK:Test vCard\r\n" + "UID:foo-bar\r\n" "X-LIC-ERROR;X-LIC-ERRORTYPE=RESTRICTION-CHECK:Failed restrictions for N \r\n" " property. Expected 1 instances of the property and got 0\r\n" "END:VCARD\r\n"; - /* Change VERSION from 4.0 to 3.0 */ - prop = vcardcomponent_get_first_property(card, VCARD_VERSION_PROPERTY); - vcardproperty_set_version(prop, VCARD_VERSION_30); - /* Remove N property */ prop = vcardcomponent_get_first_property(card, VCARD_N_PROPERTY); vcardcomponent_remove_property(card, prop); vcardproperty_free(prop); + vcardcomponent_transform(card, VCARD_VERSION_30); + vcardrestriction_check(card); assert_str_equals(want, vcardcomponent_as_vcard_string(card)); + strip_errors(card); +} + +static void test_v3_to_v4(vcardcomponent *card) +{ + const char *want = + "BEGIN:VCARD\r\n" + "VERSION:4.0\r\n" + "FN:Mickey Mouse\r\n" + "PHOTO:data:image/jpeg;base64,ABCDEF\r\n" + "BDAY:--1118T03\r\n" + "ADR:;;123 Main Street,Disney World;Orlando;FL;32836;USA;;;;;;;;;;;\r\n" + "TEL:+1-888-555-1212\r\n" + "LANG;PREF=1:en\r\n" + "LANG;PREF=2:fr\r\n" + "TZ;VALUE=UTC-OFFSET:-0230\r\n" + "GEO:geo:46.772673,-71.282945\r\n" + "LOGO;MEDIATYPE=image/png:https://example.com/logo.png\r\n" + "CATEGORIES:aaa,zzz\r\n" + "group1.NOTE;LANGUAGE=en;PID=1,3;SORT-AS=bar,foo;TYPE=WORK:Test vCard\r\n" + "UID;VALUE=TEXT:foo-bar\r\n" + "END:VCARD\r\n"; + + vcardcomponent_transform(card, VCARD_VERSION_40); + + assert_str_equals(want, vcardcomponent_as_vcard_string(card)); } int main(int argc, const char **argv) @@ -306,6 +382,7 @@ int main(int argc, const char **argv) card = test_comp_vanew(); test_add_props(card); test_n_restriction(card); + test_v3_to_v4(card); vcardcomponent_free(card); diff --git a/test-data/test.vcf b/test-data/test.vcf index 214bbd2be..a98b6c936 100644 --- a/test-data/test.vcf +++ b/test-data/test.vcf @@ -3,7 +3,7 @@ VERSION: 4.0 FN:Simon Perreault N:Perreault;Simon;;;ing. jr,M.Sc. BDAY:--0203 -BDAY:--0203 +BDAY:0000 ANNIVERSARY:20090808T1430-0500 GENDER:M;manly LANG;PREF=1:fr @@ -15,8 +15,8 @@ TEL;VALUE=URI;TYPE=voice;PREF=100:tel:+1-418-656-9254;ext=102 TEL;VALUE=URI;TYPE="work,cell,foo,voice,bar,video,text":tel:+1-418-262-6501 EMAIL;TYPE=WORK:simon.perreault@viagenie.ca GEO;TYPE=WORK:geo:46.772673,-71.282945 -KEY;TYPE=WORK:http://www.viagenie.ca/simon.perreault/simon.asc -TZ:-0500 +KEY;VALUE=URI;TYPE=WORK:http://www.viagenie.ca/simon.perreault/simon.asc +TZ;VALUE=UTC-OFFSET:-0500 URL;TYPE=HOME:http://nomis80.org NOTE;LANGUAGE=en;PID=3,1.0:Test vCard