diff --git a/Migration/SquirrelMail/addressbook.pl b/Migration/SquirrelMail/addressbook.pl new file mode 100644 index 000000000..129725188 --- /dev/null +++ b/Migration/SquirrelMail/addressbook.pl @@ -0,0 +1,343 @@ +#!/usr/bin/perl + +# Copyright 2011 Inverse inc. +# +# See the enclosed file COPYING for license information (GPL). +# If you did not receive this file, see +# http://www.fsf.org/licensing/licenses/gpl.html + +=head1 NAME + +addressbook.pl - import addressbooks from SquirrelMail + +=head1 SYNOPSIS + +addressbook.pl --help + +addressbook.pl --config [--username ] [--verbose=<0,1,2] + +=head1 DESCRIPTION + +This script imports SquirrelMail .abook files into SOGo. + +=head1 AUTHOR + +=over + +=item Francis Lachapelle + +=back + +=head1 COPYRIGHT + +Copyright (c) 2011 Inverse inc + +This program is available under the GPL. + +=cut + +use diagnostics; +use strict; +use warnings; + +use Config::Simple; +use Pod::Usage; +use Getopt::Long; +use Log::Log4perl; +use Digest::MD5 qw(md5_hex); +use HTTP::Request; +use LWP::UserAgent; +use MIME::Base64; +use XML::Simple; + +$| = 1; + +# Global variables +my $help = undef; +my $conffile = undef; +my $forceusername = undef; +my $username = undef; +my $pwdhash = undef; +my $folder_destination = undef; +my $logLevel = 1; +my @files = (); +my $ua = undef; + +my $cardtemplate = < \$conffile, + "username|u:s" => \$forceusername, + "<>" => \&addFile, + "help|?" => \$help, + "verbose|v:i" => \$logLevel, + ) or pod2usage( -verbose => 1); + +pod2usage( -verbose => 2) if $help; +pod2usage( -verbose => 1) unless ($conffile && scalar(@files) > 0); + +if ($logLevel == 0) { + $logLevel = 'WARN'; +} elsif ($logLevel == 1) { + $logLevel = 'INFO'; +} else { + $logLevel = 'DEBUG'; +} +my $logConf = < %m%n +END + +Log::Log4perl->init( \$logConf ); +my $logger = Log::Log4perl->get_logger(''); + +# +# Read preferences from file +# +my $cfg = new Config::Simple($conffile); + +# +# Verify configuration paramaters +# +foreach ('sogo.url', 'sogo.username', 'sogo.password', 'addressbooks.folder_destination') { + unless ($cfg->param($_)) { + if (m/^(.+)\.(.+)$/) { + $logger->error("The paramter '$2' in the block [$1] is not defined in the configuration file $conffile"); + exit 0; + } + } +} + +# Remove last slash of URL if defined +if (substr($cfg->param('sogo.url'), -1, 1) eq '/') { + my $url = $cfg->param('sogo.url'); + chop $url; + $cfg->param('sogo.url', $url); +} + +# Build password hash +$pwdhash = encode_base64($cfg->param('sogo.username') . ':' . $cfg->param('sogo.password')); + +$username = $forceusername if ($forceusername); + +$ua = LWP::UserAgent->new(); +$ua->agent('Mozilla/5.0'); +$ua->timeout(1800); + +foreach my $filename (@files) { + processFile($filename); +} + +# +# Subroutines +# + +sub addFile { + my $filename = shift; + + push(@files, $filename); +} + +sub processFile { + my $filename = shift; + my $url = undef; + my $count = 0; + my $err = 0; + + unless ($forceusername) { + if ($filename =~ m/^(.+)(\.[^\.]+)$/) { + $username = $1; + } + else { + $username = undef; + } + } + unless ($username) { + $logger->warn("Can't identify owner of file $filename"); + return; + } + if ($url = &addressBookExists($username, $cfg->param('addressbooks.folder_destination'))) { + $logger->warn("[$username] Addressbook \"".$cfg->param('addressbooks.folder_destination')."\" already exists ($url)"); + } + else { + $logger->info("[$username] Addressbook \"".$cfg->param('addressbooks.folder_destination')."\" doesn't exist"); + $url = &createAddressBook($username, $cfg->param('addressbooks.folder_destination')); + } + + if ($url) { + if (open (FILE, $filename)) { + while () { + chomp; + next unless length; + my ($nickname, $givenname, $surname, $mail, $note) = split(/\|/); + my $uid = md5_hex($_); + my $card = sprintf($cardtemplate, $prodid, $uid, "$surname $givenname", $nickname, $mail, $note); + + $count++; + $err++ unless (&createContact($uid, + $url . $uid . ".vcf", + $card)); + } + close FILE; + + $logger->info("[$username] Imported $filename: $count contacts ($err skipped)"); + } + else { + $logger->error("Can't open $filename: $!"); + } + } + else { + $logger->error("[$username] File $filename skipped (missing destination addressbook)"); + } +} + +sub url { + my ($username) = @_; + + return $cfg->param('sogo.url') . "/SOGo/dav/$username/Contacts/"; +} + +sub addressBookExists +{ + my ($username, $addressbook) = @_; + + my $result = 0; + my $propfind = < + + + + + +XML +; + my $request = HTTP::Request->new(); + $request->method('PROPFIND'); + $request->uri(&url($username)); + $request->header('Content-Type' => 'text/xml; charset=utf8'); + $request->header('Content-Length' => length($propfind)); + $request->header('Depth' => 1); + $request->header('Authorization' => "Basic $pwdhash"); + $request->content($propfind); + + my $response = &httpRequest($request, $username); + if ($response) { + my $xml = XMLin($response); + foreach my $ab (@{$xml->{'D:response'}}) { + my $displayname = $ab->{'D:propstat'}->{'D:prop'}->{'D:displayname'}; + $logger->debug("[$username] Found addressbook \"$displayname\""); + if ($addressbook eq $displayname) { + $result = $cfg->param('sogo.url') . $ab->{'D:href'}; + last; + } + } + } + + return $result; +} + +sub createAddressBook { + my ($username, $addressbook) = @_; + + my $result = 0; + my $uid = md5_hex(localtime); + my $url = &url($username) . $uid; + my $proppatch = < + + + + %s + + + +XML +; + + my $request = HTTP::Request->new(); + $request->method('MKCOL'); + $request->uri($url); + $request->header('Authorization' => "Basic $pwdhash"); + + my $response = &httpRequest($request, $username); + if ($response) { + $proppatch = sprintf($proppatch, $addressbook); + $request = HTTP::Request->new(); + $request->method('PROPPATCH'); + $request->uri($url); + $request->header('Content-Type' => 'text/xml; charset=utf8'); + $request->header('Content-Length' => length($proppatch)); + $request->header('Depth' => 0); + $request->header('Authorization' => "Basic $pwdhash"); + $request->content($proppatch); + + $response = &httpRequest($request, $username); + if ($response) { + $logger->info("[$username] Addressbook \"$addressbook\" created ($url)"); + $result = $url . '/'; + } + } + + return $result; +} + +sub createContact { + my ($uid, $url, $card) = @_; + + my $request = HTTP::Request->new(); + $request->method('PUT'); + $request->uri($url); + $request->header('Content-Type' => 'text/vcard; charset=utf-8'); + $request->header('Content-Length' => length($card)); + $request->header('Authorization' => "Basic $pwdhash"); + $request->content($card); + + return (&httpRequest($request, $uid)); +} + +sub httpRequest { + my ($request, $uid) = @_; + + my $result = undef; + my $i; + for ($i = 0; $i < 30; $i++) { + my $response = $ua->request($request); + if ($response->is_success) { + $logger->debug("[$username] HTTP request " . $request->method . " $uid: " . $response->status_line); + $result = $response->decoded_content || 1; + last; + } + else { + $logger->warn("[$username] HTTP request " . $request->method . " $uid: " . $response->status_line); + if ($response->code == 500) { + $logger->warn("[$username] HTTP request " . $request->method . " $uid: sleeping 2 secs"); + sleep(2); + } + else { + $result = 0; + last; + } + } + } + + if ($i == 30) { + $logger->error("[$username] HTTP request " . $request->method . " $uid: Can't reach server for the past 60 secs - exiting."); + exit(-4); + } + + return $result; +} diff --git a/Migration/SquirrelMail/default.conf b/Migration/SquirrelMail/default.conf new file mode 100644 index 000000000..c210904fa --- /dev/null +++ b/Migration/SquirrelMail/default.conf @@ -0,0 +1,7 @@ +[sogo] +url = http://localhost/ +username = sogo +password = qwerty + +[addressbooks] +folder_destination = "Imported Contacts" \ No newline at end of file diff --git a/NEWS b/NEWS index d6f10003a..c0ef6c5e5 100644 --- a/NEWS +++ b/NEWS @@ -1,7 +1,7 @@ 1.3-20111028 (1.3.10) --------------------- New Features - - + - new migration script for SquirrelMail (address books) Enhancements - updated Norwegian translation