package Mac::Glue::Apps::AddressBookExport; use strict; use Encode qw(decode); use Carp; use Template; use Text::vCard::Addressbook; our $VERSION = 0.2; my $is_mac = 0; $is_mac = 1 if $^O eq 'darwin'; require Mac::Glue if $is_mac; =head1 NAME Mac::Glue::Apps::AddressBookExport - Export from Address Book and publish =head1 SYNOPSIS use Mac::Glue::Apps::AddressBookExport; my $exporter = Mac::Glue::Apps::AddressBookExport->new({ 'glue_name' => 'Address Book', 'template_dir' => '/path/to/template_dir/', 'out_dir' => '/where/to/write/out/pages_dir/', 'skip_with_image' => 1, }); $exporter->export_address_book(); This will generate an index page (with names and phone numbers) in your 'out_dir'. It will also create 'out_dir/people/.html' which will be all those people who's name starts with that letter, showing name, phone numbers, emails and addresses. =head1 DESCRIPTION This package uses Mac::Glue to export all vCards from your Apple Address Book. I can write this file to disk, or process the results using Text::vCard and Template Toolkit to generate web pages. =head2 new() my $exporter = Mac::Glue::Apps::AddressBookExport->new({ 'glue_name' => 'Address Book', 'template_dir' => '/path/to/template_dir/', 'out_dir' => '/where/to/write/out/pages_dir/', 'skip_with_image' => 1, }); All options can be set here, rather than having to be passed to each method, but equally options can be set as each method is called (overwriting what is already in the object). This constructor doesn't actually do anything other than store the options submitted to it and return the object. See the examples directory for template examples. =cut sub new { my ($class,$self) = @_; # Looks like they didn't supply params $self = {} unless $self && ref($self) eq 'HASH'; bless($self, $class); return $self; } =head2 export_address_book() $exporter->export_address_book({ 'glue_name' => 'Address Book', # Default # Optional dump of vcard file 'out_file' => '/full/path/to/outfile.vcf', # Generate templates 'template_dir' => '/path/to/template_dir/', 'out_dir' => '/where/to/write/out/pages_dir/', }); The out_file will be version 3.0 of the vCard spec, you can use Text::vCard::Addressbook to parse this. If 'template_dir' and 'out_dir' are specified then the generate_web_pages() will be called after the address book has been exported. =cut sub export_address_book { my $self = shift; $self->_pop_from_conf(shift); croak "You can not export unless running on a mac" unless $is_mac; # Get from address book $self->get_vcards_from_addressbook(); if(defined $self->{out_file}) { # User wanted to write to disk open(FH,'>' . $self->{out_file}) or carp "Could not print $!"; print FH $self->{vcards}; close(FH); } # See if they want to process locally if(defined $self->{template_root} && defined $self->{out_dir}) { $self->generate_web_pages(); } } =head2 generate_web_pages() $exporter->generate_web_pages({ 'template_root' => '/path/to/template/dir', 'out_dir' => '/path/results/printed/to', 'vcards' => $vcard_data, }); This method is called automatically from export_address_book() if 'template_root' and 'out_dir' have been specified. The template_dir should contain 'index.html' and 'by_name.html' template (see examples directory of this package for a starting point), but it will check and just not process if they are missing. 'vcards' will have already been populated if export_address_book() has been called. Alternativly you can supply the vcard data which is submitted to Text::vCard::Addressbook as source_text. =cut sub generate_web_pages { my $self = shift; $self->_pop_from_conf(shift); croak "'template_root' not a directory or does not exist" unless -d $self->{template_root}; croak "'out_dir' not a directory or does not exist" unless -d $self->{out_dir}; croak "No 'vcards' data defined" unless defined $self->{vcards}; my %config = ( INCLUDE_PATH => $self->{template_root}, # Add cache options ? ); my $tt = Template->new(\%config); # Get the Text::vCard object $self->{address_book} = Text::vCard::Addressbook->new({ 'source_text' => $self->{vcards}, }); # Sort my $vcards = $self->{address_book}->vcards; if(-r $self->{template_root} . '/index.html') { # use TT for output my %vals = ( 'people' => $vcards, 'first_letter' => \&get_first_letter_from_vcard, ); my $file; my $template_response = $tt->process('index.html',\%vals, \$file); carp $tt->error if $tt->error; open(FH,'>' . $self->{out_dir} . '/index.html'); print FH $file; close(FH); } if(-r $self->{template_root} . '/by_name.html') { # Print each person my $people_dir = $self->{out_dir} . '/people'; mkdir $people_dir unless -d $people_dir; # sort out who goes where my %letters; foreach my $vcard (@{$vcards}) { my $letter = get_first_letter_from_vcard($vcard); unless(defined $letters{$letter}) { my @store; $letters{$letter} = \@store; } push(@{$letters{$letter}}, $vcard); } # Generate the pages while(my($letter,$vcards) = each %letters) { my %vals = ( 'people' => $vcards, 'letter' => $letter, 'letters' => \%letters, 'first_letter' => \&get_first_letter_from_vcard, ); my $file; my $template_response = $tt->process('by_name.html',\%vals, \$file); carp $tt->error() if $tt->error(); open(FH,">$people_dir/$letter.html") or carp "Could not open file $people_dir/$letter.html $!"; print FH $file; close(FH); } } } =head2 get_vcards_from_addressbook() my $vcards = $exporter->get_vcards_from_addressbook({ 'skip_with_image' => 1, 'glue_name' => 'Address Book', }); This is the method which calls Mac::Glue and extracts the vcard information from Apples Address Book. 'skip_with_image' is there so that and person with an image associated can be skipped on the export. This is because Text::vCard can not support it currently. The glue_name defaults to 'Address Book', but as you can set it when you create your glue (see Mac::Glue) this option allows you to overwrite the default. =cut sub get_vcards_from_addressbook { croak "You can not export from Apple Address Book unless on a mac" unless $is_mac; my $self = shift; $self->_pop_from_conf(shift); my $glue_name = $self->{'glue_name'} || 'Address Book'; my $address = new Mac::Glue $glue_name; # get everyone my @people = $address->prop('people')->get; my $vcards; for my $person (@people) { # Loop and get each persons card # Skip if they have a photo as Text::vFile::asData can't cope next if $person->prop('image')->get && defined $self->{'skip_with_image'}; $vcards .= $person->prop('vcard')->get; } $address->quit(); return $self->{vcards} = $vcards; } =head2 get_first_letter_from_vcard() This is a function, NOT a method. my $first_letter = get_first_letter_from_vcard($vcard,'fullname'); This function can take two arguments. The First must be a Text::vCard object. The second is optional (defaulting to 'fullname') which is the method to call on the object. The first letter of the return value from the method is then returned upper cased. This is used to sort which page each person is put on and in the template to generate the links for each person and which letters are active. =cut sub get_first_letter_from_vcard { my $vcard = shift; return '?' unless $vcard; my $method = shift || 'fullname'; my $name = $vcard->$method() || 'x'; my $l = ''; $l = decode('utf-8',substr($name,0,1)) if $name ne ''; if($l eq '') { $l = 'other'; } return uc($l); } # So they can use each method seperatly, or from previous configs sub _pop_from_conf { my $self = shift; if(my $conf = shift) { # Take everything out of conf and stick in self map { $self->{$_} = $conf->{$_} } keys %{$conf}; } } =head1 NOTES One could extend this package to upload the vCard file to a server and have that then generate webpages, but for now that's an exercise for the user! I'm just going to rsync my pages up. If anyone wants to extend this to inserting info into a DB or something else please let me know. =head1 AUTHOR Leo Lapworth, LLAP@cuckoo.org =head1 COPYRIGHT Copyright (c) 2005 Leo Lapworth. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO Mac::Glue Text::vCard Template =cut 1;