#!/usr/bin/perl # # gc2fab - Google Contacts to FastMail.FM Address Book # # This script copies (parts of) data in Google Contacts to the # address book at FastMail.FM. Following data are copied: # # name, email addresses and phone numbers (cell, home, work) # # The script uses SOAP (SOAP is not "simple"!) to access data # stored at FastMail.FM. The SOAP::MessaginEngine module can be # found at: # # http://wiki.fastmail.fm/index.php?title=FastServicesPerl # # Otherwise, just save the script, install the necessary modules, # change usernames and passwords (read the comments) and just run it. # # The script uses Google Contacts as the master source of data. On # the first run, all contacts will be copied to FastMail.FM. The copied # contacts will have a note that begins with "gbook:". This can be # used to search for contacts that are being kept of by this script. # # Contacts deleted from Google Contacts will be deleted from # the address book. Updated contacts will be... updated (what a # surprise!). # # Changes made to the copied contacts in FastMail.FM will *not* be # copied back to Google Contacts (it's easy enough to add a fix for # this tough, but I don't need it right now). # # This script does the trick for me. If it works for you - fine. # When you find a bug, please send me a patch. I'm saying "when" # and not "if" - all code has bugs. # # Also, please consider subscribing to the fm-talk mailing list: # # https://mail.krot.org/mailman/listinfo/fm-talk # # Thanks. # # P.S. Use at own risk, don't blame me (or Google or FastMail) if something # goes wrong or all your addresses are deleted. # # Copyright (c) by Kirill Miazine # # This software is distributed under an ISC-style license, please see # for details. # use strict; use IO::All; use LWP::UserAgent; use JSON; use URI; use Date::Parse; use Date::Format; use SOAP::MessagingEngine; import SOAP::Data qw(name value); # Gmail username my $gmuser = 'km@krot.org'; # Gmail password (put password in ~/.gmpw or enter it here) my $gmpass = io("$ENV{'HOME'}/.gmpw")->chomp->getline; # FastMail username my $fmuser = 'km@krot.org'; # FastMail password (put password in ~/.fmpw or enter it here) my $fmpass = io("$ENV{'HOME'}/.fmpw")->chomp->getline; my $ua = LWP::UserAgent->new(); my $res = $ua->post('https://www.google.com/accounts/ClientLogin', {accountType => 'HOSTED_OR_GOOGLE', Email => $gmuser, Passwd => $gmpass, service => 'cp', source => 'krot-gmail2fastmail-1'}); die $res->status_line if !$res->is_success; my %params = map { split /=/ } split /\n/, $res->content; my ($sid, $lsid, $token) = @params{qw(SID LSID Auth)}; my $req_url = 'https://www.google.com/m8/feeds/contacts/default/thin'; my %req_hdr = (Authorization => "GoogleLogin auth=$token",'GData-Version' => 2); my %req_params = (alt => 'json', showdeleted => 'true'); my $res = $ua->get(urlqq($req_url, %req_params, 'max-results' => 0), %req_hdr); my $gdata = from_json($res->content()); my $count = int($gdata->{'feed'}->{'openSearch$totalResults'}->{'$t'}); exit if !$count; my @contacts; my $start_index = 1; my $max_results = 50; $req_params{'max-results'} = $max_results; while (1) { $req_params{'start-index'} = $start_index; $res = $ua->get(urlqq($req_url, %req_params), %req_hdr); my $gdata = from_json($res->content(), {utf8 => 1}); for my $i (@{$gdata->{'feed'}->{'entry'}}) { my $id = $i->{'id'}->{'$t'}; my $tag = (split /\//, $id)[-1]; my $updated = int(str2time($i->{'updated'}->{'$t'})); my $deleted = exists $i->{'gd$deleted'} ? 1 : 0; my $full = $i->{'title'}->{'$t'}; my $first = $full; my $last = ''; if ($full =~ /\s+/) { my @names = split /\s+/, $full; $last = pop @names; $first = join ' ', @names; } my (@email, @cell, @home, @work); @email = map { $_->{'address'} } @{$i->{'gd$email'}}; @cell = map { $_->{'$t'} } grep { $_->{'rel'} =~ /\#mobile/ } @{$i->{'gd$phoneNumber'}}; @home = map { $_->{'$t'} } grep { $_->{'rel'} =~ /\#home/ } @{$i->{'gd$phoneNumber'}}; @work = map { $_->{'$t'} } grep { $_->{'rel'} =~ /\#work/ } @{$i->{'gd$phoneNumber'}}; push @contacts, {id => $id, tag => $tag, updated => $updated, name => $full, first => $first, last => $last, email => \@email, cell => \@cell, home => \@home, work => \@work}; if ($deleted) { delete @{$contacts[-1]}{qw(name first last email cell home work)}; $contacts[-1]->{'deleted'} = 1; } } $start_index += $max_results; last if $start_index >= $count; } my @gcontacts = grep { !exists $_->{'deleted'} } @contacts; my @gdeleted = grep { exists $_->{'deleted'} } @contacts; my $sess = SOAP::MessagingEngine->new('https://www.fastmail.fm/SOAP/', $fmuser, $fmpass) or exit; my ($table, $fields, $fvals, $crit, $rows); $table = name(Table => 'Addresses'); $fields = name( FieldList => value([name(Field => 'AddressId'), name(Field => 'LastUpdate'), name(Field => 'FirstName'), name(Field => 'SurName'), name(Field => 'Notes')]) ); $rows = $sess->Database_Select($table, $fields); my @fcontacts = $rows ? map { ($_->{'tag'} = delete $_->{'notes'}) =~ s/^gbook:(\S+?)(:(\d+))?$/$1/; $_->{'updated'} = (int($3) || $_->{'updated'}); $_ } grep { $_->{'notes'} =~ /^gbook:/ } map { {id => $_->[0], updated => int(str2time($_->[1]) + 21600), first => $_->[2], last => $_->[3], notes => $_->[4] } } @{$rows} : (); my %gbt = map { $_->{'tag'} => $_ } @gcontacts; my %fbt = map { $_->{'tag'} => $_ } @fcontacts; for my $i (@gdeleted) { my $tag = $i->{'tag'}; next if !exists $fbt{$tag}; my $aid = $fbt{$tag}->{'id'}; $crit = name(CritList => value([name(Crit => { Field => 'AddressId', Op => '=', Value => $aid })])); $table = name(Table => 'Contacts'); $sess->Database_Delete($table, $crit); $table = name(Table => 'Addresses'); $sess->Database_Delete($table, $crit); delete $fbt{$tag}; } for my $i (@gcontacts) { my $tag = $i->{'tag'}; my $aid = exists $fbt{$tag} ? $fbt{$tag}->{'id'} : 0; $fvals = name( FieldValueList => value([ map { name(FieldValue => $_) } ( {Field => 'FirstName', Value => $i->{'first'}}, {Field => 'SurName', Value => $i->{'last'}}, {Field => 'Notes', Value => "gbook:$tag:" . time}, ) ]) ); my $refresh = 1; if (!$aid) { print "Inserting address $tag ($i->{'name'})\n"; $table = name(Table => 'Addresses'); $aid = $sess->Database_Insert($table, $fvals); } elsif ($gbt{$tag}->{'updated'} > $fbt{$tag}->{'updated'}) { print "Address $tag ($i->{'name'}) is updated:\n"; print " ", ctime($gbt{$tag}->{'updated'}), " (G)\n"; print " ", ctime($fbt{$tag}->{'updated'}), " (F)\n"; $crit = name(CritList => value([name(Crit => { Field => 'AddressId', Op => '=', Value => $aid })])); $table = name(Table => 'Addresses'); $sess->Database_Update($table, $fvals, $crit); $table = name(Table => 'Contacts'); my $res = $sess->Database_Delete($table, $crit); } else { $refresh = 0; } next if !$refresh; my $entry = sub { return (name(Table => 'Contacts'), name( FieldValueList => value([ map { name(FieldValue => $_) } ( {Field => 'AddressId', Value => $aid}, {Field => 'ContactType', Value => shift}, {Field => 'Details', Value => shift}, {Field => 'IsDefault', Value => shift}, ) ]) )); }; my $default = 1; for my $j (@{$i->{'email'}}) { $sess->Database_Insert($entry->(0, $j, $default)); $default = 0; } for my $j (@{$i->{'cell'}}) { $sess->Database_Insert($entry->(3, $j, 0)); } for my $j (@{$i->{'home'}}) { $sess->Database_Insert($entry->(1, $j, 0)); } for my $j (@{$i->{'work'}}) { $sess->Database_Insert($entry->(2, $j, 0)); } } $sess->Logout(); sub urlqq { my ($url, %args) = @_; my $uri = URI->new($url); while (my ($key, $val ) = each %args) { $args{$key} = ref $val eq 'HASH' ? [%{$val}] : $val; } $uri->query_form(%args) if %args; return $uri->canonical(); }