Initial commit

This commit is contained in:
Daniel Berteaud 2023-10-24 18:09:43 +02:00
parent 015efc8285
commit a12fa50517
4 changed files with 413 additions and 0 deletions

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
FROM danielberteaud/alpine:latest
MAINTAINER Daniel Berteaud <dbd@ehtrace.com>
ENV PATH_KEEP_VCARDS=0
RUN set -eux &&\
apk --no-cache upgrade &&\
apk add \
perl-app-cpanminus \
openssl \
libxml2 \
libexpat \
perl-net-ssleay \
perl-io-socket-ssl \
&&\
apk add --virtual build-deps \
make \
libxml2-dev \
gcc \
musl-dev \
expat-dev \
perl-dev \
openssl-dev \
&&\
cpanm \
LWP::Protocol::https \
OpenERP::XMLRPC::Client \
Data::UUID \
HTML::Parse \
HTML::FormatText \
Test::utf8 \
URI::Simple \
HTTP::DAV \
Term::ReadKey \
YAML::XS \
Hash::Merge::Simple \
&&\
apk del build-deps &&\
rm -rf /root/.cpanm
COPY odoo2carddav /usr/local/bin
CMD ["odoo2carddav"]

View File

@ -1,2 +1,93 @@
# odoo2carddav
odoo2carddav is a script which can extract contacts from Odoo (using its XMLRPC API), build vcards, and upload them to a carddav server.
It has been tested with Zimbra as carddav server, but should work with others.
You can configure it with a config file
```yaml
---
odoo:
# URL of the Odoo server
url: https://odoo.example.org
# User to connect with
user: odoo2carddav
# Recommended to use an API key
password: <password or API key>
# The name of the database to target on odoo server
database: odoo
# A list of filters to limit contacts returned by odoo
# Note : filters can only be set in a config file (not available as env var or cli arg)
filters:
- ["country_code", "=", "FR"]
- ["name", "ilike", "berteaud"]
dav:
# Url of the dav server
url: https://zimbra.example.org/dav/odoo2carddav%example.org/Odoo
# User and password for the carddav server
user: odoo2carddav
password: <password for the dav server>
# If true, then contacts which exists on the carddav server, but not in odoo will be deleted
delete: True
path:
# Local directory where the script will build vcards. Defaults is to use mkdtemp using /tmp/odoo2carddav.XXXXX as template
workdir: /tmp/odoo2carddav
# If True, vcards won't be deleted from the workdir when the script is finished
keep_vcard: True
```
Then, you can call the script with
```sh
odoo2carddav --config /etc/odoo2carddav.yml
```
You can also configure it with env vars
```
ODOO_URL=https://odoo.example.org \
ODOO_USER=foo \
ODOO_PASSWORD=bar odoo2carddav
```
Or using cli args
```sh
odoo2carddav --config --dav-url=https://webdav.example.org/Contacts/odoo
```
If you combine several sources for the configuration, the precedence will be (from lower to higher priority)
- default values bundled in the script
- environment
- config file
- cli args
The following perl modules are needed to run it
- OpenERP::XMLRPC::Client
- Data::Dumper
- Data::UUID
- Encode
- HTML::Parse
- HTML::FormatText
- Test::utf8
- URI::Simple
- Term::ReadKey
- HTTP::DAV
- Getopt::Long
- YAML::XS
- File::Basename
- File::Path
- File::Temp
- Hash::Merge::Simple
Or you can use the docker image
```
docker run --rm -e ODOO_URL=https://myodoo.acme.org \
-e ODOO_USER=contacts \
-e ODOO_PASSWORD=XXXXXXXXX \
-e DAV_URL=https://radical.acme.org/contacts/odoo \
-e DAV_USER=contacts \
-e DAV_PASSWORD=SuperS3cr3tP@ssW0rd \
-e DAV_DELETE=1
danielberteaud/odoo2vcard
```

33
config.yml.sample Normal file
View File

@ -0,0 +1,33 @@
---
# vim: syntax=yaml
odoo:
# URL of the Odoo server
url: https://odoo.example.org
# User and password (or API key) for Odoo
user: odoo2carddav
password: <password or API key>
# The name of the database to target on odoo server
database: odoo
# A list of filters to limit contacts returned by odoo
# Note : filters can only be set in a config file (not available as env var or cli arg)
filters:
- ["country_code", "=", "FR"]
- ["name", "ilike", "berteaud"]
dav:
# Url of the dav server
url: https://zimbra.example.org/dav/odoo2carddav%example.org/Odoo
# User and password for the carddav server
user: odoo2carddav
password: <password for the dav server>
# If true, then contacts which exists on the carddav server, but not in odoo will be deleted
delete: True
path:
# Local directory where the script will build vcards. Defaults is to use mkdtemp using /tmp/odoo2carddav.XXXXX as template
workdir: /tmp/odoo2carddav
# If True, vcards won't be deleted from the workdir when the script is finished
keep_vcard: True

246
odoo2carddav Executable file
View File

@ -0,0 +1,246 @@
#!/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 Test::utf8 qw(is_within_latin_1);
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 => []
},
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 = ();
foreach my $contact_id (@{$odoo->search('res.partner', [])}){
my $contact = $odoo->read('res.partner', [ $contact_id ])->[0];
if (defined $contact->{is_company}){
print "Skiping $contact->{name} because it's a company\n";
next;
}
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;
}
if (is_within_latin_1($contact->{$_})){
$vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", decode("Latin1", $contact->{$_})) . "\n";
} else {
$vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", $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;
}
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";
$d->put( -local => "$conf->{path}->{workdir}/*.vcf" );
}
if (not $conf->{path}->{keep_vcards}){
print "Deleting vcards from $conf->{path}->{workdir}\n";
rmtree($conf->{path}->{workdir});
}