#!/usr/bin/perl

my $VERSION = 1.0;

#
# Convert Palm Address datatbase to Yahoo CSV format.
#
# Install this file as padb2csv and csv2padb.
#
# For documentation use pod2text/pod2man on this file or
# run it with '-h' arument.
#
#	Copyright (C) 2001 by Alexander Kolbasov
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of the Artistic License, a copy of which
#    can be found with the Perl distribution.
#
#    This code 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
#    Artistic License for more details.
#
# $Id: padb2csv,v 1.6 2001/12/13 18:48:55 akolb Exp $

use strict;
use File::Basename;
use Getopt::Std;
use Palm::PDB;
use Palm::Address;
use Text::CSV_XS;

my $padb2csv = 'padb2csv';
my $csv2padb = 'csv2padb';
my $p2c;

my $default_db = 'AddressDB.pdb';

my $padb2csv_usage = <<EOU;
Usage: $padb2csv [-v] [-h] [-o] [-e expr] [-m expr] [input_file [output_file]]

\t-h:\tprint help
\t-v:\tbe verbose: print names as they are added.
\t-o:\toverride output file
\t-e expr: Drop records matching expr
\t-m expr: Only process records matching expr

\tinput_file:\tPalm .pdb Address database. Use $default_db by default.
\toutput_file:\tYahoo CSV formatted file. '-' means write to STDOUT.
\tIf no files are given, read from $default_db and write to stdout.

EOU

my $csv2padb_usage = <<EOU;
Usage: $csv2padb [-v] [-h] [-o] [-e expr] [-m expr] [input_file [output_file]]

\t-h:\tprint help
\t-v:\tbe verbose:print names as they are added. 
\t-o:\toverride output file
\t-e expr: Drop records matching expr
\t-m expr: Only process records matching expr

\tinput_file:\tYahoo CSV formatted address file. '-' means use STDIN.
\toutput_file:\tPalm .pdb file. Use $default_db if no output_file is gven.
\tIf no files are given, read from stdin and write to $default_db

EOU

my %opts;
#
# Parse arguments.
#

my $name = basename($0, ".pl");

die "This program should be called either $padb2csv or $csv2padb"
    if $name ne $padb2csv && $name ne $csv2padb;

$p2c = 1 if $name eq $padb2csv;

my $usage = $p2c ? $padb2csv_usage : $csv2padb_usage;

getopts('hovm:e:', \%opts) || ((print STDERR $usage), exit 1);

((print $usage), exit 0) if $opts{h};

my $argc = @ARGV;

die $usage if $argc > 2;

my $verbose = 1 if $opts{v};
my $overwrite = 1 if $opts{o};
my $match = $opts{m};
my $except = $opts{e};

# Get input file
my $input = ($argc ? $ARGV[0] : ($p2c ? $default_db : '-'));
my $output = ($argc == 2 ? $ARGV[1] : ($p2c ? '-' : $default_db));

# Check output file for overwrites.
die "File $output already exist, use -o flag if want to overwrite $output\n"
    if ($output ne '-' && ! $overwrite && stat $output);

exit ($p2c ? palm2csv($input, $output): csv2palm($input, $output));

