|
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;
|