#!/usr/bin/perl # Copyright (c) 2009 Jonathan Kamens . # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # To read the full GNU General Public License, see # . # Converts a CSV generated by Outlook 2007 contacts export into a CSV # that can more easily be imported by Thunderbird 3. # # Specify the name of the Outlook-generated CSV file on the command # line, or feed it via stdin. # # Thunderbird-compatible CSV is generated to stdout. # # After generating the Thunderbird-compatible CSV and saving it to a # *.csv file, run Tools > Import... in Thunderbird, tell it you want # to import Address Books, then tell it you want to import a CSV file, # then open the CSV file you generated. # # Then check the column mappings. If they're wrong, it's probably # because the Thunderbird you're using has different address book # fields from mine, or puts them in a different order. Adjust the # @columns array below to compensate for this, generate a new CSV, and # try again. use strict; use warnings; use File::Basename; use Getopt::Long; use POSIX qw(strftime); use Text::CSV; my $whoami = basename $0; # -2 means Thunderbird 2 compatibility. # -3 means Thunderbird 3 compatibility. my $usage = "Usage: $whoami [-2|-3]\n"; my($t2, $t3); die $usage if (! GetOptions("2" => \$t2, "3" => \$t3, )); die "Don't specify both -2 and -3\n$usage" if ($t2 && $t3); # First item in each array is the Thunderbird field name. Remainder # of columns are Outlook fields to look in, or subroutine references # to call. First Outlook field that has a value, or the first # subroutine to return a value, is the one that gets used. The # subroutine gets passed a hashref containing the Outlook fields. my(@columns) = ( [ 'First Name', 'First Name' ], [ 'Last Name', 'Last Name' ], [ 'Display Name', \&display_name ], [ 'Nickname' ], [ 'Primary Email', \&email1 ], [ 'Secondary Email', \&email2 ], [ 'Screen Name' ], [ 'Work Phone', 'Business Phone' ], [ 'Home Phone', 'Home Phone' ], [ 'Fax Number', 'Business Fax', 'Home Fax', 'Other Fax' ], [ 'Pager Number', 'Pager' ], [ 'Mobile Number', 'Mobile Phone' ], [ 'Home Address', 'Home Street' ], [ 'Home Address 2', 'Home Street 2' ], [ 'Home City', 'Home City' ], [ 'Home State', 'Home State' ], [ 'Home ZipCode', 'Home Postal Code' ], [ 'Home Country', 'Home Country/Region' ], [ 'Work Address', 'Business Street' ], [ 'Work Address 2', 'Business Street 2' ], [ 'Work City', 'Business City' ], [ 'Work State', 'Business State' ], [ 'Work ZipCode', 'Business Postal Code' ], [ 'Work Country', 'Business Country/Region' ], [ 'Job Title', 'Job Title' ], [ 'Department', 'Department' ], [ 'Organization', 'Company' ], [ 'Web Page 1', 'Web Page' ], [ 'Web Page 2' ], [ 'Birth Year', \&birth_year ], [ 'Birth Month', \&birth_month ], [ 'Birth Day', \&birth_day ], [ 'Custom 1', 'User 1' ], [ 'Custom 2', 'User 2' ], [ 'Custom 3', 'User 3' ], [ 'Custom 4', 'User 4' ], [ 'Notes', 'Notes' ], ); if ($t2) { # Thunderbird 2 doesn't have 'Screen Name' field @columns = grep($_->[0] ne 'Screen Name', @columns); } elsif ($t3) { # nothing to do here } my $csv = Text::CSV->new({binary => 1}); if (@ARGV) { open(STDIN, "<", $ARGV[0]) or die "open($ARGV[0]): $!\n"; shift @ARGV; die "Extra arguments: @ARGV\n" if (@ARGV); } { my $header_ref = $csv->getline(\*STDIN) or die; $csv->column_names(@$header_ref); } $csv->combine(map($_->[0], @columns)); print $csv->string(), "\n"; while (my $row = $csv->getline_hr(\*STDIN)) { my @values; foreach my $colref (@columns) { my($thunderbird, @outlook) = @$colref; my $val = ''; foreach my $outlook (@outlook) { if (ref $outlook eq 'CODE') { $val = &$outlook($row); } else { $val = $row->{$outlook}; } last if $val; } push(@values, $val); } $csv->combine(@values); print $csv->string(), "\n"; } sub birth_parts { my($row) = @_; my($birthday) = $row->{Birthday}; return('', '', '') if (! $birthday); return('', '', '') if ($birthday =~ m,^[0/]+$,); if ($birthday !~ m,^(\d+)/(\d+)/(\d+)$,) { warn "Invalid birthday: $birthday\n"; return('', '', ''); } my($month, $day, $year) = ($1, $2, $3); # Ugh! if ($year < 100) { my($this_year) = strftime("%Y", localtime()); my($century) = $this_year - ($this_year % 100); $year += $century; $year -= 100 while ($year > $this_year); } return($year, $month, $day); } sub birth_year { (&birth_parts(@_))[0]; } sub birth_month { (&birth_parts(@_))[1]; } sub birth_day { (&birth_parts(@_))[2]; } sub display_name { my($row) = @_; $row->{'First Name'} . ' ' . $row->{'Last Name'}; } sub emails { my($row) = @_; # Return all email address, ignoring bogus Exchange addresses # without any '@' signs in them. grep(/\@/, map($row->{$_}, 'E-mail Address', 'E-mail 2 Address', 'E-mail 3 Address')); } sub email1 { (&emails(@_))[0] || ''; } sub email2 { (&emails(@_))[1] || ''; } # $Id: outlook-contacts-to-thunderbird.pl,v 1.6 2009/09/11 14:30:58 jik Exp $