Blame lib/Net/CardDAVTalk/VCard.pm

Packit 5b1f1f
package Net::CardDAVTalk::VCard;
Packit 5b1f1f
Packit 5b1f1f
use 5.014;
Packit 5b1f1f
use strict;
Packit 5b1f1f
use warnings;
Packit 5b1f1f
use Text::VCardFast qw(vcard2hash hash2vcard);
Packit 5b1f1f
use Encode qw(decode_utf8 encode_utf8);
Packit 5b1f1f
use MIME::Base64 qw(decode_base64);
Packit 5b1f1f
use List::Pairwise qw(mapp);
Packit 5b1f1f
use List::MoreUtils qw(all pairwise);
Packit 5b1f1f
use Date::Format qw(strftime);
Packit 5b1f1f
use File::MMagic;
Packit 5b1f1f
use Data::Dumper;
Packit 5b1f1f
Packit 5b1f1f
=head1 NAME
Packit 5b1f1f
Packit 5b1f1f
Net::CardDAVTalk::VCard - A wrapper for VCard files
Packit 5b1f1f
Packit 5b1f1f
=head1 SUBROUTINES/METHODS
Packit 5b1f1f
Packit 5b1f1f
# Core {{{
Packit 5b1f1f
Packit 5b1f1f
=head2 $class->new()
Packit 5b1f1f
Packit 5b1f1f
Create a basic VCard object with no fields set
Packit 5b1f1f
Packit 5b1f1f
=cut
Packit 5b1f1f
Packit 5b1f1f
my $FileMagic = File::MMagic->new;
Packit 5b1f1f
Packit 5b1f1f
sub new {
Packit 5b1f1f
  my $Proto = shift;
Packit 5b1f1f
  my $Class = ref($Proto) || $Proto;
Packit 5b1f1f
Packit 5b1f1f
  my $Self = {
Packit 5b1f1f
    type => 'VCARD',
Packit 5b1f1f
    properties => {
Packit 5b1f1f
      version => [
Packit 5b1f1f
        {
Packit 5b1f1f
          name => "version",
Packit 5b1f1f
          value => "3.0"
Packit 5b1f1f
        },
Packit 5b1f1f
      ],
Packit 5b1f1f
    }
Packit 5b1f1f
  };
Packit 5b1f1f
Packit 5b1f1f
  return bless $Self, $Class;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
=head2 $class->new_fromstring($String)
Packit 5b1f1f
Packit 5b1f1f
Create a new object and populate it by parsing the VCard file
Packit 5b1f1f
who's contents are given in the string.
Packit 5b1f1f
Packit 5b1f1f
=cut
Packit 5b1f1f
Packit 5b1f1f
sub new_fromstring {
Packit 5b1f1f
  my $Proto = shift;
Packit 5b1f1f
  my $Class = ref($Proto) || $Proto;
Packit 5b1f1f
  my $Data = shift;
Packit 5b1f1f
Packit 5b1f1f
  my $Parsed = eval { vcard2hash($Data, multival => [ qw(n adr org) ]) };
Packit 5b1f1f
Packit 5b1f1f
  my $Self = $Parsed->{objects}->[0];
Packit 5b1f1f
  if ($Self->{type} ne 'vcard') {
Packit 5b1f1f
    warn "Found non-vcard '$Self->{type}' for in $_";
Packit 5b1f1f
    return undef;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  bless $Self, $Class;
Packit 5b1f1f
Packit 5b1f1f
  $Self->Normalise();
Packit 5b1f1f
Packit 5b1f1f
  $Self->{_raw} = $Data;
Packit 5b1f1f
Packit 5b1f1f
  return $Self;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
=head2 $class->new_fromfile($File)
Packit 5b1f1f
Packit 5b1f1f
Given a filename or filehandle, read and parse a vcard from it.
Packit 5b1f1f
Packit 5b1f1f
=cut
Packit 5b1f1f
Packit 5b1f1f
sub new_fromfile {
Packit 5b1f1f
  my $Proto = shift;
Packit 5b1f1f
  my $Class = ref($Proto) || $Proto;
Packit 5b1f1f
Packit 5b1f1f
  my $FileR = shift;
Packit 5b1f1f
Packit 5b1f1f
  my $Fh;
Packit 5b1f1f
  if (ref $FileR) {
Packit 5b1f1f
    $Fh = $FileR;
Packit 5b1f1f
  } else {
Packit 5b1f1f
    open($Fh, $FileR)
Packit 5b1f1f
      || die "Could not read '$FileR': $!";
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  my $Input = do { local $/; <$Fh>; };
Packit 5b1f1f
Packit 5b1f1f
  my $Self = $Class->new_fromstring($Input);
Packit 5b1f1f
  $Self->{file} = $FileR if !ref $FileR;
Packit 5b1f1f
Packit 5b1f1f
  return $Self;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
=head2 $self->as_string()
Packit 5b1f1f
Packit 5b1f1f
Return a string representation of the VCard (inverse of
Packit 5b1f1f
new_fromstring)
Packit 5b1f1f
Packit 5b1f1f
=cut
Packit 5b1f1f
Packit 5b1f1f
sub as_string {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  delete $Self->{_raw};
Packit 5b1f1f
  $Self->{_raw} = eval { hash2vcard({ objects => [ $Self ] }) };
Packit 5b1f1f
  return $Self->{_raw};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
=head2 $self->uid()
Packit 5b1f1f
Packit 5b1f1f
Get or set the uid field of the card.
Packit 5b1f1f
Packit 5b1f1f
=cut
Packit 5b1f1f
Packit 5b1f1f
sub uid {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  $Self->V('uid', 'value', @_);
Packit 5b1f1f
}
Packit 5b1f1f
sub rev {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  $Self->V('rev', 'value', @_);
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
# }}}
Packit 5b1f1f
Packit 5b1f1f
# ME VCard manipulation {{{
Packit 5b1f1f
Packit 5b1f1f
my @VParamTypes = qw(work home text voice fax cell cell video pager textphone internet);
Packit 5b1f1f
push @VParamTypes, map { uc } @VParamTypes;
Packit 5b1f1f
Packit 5b1f1f
my @VItemN = qw(surnames givennames additionalnames honorificprefixs honorificsuffixes);
Packit 5b1f1f
my @VItemADR = qw(postofficebox extendedaddress streetaddress locality region postalcode countryname);
Packit 5b1f1f
my @VItemORG = qw(company department);
Packit 5b1f1f
my %VExpand = (n => \@VItemN, adr => \@VItemADR, org => \@VItemORG);
Packit 5b1f1f
Packit 5b1f1f
my @ProtoPrefixes = (
Packit 5b1f1f
  [ 'tel', qr/tel:/ ],
Packit 5b1f1f
  [ 'impp', qr/skype:/ ],
Packit 5b1f1f
  [ 'impp', qr/xmpp:/ ],
Packit 5b1f1f
  [ 'x-skype', qr/skype:/ ],
Packit 5b1f1f
  [ 'x-socialprofile', qr/twitter:/ ],
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %ABLabelTypeMap = (Home => 'home', Mobile => 'cell', Twitter => 'twitter');
Packit 5b1f1f
Packit 5b1f1f
my %VCardEmailTypeMap = (
Packit 5b1f1f
  home => 'personal',
Packit 5b1f1f
  work => 'work',
Packit 5b1f1f
);
Packit 5b1f1f
my %RevVCardEmailTypeMap = reverse %VCardEmailTypeMap;
Packit 5b1f1f
Packit 5b1f1f
my %VCardAdrTypeMap = (
Packit 5b1f1f
  home => 'home',
Packit 5b1f1f
  work => 'work',
Packit 5b1f1f
);
Packit 5b1f1f
my %RevVCardAdrTypeMap = reverse %VCardAdrTypeMap;
Packit 5b1f1f
Packit 5b1f1f
my %VCardTelTypeMap = (
Packit 5b1f1f
  home => 'home',
Packit 5b1f1f
  work => 'work',
Packit 5b1f1f
  cell => 'mobile',
Packit 5b1f1f
  fax => 'fax',
Packit 5b1f1f
  pager => 'pager',
Packit 5b1f1f
);
Packit 5b1f1f
my %RevVCardTelTypeMap = reverse %VCardTelTypeMap;
Packit 5b1f1f
Packit 5b1f1f
my %VCardTypeMap = (
Packit 5b1f1f
  email => [ \%VCardEmailTypeMap, \%RevVCardEmailTypeMap ],
Packit 5b1f1f
  adr => [ \%VCardAdrTypeMap, \%RevVCardAdrTypeMap ],
Packit 5b1f1f
  tel => [ \%VCardTelTypeMap, \%RevVCardTelTypeMap ],
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %IMPPServiceTypeMap = qw(
Packit 5b1f1f
  skype       skype
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %IMPPProtoPrefixes = (
Packit 5b1f1f
  'skype' =>      ['skype'],
Packit 5b1f1f
  'msn' =>        ['msn','msnim'],
Packit 5b1f1f
  'googletalk' => ['xmpp'],
Packit 5b1f1f
  'facebook' =>   ['xmpp'],
Packit 5b1f1f
  'aim' =>        ['aim'],
Packit 5b1f1f
  'yahoo' =>      ['ymsgr'],
Packit 5b1f1f
  'icq' =>        ['icq','aim'],
Packit 5b1f1f
  'jabber' =>     ['xmpp'],
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %XSocialProfileTypeMap = qw(
Packit 5b1f1f
  twitter     twitter
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %XServiceTypeMap = qw(
Packit 5b1f1f
  twitter         twitter
Packit 5b1f1f
  skype           skype
Packit 5b1f1f
  skype-username  skype
Packit 5b1f1f
  aim             chat
Packit 5b1f1f
  icq             chat
Packit 5b1f1f
  google-talk     chat
Packit 5b1f1f
  jabber          chat
Packit 5b1f1f
  msn             chat
Packit 5b1f1f
  yahoo           chat
Packit 5b1f1f
  ms-imaddress    chat
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my %VCardNewOnlineMap = (
Packit 5b1f1f
  'web' => [
Packit 5b1f1f
    [ 'url' ]
Packit 5b1f1f
  ],
Packit 5b1f1f
  'chat' => sub { [
Packit 5b1f1f
    [ 'impp', { 'x-service-type' => 'jabber', 'x-user' => $_[0] } ],
Packit 5b1f1f
  ] },
Packit 5b1f1f
  'twitter' => sub { [
Packit 5b1f1f
    [ 'x-socialprofile', { 'type' => 'twitter', 'x-user' => $_[0] }, "http://twitter.com/$_[0]" ],
Packit 5b1f1f
    [ 'x-twitter' ],
Packit 5b1f1f
  ] },
Packit 5b1f1f
  'skype' => sub { [
Packit 5b1f1f
    [ 'impp', { 'x-service-type' => 'skype', 'x-user' => $_[0] } ],
Packit 5b1f1f
    [ 'x-skype' ],
Packit 5b1f1f
  ] },
Packit 5b1f1f
  'other' => sub { [
Packit 5b1f1f
    [ 'impp', { 'x-user' => $_[0] } ],
Packit 5b1f1f
  ] },
Packit 5b1f1f
);
Packit 5b1f1f
Packit 5b1f1f
my $NoteParamName = 'x-menote';
Packit 5b1f1f
Packit 5b1f1f
sub Normalise {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
Packit 5b1f1f
  $Self->{meta} = {};
Packit 5b1f1f
Packit 5b1f1f
  my $Props = $Self->{properties};
Packit 5b1f1f
Packit 5b1f1f
  # Expand/decode/normalise all values
Packit 5b1f1f
  for (values %$Props) {
Packit 5b1f1f
Packit 5b1f1f
    # All properties are array ref of items
Packit 5b1f1f
    for (@$_) {
Packit 5b1f1f
Packit 5b1f1f
      # Scalar or array ref (e.g. 'n', 'adr', etc compound fields)
Packit 5b1f1f
      my $Value = $_->{value} // $_->{values};
Packit 5b1f1f
Packit 5b1f1f
      # If non-ascii value, it's utf-8
Packit 5b1f1f
      for (ref($Value) ? @$Value : $Value) {
Packit 5b1f1f
        if (/[\x80-\xff]/) {
Packit 5b1f1f
          $_ = eval { decode_utf8($_) } // $_;
Packit 5b1f1f
        }
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      # Expand out 'n' and 'adr' fields into components.
Packit 5b1f1f
      #  Put scalars into expanded fields and scalar refs in values arrayref
Packit 5b1f1f
      if (my $VFields = $VExpand{$_->{name}}) {
Packit 5b1f1f
        @$_{@$VFields} = map { $_ // '' } @$Value[0 .. scalar(@$VFields)-1];
Packit 5b1f1f
        $_->{values} = [ \@$_{@$VFields} ];
Packit 5b1f1f
        delete $_->{value};
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      # Handle base64 encoded value
Packit 5b1f1f
      my $Encoding = $_->{params}->{encoding};
Packit 5b1f1f
      if (ref($Encoding) && lc $Encoding->[0] eq 'b') {
Packit 5b1f1f
        $Value = decode_base64($Value);
Packit 5b1f1f
        $_->{binary} = 1;
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      # Expand and lowercase comma separated type= parameters
Packit 5b1f1f
      if (my $Type = $_->{params}->{type}) {
Packit 5b1f1f
        $_->{params}->{type} = $Type = [ $Type ] if !ref $Type;
Packit 5b1f1f
        @$Type = map { split /,/, lc $_ } @$Type;
Packit 5b1f1f
      }
Packit 5b1f1f
      if (my $ServiceType = $_->{params}->{'x-service-type'}) {
Packit 5b1f1f
        $_->{params}->{'x-service-type'} = $ServiceType = [ $ServiceType ] if !ref $ServiceType;
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      $_->{value} = $Value;
Packit 5b1f1f
Packit 5b1f1f
      # Create 'groups' item that tracks items in each group
Packit 5b1f1f
      push @{$Self->{groups}->{$_->{group}}}, $_ if $_->{group};
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Add any X-ABLabel group items as 'label' attribute
Packit 5b1f1f
  if (my $Labels = $Props->{'x-ablabel'}) {
Packit 5b1f1f
    my %LabelMap = map { $_->{group} ? ($_->{group} => $_) : () } @$Labels;
Packit 5b1f1f
    for (keys %$Props) {
Packit 5b1f1f
      next if $_ eq 'x-ablabel';
Packit 5b1f1f
      for (@{$Props->{$_}}) {
Packit 5b1f1f
        if (my $Label = $LabelMap{$_->{group} // ''}) {
Packit 5b1f1f
          my $LabelV = $_->{label} = $Label->{value};
Packit 5b1f1f
          $_->{labelref} = $Label;
Packit 5b1f1f
Packit 5b1f1f
          # Attach type= param if appropriate
Packit 5b1f1f
          $LabelV = $1 if $LabelV =~ m{^_\$\!<([^>]*)};
Packit 5b1f1f
          if (my $TypeP = $ABLabelTypeMap{$LabelV}) {
Packit 5b1f1f
            my $TypeList = ($_->{params}->{type} //= []);
Packit 5b1f1f
            push @$TypeList, $TypeP if !grep { $_ eq $TypeP } @$TypeList;
Packit 5b1f1f
          }
Packit 5b1f1f
        }
Packit 5b1f1f
      }
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Handle v4 value=uri telephone numbers
Packit 5b1f1f
  my $Version = $Props->{version};
Packit 5b1f1f
  if ($Version && $Version->[0] >= 4.0) {
Packit 5b1f1f
    for (@ProtoPrefixes) {
Packit 5b1f1f
      my ($Prop, $ProtoRE) = @$_;
Packit 5b1f1f
      if (my $Items = $Props->{$Prop}) {
Packit 5b1f1f
        for (@$Items) {
Packit 5b1f1f
          if ($_->{value} =~ s/^($ProtoRE)//) {
Packit 5b1f1f
            $_->{proto_strip} = $1;
Packit 5b1f1f
            # If we found a uri prefix, better have value=uri param
Packit 5b1f1f
            if (!$_->{params}->{value} && $Prop eq 'tel') {
Packit 5b1f1f
              $_->{params}->{value} = [ 'uri' ];
Packit 5b1f1f
            }
Packit 5b1f1f
          }
Packit 5b1f1f
        }
Packit 5b1f1f
      }
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Create synthetic "online" list. Generate "online_type" and "online_value"
Packit 5b1f1f
  # based on all the different types for twitter and skype contact info
Packit 5b1f1f
  my $Online = $Props->{online} = [];
Packit 5b1f1f
Packit 5b1f1f
  # URL:foo.com
Packit 5b1f1f
  for (@{$Props->{url} // []}) {
Packit 5b1f1f
    $_->{online_type}  = 'web';
Packit 5b1f1f
    $_->{online_value} = $_->{value};
Packit 5b1f1f
Packit 5b1f1f
    push @$Online, $_;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # IMPP;X-SERVICE-TYPE=Skype;type=pref:skype:someskype
Packit 5b1f1f
  for (@{$Props->{impp} // []}) {
Packit 5b1f1f
    my $Type  = lc(($_->{params}->{'x-service-type'} // [])->[0] // '');
Packit 5b1f1f
    my $Value = $_->{value};
Packit 5b1f1f
    my $ProtoPrefixes = $IMPPProtoPrefixes{$Type} // ['x-apple'];
Packit 5b1f1f
    $Value =~ s/^$_:// for @$ProtoPrefixes;
Packit 5b1f1f
    $_->{online_type}  = $IMPPServiceTypeMap{$Type} // 'chat';
Packit 5b1f1f
    $_->{online_value} = $Value;
Packit 5b1f1f
Packit 5b1f1f
    push @$Online, $_;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # X-SOCIALPROFILE;type=twitter;x-user=sometwitter:http://twitter.com/sometwitter
Packit 5b1f1f
  for (@{$Props->{'x-socialprofile'} // []}) {
Packit 5b1f1f
    my $Type  = lc(($_->{params}->{type} // [])->[0] // '');
Packit 5b1f1f
    my $Value = $_->{params}->{'x-user'}->[0] // $_->{value};
Packit 5b1f1f
    $_->{online_type}  = $XSocialProfileTypeMap{$Type} // 'other';
Packit 5b1f1f
    $_->{online_value} = $Value;
Packit 5b1f1f
Packit 5b1f1f
    push @$Online, $_;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # X-YAHOO:someyahoo
Packit 5b1f1f
  for my $Type (keys %XServiceTypeMap) {
Packit 5b1f1f
    for (@{$Props->{"x-$Type"} // []}) {
Packit 5b1f1f
      $_->{online_type}  = $XServiceTypeMap{$Type};
Packit 5b1f1f
      $_->{online_value} = $_->{value};
Packit 5b1f1f
Packit 5b1f1f
      push @$Online, $_;
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Set contact_type to match API
Packit 5b1f1f
  for ([ 'email', \%VCardEmailTypeMap ],
Packit 5b1f1f
       [ 'tel', \%VCardTelTypeMap ],
Packit 5b1f1f
       [ 'adr', \%VCardAdrTypeMap ]) {
Packit 5b1f1f
    my ($Prop, $Map) = @$_;
Packit 5b1f1f
Packit 5b1f1f
    my $Props = $Props->{$Prop} || next;
Packit 5b1f1f
    for (@$Props) {
Packit 5b1f1f
      # Prefer calculated online_type, otherwise case on property name or type params
Packit 5b1f1f
      my ($ContactType) =
Packit 5b1f1f
        map { ($_ && $Map->{$_}) or () }
Packit 5b1f1f
          (($_->{online_type} or ()), $_->{name}, @{$_->{params}->{type} // []});
Packit 5b1f1f
Packit 5b1f1f
      $_->{contact_type} = $ContactType // 'other';
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Check N, FN and VERSION fields are present
Packit 5b1f1f
  if (!$Props->{version}) {
Packit 5b1f1f
    $Self->V('version', 'value', '3.0');
Packit 5b1f1f
  }
Packit 5b1f1f
  if (!$Props->{n}) {
Packit 5b1f1f
    $Self->V('n', 'surnames', '');
Packit 5b1f1f
  }
Packit 5b1f1f
  if (!$Props->{fn}) {
Packit 5b1f1f
    $Self->VRebuildFN();
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub DeleteUnusedLabels {
Packit 5b1f1f
  my ($Self) = @_;
Packit 5b1f1f
  my $Props = $Self->{properties};
Packit 5b1f1f
Packit 5b1f1f
  for (@{$Props->{'x-ablabel'} // []}) {
Packit 5b1f1f
    my $Group = $Self->{groups}->{$_->{group}};
Packit 5b1f1f
    my $NumItems = grep { !$_->{deleted} } @$Group;
Packit 5b1f1f
    $_->{deleted} = 1 if $NumItems <= 1;
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub ReadOnly {
Packit 5b1f1f
  $_[0]->{ReadOnly} = $_[1] if @_ > 1; return $_[0]->{ReadOnly};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub V {
Packit 5b1f1f
  my ($Self, $Prop, $Item) = splice @_, 0, 3;
Packit 5b1f1f
  $Item //= 'value';
Packit 5b1f1f
  my $Props = $Self->{properties};
Packit 5b1f1f
Packit 5b1f1f
  die "Tried to modify read-only contact, fetch directly, not from cache"
Packit 5b1f1f
    if @_ && $Self->{ReadOnly};
Packit 5b1f1f
Packit 5b1f1f
  # Always get/set first item of given type
Packit 5b1f1f
  my $V = $Props->{$Prop} && $Props->{$Prop}->[0];
Packit 5b1f1f
Packit 5b1f1f
  # If setting value, and no existing value, create new
Packit 5b1f1f
  if (!$V && @_) {
Packit 5b1f1f
    $V = $Props->{$Prop}->[0] = { name => $Prop, params => {} };
Packit 5b1f1f
Packit 5b1f1f
    # Create parts if an multipart field
Packit 5b1f1f
    if (my $VFields = $VExpand{$Prop}) {
Packit 5b1f1f
      @$V{@$VFields} = ("") x scalar @$VFields;
Packit 5b1f1f
      $V->{values} = [ \@$V{@$VFields} ];
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Get value
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    return $V ? $V->{$Item} : undef;
Packit 5b1f1f
Packit 5b1f1f
  # Set value
Packit 5b1f1f
  } else {
Packit 5b1f1f
    $Self->{vchanged}->{$Prop} = 1;
Packit 5b1f1f
Packit 5b1f1f
    local $_ = shift;
Packit 5b1f1f
Packit 5b1f1f
    if (defined $_) {
Packit 5b1f1f
      # Trim whitespace and garbage from values
Packit 5b1f1f
      s/^\s+//;
Packit 5b1f1f
      s/\s+$//;
Packit 5b1f1f
      # Ugg, saw U+200B (ZERO WIDTH SPACE) in some data, http://www.perlmonks.org/?node_id=1020973
Packit 5b1f1f
      s/\p{FORMAT}//g;
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    # Delete item if not a compound value and setting to empty string or undef
Packit 5b1f1f
    if ((!defined $_ || $_ eq '') && !$V->{values}) {
Packit 5b1f1f
      my $E = shift @{$Props->{$Prop}};
Packit 5b1f1f
      $E->{deleted} = 1;
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    # Otherwise store the new value
Packit 5b1f1f
    else {
Packit 5b1f1f
      $V->{$Item} = $_ // '';
Packit 5b1f1f
Packit 5b1f1f
      # Uggg, for compound value, delete if all values empty
Packit 5b1f1f
      if ($Prop ne 'n' && $V->{values} && all { $$_ eq '' } @{$V->{values}} ) {
Packit 5b1f1f
        my $E = shift @{$Props->{$Prop}};
Packit 5b1f1f
        $E->{deleted} = 1;
Packit 5b1f1f
      }
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    $Self->DeleteUnusedLabels;
Packit 5b1f1f
Packit 5b1f1f
    $Self->VRebuildFN if $Prop eq 'n' || $Prop eq 'org';
Packit 5b1f1f
    return $_;
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VDate {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  local $_ = shift;
Packit 5b1f1f
Packit 5b1f1f
  # Convert VCard -> Our format
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    return undef if !$_;
Packit 5b1f1f
Packit 5b1f1f
    if (/^(\d{4})-(\d{2})-(\d{2})(?:T|$)/) {
Packit 5b1f1f
      my ($Y, $M, $D) = ($1, $2, $3);
Packit 5b1f1f
      $Y = '0000' if $Y eq '1604'; # iOS magic "no year" value
Packit 5b1f1f
      return "$Y-$M-$D";
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    # V4 format
Packit 5b1f1f
    if (/^(\d{4}|--)(\d{2})(\d{2})(?:T|$)/) {
Packit 5b1f1f
      my ($Y, $M, $D) = ($1, $2, $3);
Packit 5b1f1f
      $Y = '0000' if $Y eq '--';
Packit 5b1f1f
      $Y = '0000' if $Y eq '1604'; # iOS magic "no year" value
Packit 5b1f1f
      return "$Y-$M-$D";
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
  # Convert Our format -> VCard
Packit 5b1f1f
  } else {
Packit 5b1f1f
    # Delete value if special "empty" value
Packit 5b1f1f
    return undef if $_ eq '0000-00-00';
Packit 5b1f1f
Packit 5b1f1f
    # Our format is V3 format
Packit 5b1f1f
Packit 5b1f1f
    # Convert to V4 format if V4 card
Packit 5b1f1f
    if ($Self->V('version') >= 4.0) {
Packit 5b1f1f
      my ($Y, $M, $D) = /^(\d{4})-(\d{2})-(\d{2})/;
Packit 5b1f1f
      $Y = '--' if $Y eq '0000';
Packit 5b1f1f
      $_ = $Y . $M . $D;
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    return $_;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  return undef;
Packit 5b1f1f
}
Packit 5b1f1f
sub VRebuildFN {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
Packit 5b1f1f
  my $NewFN = join " ", map {
Packit 5b1f1f
    $Self->V('n', $_) or ()
Packit 5b1f1f
  } qw(honorificprefixs givennames additionalnames surnames);
Packit 5b1f1f
Packit 5b1f1f
  my $Suffixes = $Self->V('n', 'honorificsuffixes');
Packit 5b1f1f
  $NewFN .= ', ' . $Suffixes if $Suffixes;
Packit 5b1f1f
Packit 5b1f1f
  # FN is a required field, so we have to set it to something
Packit 5b1f1f
  unless ($NewFN) {
Packit 5b1f1f
    $NewFN = $Self->VCompany();
Packit 5b1f1f
  }
Packit 5b1f1f
  unless ($NewFN) {
Packit 5b1f1f
    my ($Email) = $Self->VEmails();
Packit 5b1f1f
    $NewFN = $Email->{value};
Packit 5b1f1f
  }
Packit 5b1f1f
  unless ($NewFN) {
Packit 5b1f1f
    $NewFN = "No Name";
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  $Self->V('fn', 'value', $NewFN);
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VTitle {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  $Self->V('n', 'honorificprefixs', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
sub VFirstName {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    return join " ", map { $_ or () } $Self->V('n', 'givennames'), $Self->V('n', 'additionalnames');
Packit 5b1f1f
  } else {
Packit 5b1f1f
    my ($GivenNames, $AdditionalNames) = split / +/, $_[0], 2;
Packit 5b1f1f
    $Self->V('n', 'givennames', $GivenNames);
Packit 5b1f1f
    $Self->V('n', 'additionalnames', $AdditionalNames);
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
sub VLastName {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  $Self->V('n', 'surnames', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VFN {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  $Self->V('fn', 'value', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VNickname {
Packit 5b1f1f
  shift->V('nickname', 'value', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
sub VBirthday {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    return $Self->VDate($Self->V('bday')) // '0000-00-00';
Packit 5b1f1f
  } else {
Packit 5b1f1f
    $Self->V('bday', 'value', $Self->VDate($_[0], 1));
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VCompany {
Packit 5b1f1f
  shift->V('org', 'company', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
sub VDepartment {
Packit 5b1f1f
  shift->V('org', 'department', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
sub VPosition {
Packit 5b1f1f
  shift->V('title', 'value', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VNotes {
Packit 5b1f1f
  shift->V('note', 'value', @_) // '';
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
my %VBasicTypeMap = (type => 'contact_type', value => 'value');
Packit 5b1f1f
my %VOnlineTypeMap = (type => 'online_type', value => 'online_value');
Packit 5b1f1f
my %VAdrTypeMap = (type => 'contact_type', street => 'streetaddress', city => 'locality', state => 'region', postcode => 'postalcode', country => 'countryname');
Packit 5b1f1f
my %RevVAdrTypeMap = reverse %VAdrTypeMap;
Packit 5b1f1f
Packit 5b1f1f
sub VKN {
Packit 5b1f1f
  my $I = shift;
Packit 5b1f1f
  join "/", map { $I->{$_} } @_;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VIsSame {
Packit 5b1f1f
  my ($Self, $Prop, $E, $N) = @_;
Packit 5b1f1f
Packit 5b1f1f
  if ($Prop eq 'email' || $Prop eq 'tel') {
Packit 5b1f1f
    # If type or value is same, consider it the same item
Packit 5b1f1f
    return 1 if $N->{contact_type} eq $E->{contact_type}
Packit 5b1f1f
      || $N->{value} eq $E->{value};
Packit 5b1f1f
Packit 5b1f1f
  } elsif ($Prop eq 'adr') {
Packit 5b1f1f
    # If type or value is same, consider it the same item
Packit 5b1f1f
    return 1 if $N->{contact_type} eq $E->{contact_type}
Packit 5b1f1f
      || all { ($N->{$_} // '') eq $E->{$_} } @VItemADR;
Packit 5b1f1f
Packit 5b1f1f
  } elsif ($Prop eq 'online') {
Packit 5b1f1f
    # If synthetic online type AND value is same, consider it the same item
Packit 5b1f1f
    return 1 if $N->{contact_type} eq ($E->{online_type} // $E->{contact_type})
Packit 5b1f1f
        && $N->{value} eq ($E->{online_value} // $E->{value});
Packit 5b1f1f
Packit 5b1f1f
  } else {
Packit 5b1f1f
    die "Unknown prop: $Prop";
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VUpdateExisting {
Packit 5b1f1f
  my ($Self, $Prop, $E, $N, $TypeMap) = @_;
Packit 5b1f1f
Packit 5b1f1f
  # Need to update vcard specific properties
Packit 5b1f1f
  if (my $Maps = $VCardTypeMap{$Prop}) {
Packit 5b1f1f
    if (my $ParamType = $Maps->[1]->{$N->{contact_type}}) {
Packit 5b1f1f
      # Make sure only the single right type is present in the vcard type param
Packit 5b1f1f
      my $Types = ($E->{params}->{type} //= []);
Packit 5b1f1f
      @$Types = grep { !$Maps->[0]->{$_} } @$Types;
Packit 5b1f1f
      push @$Types, $ParamType;
Packit 5b1f1f
Packit 5b1f1f
      # Lets try and be smart and update any label
Packit 5b1f1f
      $Self->VUpdateLabel($E, $N) if $Prop eq 'adr';
Packit 5b1f1f
    }
Packit 5b1f1f
    elsif ($N->{contact_type} eq 'other') {
Packit 5b1f1f
      delete $E->{params}->{type};
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
  } else {
Packit 5b1f1f
    die "Unknown prop: $Prop";
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # Now copy over value(s)
Packit 5b1f1f
  $E->{$_} = $N->{$_} for values %$TypeMap;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VUpdateLabel {
Packit 5b1f1f
  my ($Self, $E, $N) = @_;
Packit 5b1f1f
Packit 5b1f1f
  my @Labels;
Packit 5b1f1f
  # In v4, it's a parameter
Packit 5b1f1f
  push @Labels, map { \$_ } @{$E->{params}->{label}};
Packit 5b1f1f
Packit 5b1f1f
  # In v3, it's a separate property. Either in same group...
Packit 5b1f1f
  if (my $Group = $E->{group}) {
Packit 5b1f1f
    for (@{$E->{groups}->{$Group}}) {
Packit 5b1f1f
      push @Labels, \$_->{value} if $_->{name} eq 'label';
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
  # ... or check for label with same type (e.g. 'work', 'home', etc)
Packit 5b1f1f
  if (!@Labels) {
Packit 5b1f1f
    my ($EType) = grep { $VCardAdrTypeMap{$_} } @{$E->{params}->{type} // []};
Packit 5b1f1f
    my $Labels = $Self->{properties}->{label};
Packit 5b1f1f
    if ($EType && $Labels) {
Packit 5b1f1f
      for (@$Labels) {
Packit 5b1f1f
        my ($Type) = grep { $VCardAdrTypeMap{$_} } @{$_->{params}->{type} // []};
Packit 5b1f1f
        push @Labels, \$_->{value} if $Type && $Type eq $EType;
Packit 5b1f1f
      }
Packit 5b1f1f
    }
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  my @EI = @$E{@VItemADR};
Packit 5b1f1f
  my @NI = @$N{@VItemADR};
Packit 5b1f1f
Packit 5b1f1f
  for my $Label (@Labels) {
Packit 5b1f1f
    pairwise {
Packit 5b1f1f
      $$Label =~ s/\b\Q$a\E\b/$b/ if length $a >= 3;
Packit 5b1f1f
    } @EI, @NI;
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub _MakeItem {
Packit 5b1f1f
  my ($Name, $Type, $Value, $Params, @Extra) = @_;
Packit 5b1f1f
  +{
Packit 5b1f1f
    name                              => $Name,
Packit 5b1f1f
    contact_type                      => $Type,
Packit 5b1f1f
    (ref $Value ? 'values' : 'value') => $Value,
Packit 5b1f1f
    params                            => $Params // {},
Packit 5b1f1f
    @Extra,
Packit 5b1f1f
  };
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VNewItem {
Packit 5b1f1f
  my ($Self, $Prop, $N) = @_;
Packit 5b1f1f
  my $Type = $N->{online_type} // $N->{contact_type};
Packit 5b1f1f
  my $Value = $N->{online_value} // $N->{value};
Packit 5b1f1f
Packit 5b1f1f
  my @New;
Packit 5b1f1f
Packit 5b1f1f
  if (my $Maps = $VCardTypeMap{$Prop}) {
Packit 5b1f1f
    my $Params = {};
Packit 5b1f1f
    my %Extra;
Packit 5b1f1f
Packit 5b1f1f
    # Set vcard type parameter
Packit 5b1f1f
    if (my $ParamType = $Maps->[1]->{$Type}) {
Packit 5b1f1f
      $Params->{type} = [ $ParamType ];
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    # Expand address value into array ref components
Packit 5b1f1f
    if ($Prop eq 'adr') {
Packit 5b1f1f
      @Extra{@VItemADR} = @$N{@VItemADR};
Packit 5b1f1f
      $Value = [ \@Extra{@VItemADR} ];
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    $Params->{$NoteParamName} = $N->{note} if $N->{note};
Packit 5b1f1f
    if ($N->{pref}) {
Packit 5b1f1f
      $Params->{type} //= [];
Packit 5b1f1f
      push @{$Params->{type}}, 'pref';
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    push @New, _MakeItem($Prop, $Type, $Value, $Params, %Extra);
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  elsif ($Prop eq 'online') {
Packit 5b1f1f
Packit 5b1f1f
    my $NewMap = $VCardNewOnlineMap{$Type} // $VCardNewOnlineMap{other};
Packit 5b1f1f
    push @New, _MakeItem($_->[0], $Type, $Value, $_->[1])
Packit 5b1f1f
      for @{ref $NewMap eq 'CODE' ? $NewMap->($N->{online_value}) : $NewMap};
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  else {
Packit 5b1f1f
    die "Unknown prop: $Prop";
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  if ($N->{note}) {
Packit 5b1f1f
    $_->{$NoteParamName} = $N->{note} for @New;
Packit 5b1f1f
  }
Packit 5b1f1f
  if ($N->{pref}) {
Packit 5b1f1f
    $_->{pref} = 1 for @New;
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  return @New;
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VL {
Packit 5b1f1f
  my ($Self, $Prop, $TypeMap) = splice @_, 0, 3;
Packit 5b1f1f
  my $Props = $Self->{properties};
Packit 5b1f1f
Packit 5b1f1f
  die "Tried to modify read-only contact, fetch directly, not from cache"
Packit 5b1f1f
    if @_ && $Self->{ReadOnly};
Packit 5b1f1f
Packit 5b1f1f
  my @E = grep { !$_->{deleted} } @{$Props->{$Prop} // []};
Packit 5b1f1f
Packit 5b1f1f
  # Easy part, return items
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    my %Seen;
Packit 5b1f1f
    return map {
Packit 5b1f1f
      my $I = $_;
Packit 5b1f1f
      # dedup. this might be wrong if the second has pref or note
Packit 5b1f1f
      my $VKN = VKN($I, values %$TypeMap);
Packit 5b1f1f
      if ($Seen{$VKN}) {
Packit 5b1f1f
        ();
Packit 5b1f1f
      }
Packit 5b1f1f
      else {
Packit 5b1f1f
        $Seen{$VKN} = 1;
Packit 5b1f1f
        my %Props = mapp { ($a => $I->{$b}) } %$TypeMap;
Packit 5b1f1f
        $Props{pref} = 1 if grep { $_ eq 'pref' } @{$_->{params}->{type} // []};
Packit 5b1f1f
        $Props{note} = $_->{params}->{$NoteParamName}->[0] if $_->{params}->{$NoteParamName};
Packit 5b1f1f
        \%Props;
Packit 5b1f1f
      }
Packit 5b1f1f
    } @E;
Packit 5b1f1f
Packit 5b1f1f
  # Harder part, set items. Try and preserve existing items
Packit 5b1f1f
  } else {
Packit 5b1f1f
    $Self->{vchanged}->{$Prop} = 1;
Packit 5b1f1f
Packit 5b1f1f
    # Find exact existing matches moved to different spot
Packit 5b1f1f
    my %EMap = map { VKN($_, values %$TypeMap) => $_ } @E;
Packit 5b1f1f
Packit 5b1f1f
    my $Pos = 0;
Packit 5b1f1f
Packit 5b1f1f
    my @R;
Packit 5b1f1f
    for my $New (@_) {
Packit 5b1f1f
      my $N = { mapp { $b => ($New->{$a} // '') } %$TypeMap };
Packit 5b1f1f
Packit 5b1f1f
      my @NewItems;
Packit 5b1f1f
Packit 5b1f1f
      # Exact existing item exists (maybe different position)
Packit 5b1f1f
      if (my $E = delete $EMap{VKN($N, values %$TypeMap)}) {
Packit 5b1f1f
        push @NewItems, $E;
Packit 5b1f1f
Packit 5b1f1f
      } else {
Packit 5b1f1f
        my $E = $E[$Pos];
Packit 5b1f1f
Packit 5b1f1f
        # Same item in same position, update value(s)
Packit 5b1f1f
        # Not for online though, we always replace those
Packit 5b1f1f
        if ($Prop ne 'online' && $E && $Self->VIsSame($Prop, $E, $N)) {
Packit 5b1f1f
          # Don't re-use this item
Packit 5b1f1f
          delete $EMap{VKN($E, values %$TypeMap)};
Packit 5b1f1f
Packit 5b1f1f
          $Self->VUpdateExisting($Prop, $E, $N, $TypeMap);
Packit 5b1f1f
Packit 5b1f1f
          push @NewItems, $E;
Packit 5b1f1f
        }
Packit 5b1f1f
Packit 5b1f1f
        # Add new item!
Packit 5b1f1f
        else {
Packit 5b1f1f
          push @NewItems, $Self->VNewItem($Prop, $N);
Packit 5b1f1f
Packit 5b1f1f
        }
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      if (my $Note = $New->{note}) {
Packit 5b1f1f
        $_->{params}->{$NoteParamName} = [ $Note ] for @NewItems;
Packit 5b1f1f
      } else {
Packit 5b1f1f
        delete $_->{params}->{$NoteParamName} for @NewItems;
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      if ($New->{pref}) {
Packit 5b1f1f
        for (@NewItems) {
Packit 5b1f1f
          $_->{params}->{type} //= [];
Packit 5b1f1f
          push @{$_->{params}->{type}}, 'pref';
Packit 5b1f1f
        }
Packit 5b1f1f
      } else {
Packit 5b1f1f
        for (@NewItems) {
Packit 5b1f1f
          $_->{params}->{type} //= [];
Packit 5b1f1f
          @{$_->{params}->{type}} = grep { $_ ne 'pref' } @{$_->{params}->{type}};
Packit 5b1f1f
        }
Packit 5b1f1f
      }
Packit 5b1f1f
Packit 5b1f1f
      # Always add to result list
Packit 5b1f1f
      push @R, @NewItems;
Packit 5b1f1f
      $Pos += @NewItems;
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    # For tel, email, adr, just replace list
Packit 5b1f1f
    if ($Prop eq 'email' || $Prop eq 'tel' || $Prop eq 'adr') {
Packit 5b1f1f
      @{$Props->{$Prop}} = @R;
Packit 5b1f1f
Packit 5b1f1f
    } elsif ($Prop eq 'online') {
Packit 5b1f1f
      # Maps to multiple props. Delete the old ones of types we're replacing
Packit 5b1f1f
      my %ReplaceTypes = map { $_->{contact_type} => 1 } @R;
Packit 5b1f1f
      $_->{deleted} = 1 for grep { $ReplaceTypes{$_->{online_type}} } @E;
Packit 5b1f1f
Packit 5b1f1f
      push @{$Props->{$Prop}}, @R;
Packit 5b1f1f
Packit 5b1f1f
    } else {
Packit 5b1f1f
      die "Unknown prop: $Prop";
Packit 5b1f1f
    }
Packit 5b1f1f
Packit 5b1f1f
    $Self->DeleteUnusedLabels;
Packit 5b1f1f
Packit 5b1f1f
    $Self->VRebuildFN if $Prop eq 'email';
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VEmails {
Packit 5b1f1f
  shift->VL('email', \%VBasicTypeMap, @_);
Packit 5b1f1f
}
Packit 5b1f1f
sub VPhones {
Packit 5b1f1f
  shift->VL('tel', \%VBasicTypeMap, @_);
Packit 5b1f1f
}
Packit 5b1f1f
sub VOnline {
Packit 5b1f1f
  shift->VL('online', \%VOnlineTypeMap, @_);
Packit 5b1f1f
}
Packit 5b1f1f
sub VAddresses {
Packit 5b1f1f
  shift->VL('adr', \%VAdrTypeMap, @_);
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VKind {
Packit 5b1f1f
  shift->V('x-addressbookserver-kind', 'value', @_) // 'contact';
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VGroupContactUIDs {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  my $Props = $Self->{properties};
Packit 5b1f1f
Packit 5b1f1f
  die "Tried to modify read-only contact, fetch directly, not from cache"
Packit 5b1f1f
    if @_ && $Self->{ReadOnly};
Packit 5b1f1f
Packit 5b1f1f
  if (!@_) {
Packit 5b1f1f
    return 
Packit 5b1f1f
      map { s/^urn://; s/^uuid://; $_ }
Packit 5b1f1f
      map { $_->{value} }
Packit 5b1f1f
      @{$Props->{'x-addressbookserver-member'} ||[]};
Packit 5b1f1f
Packit 5b1f1f
  } else {
Packit 5b1f1f
    @{$Props->{'x-addressbookserver-member'}} = map {
Packit 5b1f1f
      {
Packit 5b1f1f
        name => 'x-addressbookserver-member',
Packit 5b1f1f
        params => {},
Packit 5b1f1f
        value => 'urn:uuid:' . $_,
Packit 5b1f1f
      }
Packit 5b1f1f
    } @{$_[0]};
Packit 5b1f1f
Packit 5b1f1f
    $Self->{vchanged}->{'x-addressbookserver-member'} = 1;
Packit 5b1f1f
Packit 5b1f1f
    return @{$_[0]};
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VGroupIds {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  !@_ || die "You can't set GroupIds on a contact, use ME::CalDAV::UpdateGroups";
Packit 5b1f1f
  return sort keys %{$Self->{ABGroups} || {}};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VChanged {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  return keys %{$Self->{vchanged} // {}};
Packit 5b1f1f
}
Packit 5b1f1f
sub VClearChanged {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  delete $Self->{vchanged};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub MFlagged {
Packit 5b1f1f
  return shift->MMeta('SF:flagged', @_) || 0;
Packit 5b1f1f
}
Packit 5b1f1f
sub MImportance {
Packit 5b1f1f
  # Defaults to empty string, make it a number
Packit 5b1f1f
  return shift->MMeta('CY:importance', @_) || 0;
Packit 5b1f1f
}
Packit 5b1f1f
sub MMeta {
Packit 5b1f1f
  my ($Self, $Prop) = (shift, shift);
Packit 5b1f1f
  if (@_) {
Packit 5b1f1f
    $Self->{meta}->{$Prop} = shift;
Packit 5b1f1f
    $Self->{metachanged}->{$Prop} = 1;
Packit 5b1f1f
  }
Packit 5b1f1f
  return $Self->{meta}->{$Prop};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub MChanged {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  return map { [ $_, $Self->{meta}->{$_} ] } keys %{$Self->{metachanged} // {}};
Packit 5b1f1f
}
Packit 5b1f1f
sub MClearChanged {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  delete $Self->{metachanged};
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
sub VPhoto {
Packit 5b1f1f
  my $Self = shift;
Packit 5b1f1f
  !@_ || die "You can't set a photo on a contact (yet)";
Packit 5b1f1f
  my $Prop = $Self->{properties}->{photo}->[0] // undef;
Packit 5b1f1f
  return unless $Prop;
Packit 5b1f1f
Packit 5b1f1f
  # XXX assuming binary (inline)
Packit 5b1f1f
  # v2.1: implied
Packit 5b1f1f
  # v3:   param VALUE=BINARY
Packit 5b1f1f
  # v4:   data: URI
Packit 5b1f1f
Packit 5b1f1f
  # XXX assuming v2.1/v3 TYPE= param
Packit 5b1f1f
  my $Type = exists $Prop->{params}->{type}->[0] ? "image/$Prop->{params}->{type}->[0]" : $FileMagic->checktype_contents($Prop->{value});
Packit 5b1f1f
Packit 5b1f1f
  # XXX using REV for modified, probably safe
Packit 5b1f1f
  my $Modified = $Self->{properties}->{rev}->[0]->{value} // strftime("%Y%m%dT%H%M%SZ", @{[gmtime]});
Packit 5b1f1f
Packit 5b1f1f
  # X-ABCROP-RECTANGLE=ABClipRect_1&0&248&640&640&RulkMf15QMtW5L8kpxRZBw==
Packit 5b1f1f
  my $Crop;
Packit 5b1f1f
  if (exists $Prop->{params}->{'x-abcrop-rectangle'}) {
Packit 5b1f1f
    my ($Label, $X, $Y, $W, $H, $Checksum) = split '&', $Prop->{params}->{'x-abcrop-rectangle'}->[0];
Packit 5b1f1f
    $Crop = {
Packit 5b1f1f
      X => $X,
Packit 5b1f1f
      Y => $Y,
Packit 5b1f1f
      W => $W,
Packit 5b1f1f
      H => $H,
Packit 5b1f1f
    };
Packit 5b1f1f
  }
Packit 5b1f1f
Packit 5b1f1f
  # bundle it all up
Packit 5b1f1f
  return {
Packit 5b1f1f
    Data     => $Prop->{value},
Packit 5b1f1f
    Type     => $Type,
Packit 5b1f1f
    Size     => length $Prop->{value},
Packit 5b1f1f
    Modified => $Modified,
Packit 5b1f1f
    ($Crop ? (Crop => $Crop) : ()),
Packit 5b1f1f
  }
Packit 5b1f1f
}
Packit 5b1f1f
Packit 5b1f1f
# }}}
Packit 5b1f1f
Packit 5b1f1f
1;