#!/usr/bin/perl use strict; use warnings; use utf8; use OpenERP::XMLRPC::Client; use Data::Dumper; use Data::UUID; use Encode qw(encode decode); use HTML::Parse; use HTML::FormatText; use URI::Simple; use Term::ReadKey; use HTTP::DAV; use Getopt::Long; use YAML::XS qw(LoadFile); use File::Basename; use File::Path qw(rmtree); use File::Temp; use Hash::Merge::Simple qw(merge); my $conf_file = './odoo2carddav.yml'; my $conf = { odoo => { url => undef, user => undef, password => undef, database => undef, filters => [ ["is_company", "=", 0] ] }, dav => { url => undef, user => undef, password => undef, delete => 0 }, path => { workdir => mkdtemp('/tmp/odoo2carddav.XXXXXXXXX'), keep_vcards => 0 } }; my $env_conf = { odoo => { url => $ENV{ODOO_URL}, user => $ENV{ODOO_USER}, password => $ENV{ODOO_PASSWORD}, database => $ENV{ODOO_DATABASE} }, dav => { url => $ENV{DAV_URL}, user => $ENV{DAV_USER}, password => $ENV{DAV_PASSWORD}, delete => $ENV{DAV_DELETE} }, path => { workdir => $ENV{PATH_WORKDIR}, keep_vcards => $ENV{PATH_KEEP_VCARDS} } }; my $cli_conf = {}; GetOptions( "config=s" => \$conf_file, "odoo-url=s" => \$cli_conf->{odoo}->{url}, "odoo-database=s" => \$cli_conf->{odoo}->{database}, "odoo-password=s" => \$cli_conf->{odoo}->{password}, "path-workdir=s" => \$cli_conf->{path}->{workdir}, "path-keep-vcards" => \$cli_conf->{path}->{keep_vcards}, "dav-url=s" => \$cli_conf->{dav}->{url}, "dav-user=s" => \$cli_conf->{dav}->{user}, "dav-password=s" => \$cli_conf->{dav}->{password}, "dav-delete" => \$cli_conf->{dav}->{delete} ); # Remove undefined values from cli and env config, so we can merge conf hashref easily foreach my $hash ($cli_conf, $env_conf){ foreach my $k1 (keys %{$hash}){ foreach my $k2 (keys %{$hash->{$k1}}){ delete $hash->{$k1}->{$k2} unless defined $hash->{$k1}->{$k2}; } delete $hash->{$k1} unless defined $hash->{$k1}; } } # Merge config from various sources. In order : # - default conf inlined in the script # - config file (if it exists) # - cli arguments if (-e $conf_file){ print "Reading $conf_file\n"; $conf = merge($conf, $env_conf, LoadFile($conf_file), $cli_conf); } else { print "Config file $conf_file not found\n"; $conf = merge($conf, $env_conf, $cli_conf); } # Prompt for password if needed if (not defined $conf->{odoo}->{password}) { $conf->{odoo}->{password} = prompt_pass("Enter Odoo password: "); } if (defined $conf->{dav}->{url} and not defined $conf->{dav}->{password}) { $conf->{dav}->{password} = prompt_pass("Enter webdav password for $conf->{dav}->{url}: "); } # Sanitize some input if (not defined $conf->{odoo}->{url}){ die "odoo.url is required\n"; } if (not defined $conf->{odoo}->{user}){ die "odoo.user is required\n"; } if (not defined $conf->{odoo}->{password}){ die "odoo.password is required\n"; } if (defined $conf->{dav}->{url}){ if (not defined $conf->{dav}->{user}){ die "dav.user is needed as you have defined dav.url\n"; } if (not defined $conf->{dav}->{password}){ die "dav.password is needed as you have defined dav.url\n"; } } my $odoo_uri = URI::Simple->new($conf->{odoo}->{url}); my $uuidgen = Data::UUID->new; # Create an API client to query Odoo my $odoo = OpenERP::XMLRPC::Client->new( host => $odoo_uri->host, port => $odoo_uri->port || '443', proto => $odoo_uri->protocol, dbname => $conf->{odoo}->{database}, username => $conf->{odoo}->{user}, password => $conf->{odoo}->{password} ); # Odoo field -> vCard field mapping my $odoo2vcf = { name => 'N', email => 'EMAIL;TYPE=PREF,INTERNET', mobile => 'TEL;TYPE=cell,voice', phone => 'TEL;TYPE=work,voice', function => 'TITLE', commercial_company_name => 'ORG', website => 'URL;TYPE=work', comment => 'NOTE' }; # Prompt for a password in the terminal sub prompt_pass { my $desc = shift; Term::ReadKey::ReadMode('noecho'); print STDERR "$desc"; my $password = Term::ReadKey::ReadLine(0); # Rest the terminal to what it was previously doing Term::ReadKey::ReadMode('restore'); print STDERR "\n"; $password =~ s/\R\z//; return $password; } my %odoo_uuid = (); my $contact_count = 0; print "Querying Odoo API for contacts\n"; foreach my $contact_id (@{$odoo->search('res.partner', $conf->{odoo}->{filters})}){ my $contact = $odoo->read('res.partner', [ $contact_id ])->[0]; my $vcard =<<_EOV; BEGIN:VCARD VERSION:3.0 _EOV foreach (keys %{$odoo2vcf}){ next unless defined $contact->{$_}; if ($_ eq 'comment'){ # Comment is HTML formated, so extract the text only # And replace newline with literal \n $contact->{$_} = HTML::FormatText->new->format(HTML::Parse::parse_html($contact->{$_})); $contact->{$_} =~ s/\r?\n/\\n/g; } # Check if the field contains non ASCII characters. If it does, it's UTF-8, else, it's Latin1 if ($contact->{$_} =~ /([^\x{00}-\x{ff}])/){ $vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", $contact->{$_}) . "\n"; } else { $vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", decode("Latin1", $contact->{$_})) . "\n"; } } my $uuid = lc $uuidgen->to_string( $uuidgen->create_from_name(NameSpace_URL, "$conf->{odoo}->{url}/contacts/$contact->{id}") ); $vcard .= "UID:$uuid\n"; $vcard .= "END:VCARD\n"; # Track UUID present in Odoo, so we can delete contacts from carddav server later $odoo_uuid{$uuid} = 1; open VCARD, ">$conf->{path}->{workdir}/$uuid.vcf"; print VCARD $vcard; close VCARD; $contact_count++; } my $dav_success = 0; my $dav_error = 0; if (defined $conf->{dav}->{url}){ my $d = HTTP::DAV->new(); $d->credentials( -user => $conf->{dav}->{user}, -pass => $conf->{dav}->{password}, -url => $conf->{dav}->{url} ); my $dav_uri = URI::Simple->new($conf->{dav}->{url}); print("Opening webdav collection $conf->{dav}->{url}\n"); $d->open( -url => $conf->{dav}->{url} ) or die("Couldn't open $conf->{dav}->{url}: " . $d->message . "\n"); my $dav_cards = $d->propfind( -url => $dav_uri->path, -depth => 1); if ($conf->{dav}->{delete}) { print "Checking vcards to delete on webdav server\n"; # Iterate over resources in the dav server, and remove it if it doesn't correspond to an odoo contact foreach my $card (@{$dav_cards->get_resourcelist->{_resources}}){ my $card_uri = $card->get_uri->as_string; if ($card->get_property('getcontenttype') ne "text/vcard"){ print "Skipping $card_uri as it's not a .vcf\n"; next; } if (not defined $odoo_uuid{basename($card_uri, ".vcf")}){ print "Card $card_uri exists in dav server, but not in Odoo, removing from dav\n"; $card->delete; } } } print "Uploading vcards to webdav server $conf->{dav}->{url}\n"; foreach my $vcf (glob("$conf->{path}->{workdir}/*.vcf")){ if ($d->put( -local => "$vcf" ) == 1){ $dav_success++; } else { print "An error occured while uploading $vcf : " . $d->message . "\n"; $dav_error++; } } } if (not $conf->{path}->{keep_vcards}){ print "Deleting vcards from $conf->{path}->{workdir}\n"; rmtree($conf->{path}->{workdir}); } print "$contact_count contacts found in Odoo\n"; if (defined $conf->{dav}->{url}){ print "$dav_success contacts successfuly uploaded to $conf->{dav}->{url}\n"; print "$dav_error contacts failed while uploading to $conf->{dav}->{url}\n"; }