sub palm2csv {
    my ($input, $output) = @_;
    my $OUT;
    my ($work, $home, $fax, $other, $mail, $main, $pager, $mobile);
    my ($work1, $home1, $fax1, $other1, $mail1, $main1, $pager1, $mobile1);
    my ($work_p, $home_p, $fax_p, $other_p, $mail_p, $main_p, $pager_p, $mobile_p);
    my @cats;
    my $i = 0;
    my $pdb = new Palm::PDB;
    my $csv = new Text::CSV_XS;

    if ($output eq '-') {
	$OUT = \*STDOUT;
    } else {
	open (F, "> $output") ||
	    die "can't open $output for writing";
	$OUT = \*F;
    }

    # Read Palm database
    $pdb->Load($input);

    # Find indices for various mail types.
    for (@Palm::Address::phoneLabels) {
	$work   = $i if /Work/;
	$home   = $i if /Home/;
	$fax    = $i if /Fax/;
	$other  = $i if /Other/;
	$mail   = $i if /mail/;
	$main   = $i if /Main/;
	$pager  = $i if /Pager/;
	$mobile = $i if /Mobile/;
	$i++;;
    }

    my @categories   = @{$pdb->{appinfo}{categories}};

    # Build array of categories indexed by category ID.
    for my $c (@categories) {
	$cats[$c->{id}] = $c->{name};
    }

    print $OUT <<EOF;
"First","Middle","Last","Nickname","Email","Category","Distribution Lists","Yahoo! ID","Home","Work","Pager","Fax","Mobile","Other","Yahoo! Phone","Primary","Alternate Email 1","Alternate Email 2","Personal Website","Business Website","Title","Company","Work Address","Work City","Work State","Work ZIP","Work Country","Home Address","Home City","Home State","Home ZIP","Home Country","Birthday","Anniversary","Custom 1","Custom 2","Custom 3","Custom 4","Comments"
EOF

    # Parse address database
    for my $r (@{$pdb->{records}}) {
	my @fl;
	my $status;
	my $category = $cats[$r->{category}];

	# Find what phoneN means
	for ($i = 1; $i < 6; $i++) {
	    $work1   = $i if ($r->{phoneLabel}{"phone$i"} == $work);
	    $home1   = $i if ($r->{phoneLabel}{"phone$i"} == $home);
	    $fax1    = $i if ($r->{phoneLabel}{"phone$i"} == $fax);
	    $other1  = $i if ($r->{phoneLabel}{"phone$i"} == $other);
	    $mail1   = $i if ($r->{phoneLabel}{"phone$i"} == $mail);
	    $main1   = $i if ($r->{phoneLabel}{"phone$i"} == $main);
	    $pager1  = $i if ($r->{phoneLabel}{"phone$i"} == $pager);
	    $mobile1 = $i if ($r->{phoneLabel}{"phone$i"} == $mobile);
	}
	# Figure out various phones
	$work_p   = $r->{fields}{"phone$work1"}   if $work1;
	$home_p   = $r->{fields}{"phone$home1"}   if $home1;
	$fax_p    = $r->{fields}{"phone$fax1"}    if $fax1;
	$other_p  = $r->{fields}{"phone$other1"}  if $other1;
	$mail_p   = $r->{fields}{"phone$mail1"}   if $mail1;
	$main_p   = $r->{fields}{"phone$work1"}   if $main1;
	$pager_p  = $r->{fields}{"phone$pager1"}  if $pager1;
	$mobile_p = $r->{fields}{"phone$mobile1"} if $mobile1;

	#
	# Construct list of fields required by Yahoo CSV.
	# Push all field values in the right order to a single list and then
	# use csv->combine() method to convert the list into a CSV record.
	#
	push @fl, $r->{fields}{firstName};	# First
	push @fl, '';			# Middle
	push @fl, $r->{fields}{name};	# Last
	push @fl, '';			# Nickname
	push @fl, $mail_p;		# Mail
	push @fl, $category;		# Category
	push @fl, '';			# Distribution
	push @fl, '';			# Yahoo Id
	push @fl, $home_p;		# Home
	push @fl, $work_p;		# Work
	push @fl, $pager_p;		# Pager
	push @fl, $fax_p;		# Fax
	push @fl, $mobile_p;		# Mobile
	push @fl, $other_p;		# Other
	push @fl, '';			# Yahoo phone
	push @fl, $main_p;		# Primary
	push @fl, '';			# Alt e-mail 1
	push @fl, '';			# Alt e-mail 2
	push @fl, '';			# Personal website
	push @fl, '';			# Business website
	push @fl, $r->{fields}{title};	# Title
	push @fl, $r->{fields}{company};# Company
	push @fl, '';			# Work Address
	push @fl, '';			# Work City
	push @fl, '';			# Work State
	push @fl, '';			# Work ZIP
	push @fl, '';			# Work COuntry
	push @fl, $r->{fields}{address};# Home addr
	push @fl, $r->{fields}{city};	# Home City
	push @fl, $r->{fields}{state};	# Home State
	push @fl, $r->{fields}{zipCode};# Home Zip
	push @fl, $r->{fields}{country};# Home country
	push @fl, '';			# Birthday
	push @fl, '';			# Anniversary
	push @fl, $r->{fields}{custom1};# Custom1
	push @fl, $r->{fields}{custom2};# Custom2
	push @fl, $r->{fields}{custom3};# Custom3
	push @fl, $r->{fields}{custom4};# Custom4
	push @fl, $r->{fields}{note};	# Comments

	$status = $csv->combine (@fl);

	if (!$status) {
	    my $err = $csv->error_input;
	    print STDERR "parse() failed on argument: ", $err, "\n";
	} else {
	    $_ = $csv->string;

	    # Filter out record if required
	    next if $match && ! /$match/;
	    next if $except && /$except/;

	    # Write CSV record
	    print $OUT "$_\n";
	    print STDERR "$r->{fields}{firstName} $r->{fields}{name}\n" 
		if $verbose;
	}
    }
    return 0;
}

