odoo2carddav/odoo2carddav

263 lines
7.5 KiB
Perl
Executable File

#!/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";
}