From ff6e37a8bf46737cddcbf6e92314897f87a57b09 Mon Sep 17 00:00:00 2001 From: Ken Murchison Date: Sat, 29 Jun 2024 09:27:19 -0400 Subject: [PATCH] vcard_support.c: properly handle X-APPLE-OMIT-YEAR parameter --- cassandane/tiny-tests/Carddav/put_bday_noyear | 69 +++++++++++++++++++ .../JMAPContacts/card_set_create_bday_noyear | 65 +++++++++++++++++ .../contact_set_create_bday_noyear | 48 +++++++++++++ imap/vcard_support.c | 46 +++++++++++-- 4 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 cassandane/tiny-tests/Carddav/put_bday_noyear create mode 100644 cassandane/tiny-tests/JMAPContacts/card_set_create_bday_noyear create mode 100644 cassandane/tiny-tests/JMAPContacts/contact_set_create_bday_noyear diff --git a/cassandane/tiny-tests/Carddav/put_bday_noyear b/cassandane/tiny-tests/Carddav/put_bday_noyear new file mode 100644 index 00000000000..25fc7c4e270 --- /dev/null +++ b/cassandane/tiny-tests/Carddav/put_bday_noyear @@ -0,0 +1,69 @@ +#!perl +use Cassandane::Tiny; + +sub test_put_bday_noyear + :needs_component_httpd +{ + my ($self) = @_; + + my $CardDAV = $self->{carddav}; + my $Id = $CardDAV->NewAddressBook('foo'); + $self->assert_not_null($Id); + $self->assert_str_equals($Id, 'foo'); + my $href = "$Id/bar.vcf"; + + my $id = 'ae2640cc-234a-4dd9-95cc-3106258445b9'; + my $card = < 'text/vcard', + 'Authorization' => $CardDAV->auth_header(), + ); + + xlog $self, "PUT vCard v3 with no-year BDAY -- should fail"; + my $Response = $CardDAV->{ua}->request('PUT', $CardDAV->request_url($href), { + content => $card, + headers => \%Headers, + }); + $self->assert_num_equals(403, $Response->{status}); + + xlog $self, "PUT vCard v4 with no-year BDAY"; + $card =~ s/3.0/4.0/; + $Response = $CardDAV->{ua}->request('PUT', $CardDAV->request_url($href), { + content => $card, + headers => \%Headers, + }); + $self->assert_num_equals(201, $Response->{status}); + + my $res = $CardDAV->Request('GET', $href, '', + 'Accept' => 'text/vcard; version=3.0'); + + $card = $res->{content}; + $card =~ s/\r?\n[ \t]+//gs; # unfold long properties + + $self->assert_matches(qr|BDAY;X-APPLE-OMIT-YEAR=1604:1604(-)?04(-)?15|, + $card); + + xlog $self, "PUT vCard v3 with omit-year BDAY"; + $Response = $CardDAV->{ua}->request('PUT', $CardDAV->request_url($href), { + content => $card, + headers => \%Headers, + }); + $self->assert_num_equals(204, $Response->{status}); + + $res = $CardDAV->Request('GET', $href, '', + 'Accept' => 'text/vcard; version=4.0'); + + $card = $res->{content}; + $card =~ s/\r?\n[ \t]+//gs; # unfold long properties + + $self->assert_matches(qr|BDAY:--0415|, $card); +} diff --git a/cassandane/tiny-tests/JMAPContacts/card_set_create_bday_noyear b/cassandane/tiny-tests/JMAPContacts/card_set_create_bday_noyear new file mode 100644 index 00000000000..285df4a85c0 --- /dev/null +++ b/cassandane/tiny-tests/JMAPContacts/card_set_create_bday_noyear @@ -0,0 +1,65 @@ +#!perl +use Cassandane::Tiny; + +sub test_card_set_create_bday_noyear + :needs_component_jmap :needs_dependency_icalvcard +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + + my $service = $self->{instance}->get_service("http"); + $ENV{DEBUGDAV} = 1; + my $carddav = Net::CardDAVTalk->new( + user => 'cassandane', + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => 'http', + url => '/', + expandurl => 1, + ); + + my $id = 'ae2640cc-234a-4dd9-95cc-3106258445b9'; + + my $res = $jmap->CallMethods([ + ['ContactCard/set', { + create => { + "1" => { + '@type' => 'Card', + version => '1.0', + uid => $id, + name => { full => 'Jane Doe' }, + anniversaries => { + k8 => { + '@type' => 'Anniversary', + kind => 'birth', + date => { + month => 4, + day => 15 + } + } + } + } + } + }, 'R1'] + ]); + + $self->assert_not_null($res->[0][1]{created}{1}); + + my $href = $res->[0][1]{created}{1}{'cyrusimap.org:href'}; + $res = $carddav->Request('GET', $href, '', + 'Accept' => 'text/vcard; version=4.0'); + + my $card = $res->{content}; + $card =~ s/\r?\n[ \t]+//gs; # unfold long properties + + $self->assert_matches(qr|BDAY(;VALUE=DATE)?;PROP-ID=k8:--0415|, $card); + + $res = $carddav->Request('GET', $href, '', + 'Accept' => 'text/vcard; version=3.0'); + + $card = $res->{content}; + $card =~ s/\r?\n[ \t]+//gs; # unfold long properties + + $self->assert_matches(qr|BDAY(;VALUE=DATE)?;PROP-ID=k8;X-APPLE-OMIT-YEAR=1604:1604(-)?04(-)?15|, $card); +} diff --git a/cassandane/tiny-tests/JMAPContacts/contact_set_create_bday_noyear b/cassandane/tiny-tests/JMAPContacts/contact_set_create_bday_noyear new file mode 100644 index 00000000000..07a8e385a2a --- /dev/null +++ b/cassandane/tiny-tests/JMAPContacts/contact_set_create_bday_noyear @@ -0,0 +1,48 @@ +#!perl +use Cassandane::Tiny; + +sub test_contact_set_create_bday_noyear + :needs_component_jmap +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + + my $service = $self->{instance}->get_service("http"); + $ENV{DEBUGDAV} = 1; + my $carddav = Net::CardDAVTalk->new( + user => 'cassandane', + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => 'http', + url => '/', + expandurl => 1, + ); + + my $id = 'ae2640cc-234a-4dd9-95cc-3106258445b9'; + + my $res = $jmap->CallMethods([ + ['Contact/set', { + create => { + "1" => { + uid => $id, + firstName => 'Jane', + lastName => 'Doe', + birthday => '0000-04-15' + } + } + }, 'R1'], + ['Contact/get', { ids => [ "#1" ] }, 'R2'] + ]); + + $self->assert_not_null($res->[0][1]{created}{1}); + + my $href = $res->[1][1]{list}[0]{'x-href'}; + $res = $carddav->Request('GET', $href, '', + 'Accept' => 'text/vcard; version=4.0'); + + my $card = $res->{content}; + $card =~ s/\r?\n[ \t]+//gs; # unfold long properties + + $self->assert_matches(qr|BDAY(;VALUE=DATE)?:--0415|, $card); +} diff --git a/imap/vcard_support.c b/imap/vcard_support.c index 11ab6fb1e9a..e318f4759b8 100644 --- a/imap/vcard_support.c +++ b/imap/vcard_support.c @@ -843,6 +843,21 @@ EXPORTED void vcard_to_v3_x(vcardcomponent *vcard) } break; + case VCARD_BDAY_PROPERTY: + case VCARD_DEATHDATE_PROPERTY: + case VCARD_ANNIVERSARY_PROPERTY: { + vcardtimetype dt = vcardproperty_get_bday(prop); + + if (dt.year == -1) { + dt.year = 1604; + vcardproperty_set_bday(prop, dt); + vcardproperty_set_parameter_from_string(prop, + "X-APPLE-OMIT-YEAR", + "1604"); + } + break; + } + case VCARD_KEY_PROPERTY: case VCARD_LOGO_PROPERTY: case VCARD_PHOTO_PROPERTY: @@ -956,6 +971,10 @@ EXPORTED void vcard_to_v4_x(vcardcomponent *vcard) prop = vcardcomponent_get_first_property(vcard, VCARD_VERSION_PROPERTY); is_v4 = prop && vcardproperty_get_version(prop) == VCARD_VERSION_40; + if (!is_v4) { + /* Set proper VERSION */ + vcardproperty_set_version(prop, VCARD_VERSION_40); + } for (prop = vcardcomponent_get_first_property(vcard, VCARD_ANY_PROPERTY); prop; prop = next) { @@ -987,13 +1006,6 @@ EXPORTED void vcard_to_v4_x(vcardcomponent *vcard) } switch (vcardproperty_isa(prop)) { - case VCARD_VERSION_PROPERTY: - if (!is_v4) { - /* Set proper VERSION */ - vcardproperty_set_version(prop, VCARD_VERSION_40); - } - break; - case VCARD_UID_PROPERTY: /* Rewrite UID property */ param = vcardproperty_get_first_parameter(prop, @@ -1010,6 +1022,26 @@ EXPORTED void vcard_to_v4_x(vcardcomponent *vcard) } break; + case VCARD_BDAY_PROPERTY: + case VCARD_DEATHDATE_PROPERTY: + case VCARD_ANNIVERSARY_PROPERTY: + for (param = + vcardproperty_get_first_parameter(prop, VCARD_X_PARAMETER); + param; + param = vcardproperty_get_next_parameter(prop, VCARD_X_PARAMETER)) { + + if (!strcasecmpsafe(vcardparameter_get_xname(param), + "X-APPLE-OMIT-YEAR")) { + vcardtimetype dt = vcardproperty_get_bday(prop); + + dt.year = -1; + vcardproperty_set_bday(prop, dt); + vcardproperty_remove_parameter_by_ref(prop, param); + break; + } + } + break; + case VCARD_KEY_PROPERTY: /* Rewrite KEY, LOGO, PHOTO, SOUND properties */ str = "";