sub csv2palm {
    my ($input, $output) = @_;
    my $CSV;
    my $pdb = new Palm::Address;
    my $csv = new Text::CSV_XS;
    my %categories = (Business => 1, Personal => 1, QuickList => 1);
    my %phones = (Work => 0,
		  Home => 1,
		  Fax => 2,
		  Other => 3,
		  Email => 4,
		  'Personal Website' => 5,
		  Pager => 6,
		  Mobile => 7);

    my %phones_prim = (work => 'Work',
		       home => 'Home',
		       fax => 'Fax',
		       other => 'Other',
		       email => 'Email',
		       pager => 'Pager',
		       mobile => 'Mobile');

    # Map Pilot fields names to Yahoo field names.
    my %fields_map = (name	=> 'Last',
		      firstName	=> 'First',
		      company	=> 'Company',
		      address	=> 'Home Address',
		      city	=> 'Home City',
		      state	=> 'Home State',
		      zipCode	=> 'Home ZIP',
		      country	=> 'Home Country',
		      title	=> 'Title',
		      custom1	=> 'Custom 1',
		      custom2	=> 'Custom 2',
		      custom3	=> 'Custom 3',
		      custom4	=> 'Custom 4',
		      note	=> 'Comments');

    my $primary;

    my @csv_phones = qw(Home Work Mobile Email Pager Fax Other);
    push @csv_phones, 'Personal Website';

    my @columns;
    my %fields;		# Indices of each field hashed by field name
    my $i;
    my ($work_p, $home_p, $fax_p, $other_p, $mail_p, $main_p, $pager_p, $mobile_p);
    #
    # List of existing categories. Always create standard Palm categories.
    # New categories are added to this list.
    #
    my %cats = (Unfiled => 0,
		Busines => 1,
		Personal => 2,
		QuickList => 3);
    
    my $cat_id = 4;

    # Get input file descriptor.
    if ($input eq '-') {
	$CSV = \*STDOUT;
    } else {
	open (F, "< $input") ||
	    die "can't open $input for reading";
	$CSV = \*F;
    }

    # Read and parse the header.
    my $header = <$CSV>;

    $csv->parse($header) or die "Bad header \"$header\", can't proceed!\n";

    @columns = $csv->fields();

    # Get field names and record index of each field into %fields.
    for my $f (@columns) {
	$fields{$f} = $i++;
    }

    # Add standard categories.
    $pdb->addCategory('Business');
    $pdb->addCategory('Personal');
    $pdb->addCategory('QuickList');

    print STDERR "Saving database to $output....\n" if $verbose;

    # Process each record
    while (<$CSV>) {
	my @palm_phones = qw();
	my @palm_types = qw();

	# Filter records
	next if $match && ! /$match/;
	next if $except && /$except/;

	if (! $csv->parse($_)) {
	    print STDERR "can't parse string [$_]\n";
	} else {
	    @columns = $csv->fields();
	    # Create new address record
	    my $r = $pdb->new_Record;

	    # Get category information.
	    my $cat = $columns[$fields{'Category'}];
	    if (($cat ne 'Unfiled') && $categories{$cat} != 1) {
		# Add this category to the list of categories.
		$categories{$cat} = 1;
		$pdb->addCategory ($cat, $cat_id);
		$cats{$cat} = $cat_id++;
	    }
	    # Specify record category.
	    $r->{category} = $cats{$cat};

	    # Deal with primary phone.
	    $primary = $columns[$fields{'Primary'}];
	    $primary = $phones_prim{$primary};

	    if ($primary && $columns[$fields{$primary}]) {
		push @palm_phones, $columns[$fields{$primary}];
		push @palm_types, $phones{$primary};
	    }

	    for my $p (@csv_phones) {
		my $phone = $columns[$fields{$p}];
		if ($phone && ($p ne $primary)) {
		    push @palm_phones, $phone;
		    push @palm_types, $phones{$p};
		}
	    }

	    # Assign phones to 5 available slots.
	    my $nphones = @palm_phones;
	    $nphones = 5 if $nphones > 5;

	    for ($i = 1; $i <= $nphones; $i++) {
		$r->{phoneLabel}{"phone$i"} = $palm_types[$i - 1];
		$r->{fields}{"phone$i"} = $palm_phones[$i - 1];
	    }

	    #
	    # Specify other fields
	    #
	    for my $k (keys %fields_map) {
		$r->{fields}{$k} = $columns[$fields{$fields_map{$k}}];
	    }

	    # Add record to the database
	    $pdb->append_Record($r);
	    print STDERR "$r->{fields}{firstName} $r->{fields}{name}\n" 
		if $verbose;
	}
	# Save new database
	$pdb->Write ($output);
    }
    return 0;
}

