Initial commit
This commit is contained in:
parent
015efc8285
commit
a12fa50517
|
@ -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"]
|
91
README.md
91
README.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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});
|
||||
}
|
Loading…
Reference in New Issue