#
# POD section.
# 

=head1 NAME

padb2csv, csv2padb - Convert between Palm Address datatbase and Yahoo! CSV
format.

=head1 SYNOPSYS

padb2csv [B<-v>] [B<-h>] [B<-o>] [B<-m> expr] [B<-e> expr] [input_file [output_file]]

csv2padb [B<-v>] [B<-h>] [B<-o>] [B<-m> expr] [B<-e> expr] [input_file [output_file]]

=head1 OPTIONS

    -h:	print help
    -v:	be verbose:print names as they are added. 
    -o:	override output file
    -e expr: Drop records matching expr
    -m expr: Only process records matching expr

=head2 csv2padb:

input_file:  Yahoo CSV formatted address file. '-' means use I<STDIN>.

output_file: Palm .pdb file. Use F<AddressDB.pdb> if no output_file is gven.

If no files are given, read from stdin and write to F<AddressDB.pdb>.

=head2 padb2csv:

input_file: Palm .pdb Address database. Use F<AddressDB.pdb> by default.

output_file: Yahoo CSV formatted file. '-' means write to I<STDOUT>.

If no files are given, read from F<AddressDB.pdb> and write to stdout.

=head1 DESCRIPTION

Convert to/from Palm Pilot Adress book database format file and Yahoo!
Address Book CSV format.It creates a whole address book database that you
can download directly into your Palm-compatible device, overwriting existing
address database.

You may use desktop tools like I<Jpilot> to view the resulting database
before you actually download or sync it. Always keep backups in case
something goes wrong.

If you have some addresses in your PDA and some addresses in your Yahoo
address book, you may manually merge them by uploading your converted
database to Yahoo and then putting all of your Yahoo database into PDA.

You may select which records to convert using filtering options - they match
all fields of the record. If you create new entries on your PDA, you may
create a special category for them (e.g. C<New>) and then extract only these
records:

  padb2csv -v -e New AddressDB.pdb Yahoo.csv

=head1 CAVEATS

There is no 1-to-1 mapping between Yahoo fields and Palm fields. Yahoo
address book includes much more information, so if you convert your data to
.pdb format and back you may loose some of it. The best way is to keep your
master database on Yahoo and periodycally sync it with PDA. As an
alternative, you may only use Yahoo fields that have corresponding PDA
fields.

If you run padb2csv or csv2padb without arguments it will try to quietly
create an output database.

=head1 SEE ALSO

L<Palm::PDB>, L<Palm::Address>, L<Text::CSV_XS>.

=head1 README

This is csv2padb and padb2csv - tools for conversion between Palm Address
book database and Yahoo! CSV format.

Install this file as padb2csv and csv2padb in your PATH.

For documentation use pod2text/pod2man on padb2csv or run it with '-h'
arument.

	Copyright (C) 2001 by Alexander Kolbasov

   This program is free software; you can redistribute it and/or
   modify it under the terms of the Artistic License, a copy of which
   can be found with the Perl distribution.

   This code 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
   Artistic License for more details.

=head1 PREREQUISITES

This script requires the following modules:

C<strict>, C<File::Basename>, C<Getopt::Std>, C<Palm::PDB>, C<Palm::Address>,
C<Text::CSV_XS>.

=head1 AUTHOR

Alexander Kolbasov <akolb@netgate.net>

=head1 BUGS

This program may die if something goes wrong due to the way Palm::PDB
works. It inherits all the bugs and limitations of Palm::PDB and Text::CSV_XS.
In particular, it can not accept a multi-line CSV entry.



=pod OSNAMES

any (Only tested on Unix).

=pod SCRIPT CATEGORIES

Mail

=cut