File Coverage

blib/lib/App/Addex/AddressBook/AppleScript.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1 1     1   14621 use 5.20.0;
  1         3  
  1         29  
2 1     1   4 use strict;
  1         1  
  1         25  
3 1     1   3 use warnings;
  1         1  
  1         49  
4             package App::Addex::AddressBook::AppleScript;
5             $App::Addex::AddressBook::AppleScript::VERSION = '0.008';
6 1     1   411 use parent qw(App::Addex::AddressBook);
  1         232  
  1         3  
7             use experimental 'postderef';
8             # ABSTRACT: Mac::Glue-less Addex adapter for Apple Address Book and Addex
9              
10             #pod =head1 SYNOPSIS
11             #pod
12             #pod This module implements the L<App::Addex::AddressBook> interface for Mac OS X's
13             #pod Address Book application, using I<a horrible hack> to get entries from the
14             #pod address book.
15             #pod
16             #pod A much cleaner interface would be to use L<App::Addex::AddressBook::Apple>,
17             #pod which uses L<Mac::Glue> to access the address book. Unfortunately, Mac::Glue
18             #pod does not work in many recent builds of Perl, and will cease to work as the
19             #pod Carbon API is killed off.
20             #pod
21             #pod The AppleScript adapter builds an AppleScript program that prints out a dump of
22             #pod relevant address book entries, then runs it, then parses its output. The
23             #pod format of the intermediate form may change for all kinds of crazy reasons.
24             #pod
25             #pod =cut
26              
27             use App::Addex::Entry;
28             use App::Addex::Entry::EmailAddress;
29             use Encode ();
30              
31             use File::Temp ();
32              
33             sub _produce_applescript {
34             my @fields = (
35             'company', # true / false
36             'organization',
37             'first name',
38             'middle name',
39             'last name',
40             'nickname',
41             'suffix',
42             'note',
43             );
44              
45             my $dumper = '';
46             for my $field (@fields) {
47             $dumper .= <<"END_FIELD_DUMPER";
48             set _this to get $field of _person
49             if $field of _person is not missing value then
50             set _answer to _answer & "- BEGIN $field\n"
51             set _answer to _answer & ($field of _person) & "\n"
52             set _answer to _answer & "- END $field\n"
53             end if
54             END_FIELD_DUMPER
55             }
56              
57             my $osascript = <<'END_APPLESCRIPT';
58             tell application "Address Book"
59             set _people to (get people)
60              
61             set _answer to ""
62              
63             repeat with _person in _people
64             repeat 1 times
65             if count of email of _person = 0 then
66             exit repeat
67             end if
68              
69             set _answer to _answer & "--- BEGIN " & id of _person & "\n"
70              
71             $dumper
72              
73             set _answer to _answer & "- BEGIN email\n"
74             repeat with _email in (get email of _person)
75             set _answer to _answer & (label of _email) & "\n"
76             set _answer to _answer & (value of _email) & "\n"
77             end repeat
78             set _answer to _answer & "- END email\n"
79              
80             set _answer to _answer & "--- END " & id of _person & "\n\n"
81             end repeat
82             end repeat
83              
84             _answer
85             end tell
86             END_APPLESCRIPT
87              
88             $osascript =~ s/\$dumper/$dumper/;
89              
90             return $osascript;
91             }
92              
93             sub _produce_scriptfile {
94             my ($self) = @_;
95              
96             my $osascript = $self->_produce_applescript;
97              
98             my ($fh, $filename) = File::Temp::tempfile(UNLINK => 1);
99              
100             $fh->print($osascript);
101             $fh->close or die "can't close $filename: $!";
102              
103             return $filename;
104             }
105              
106             sub entries {
107             my ($self) = @_;
108              
109             my $script = $self->_produce_scriptfile;
110             my @output = `/usr/bin/osascript $script`;
111              
112             @output = map {; Encode::decode('utf-8', $_) } @output;
113              
114             my @people;
115             my $this;
116             LINE: while (my $line = shift @output) {
117             unless ($this) {
118             next LINE unless $line =~ /\A--- BEGIN (.+)\Z/;
119             $this = { id => $1 };
120             push @people, $this;
121             next LINE;
122             }
123              
124             my @input;
125             if ($line =~ /\A- BEGIN (.+)\Z/) {
126             my $field = $1;
127             push @input, shift @output until @input and $input[-1] =~ /\A- END $1\Z/;
128             pop @input;
129             $this->{ $field } = join q{}, @input;
130             chomp $this->{ $field };
131              
132             if ($field eq 'email') {
133             $this->{emails} = [ split /\n/, delete $this->{email} ];
134             }
135             }
136              
137             if ($line =~ /\A--- END \Q$this->{id}\E\Z/) {
138             undef $this;
139             next LINE;
140             }
141             }
142              
143             my @entries = map {; $self->_entrify($_) } @people;
144              
145             return @entries;
146             }
147              
148             sub _entrify {
149             my ($self, $person) = @_;
150              
151             my %fields;
152             if (my $note = $person->{note} // '') {
153             my @lines = grep { length } split /\R/, $note;
154             for my $line (@lines) {
155             next if $line =~ /^--/; # comment
156              
157             my $tmpname
158             = join q{ }, grep $_,
159             $person->@{'first name', 'middle name', 'last name', 'suffix'};
160              
161             warn("bogus line in notes on $tmpname: $line\n"), next
162             unless $line =~ /\A([^:]+):\s*(.+?)\Z/;
163             $fields{$1} = $2;
164             }
165             }
166              
167             my $fname = $person->{'first name'} // '';
168             my $mname = $person->{'middle name'} // '';
169             my $lname = $person->{'last name'} // '';
170             my $suffix = $person->{suffix} // '';
171              
172             $mname = '' unless $fields{'use middle'} // 1;
173              
174             my $name;
175             if ($person->{company} eq 'true') {
176             $name = $person->{organization};
177             } else {
178             $name = $fname
179             . (length $mname ? " $mname" : '')
180             . (length $lname ? " $lname" : '')
181             . (length $suffix ? " $suffix" : '');
182             }
183              
184             unless (length $name) {
185             warn "couldn't figure out a name for this entry\n";
186             return;
187             }
188              
189             my @emails;
190             my @kv = @{ $person->{emails} };
191              
192             for (my $i = 0; $i < $#kv; $i += 2) {
193             push @emails, App::Addex::Entry::EmailAddress->new({
194             address => $kv[ $i + 1 ],
195             label => $kv[ $i ],
196             });
197             }
198              
199             CHECK_DEFAULT: {
200             if (@emails > 1 and my $default = $fields{default_email}) {
201             my $check;
202             if ($default =~ m{\A/(.+)/\z}) {
203             $default = qr/$1/;
204             $check = sub { $_[0]->address =~ $default };
205             } else {
206             $check = sub { $_[0]->label eq $default };
207             }
208              
209             for my $i (0 .. $#emails) {
210             if ($check->($emails[$i])) {
211             unshift @emails, splice @emails, $i, 1 if $i != 0;
212             last CHECK_DEFAULT;
213             }
214             }
215              
216             warn "no email found for $name matching $fields{default_email}\n";
217             }
218             }
219              
220             my $arg = {
221             name => $name,
222             nick => $person->{nickname},
223             emails => \@emails,
224             fields => \%fields,
225             };
226              
227             return App::Addex::Entry->new($arg);
228             }
229              
230             1;
231              
232             __END__
233              
234             =pod
235              
236             =encoding UTF-8
237              
238             =head1 NAME
239              
240             App::Addex::AddressBook::AppleScript - Mac::Glue-less Addex adapter for Apple Address Book and Addex
241              
242             =head1 VERSION
243              
244             version 0.008
245              
246             =head1 SYNOPSIS
247              
248             This module implements the L<App::Addex::AddressBook> interface for Mac OS X's
249             Address Book application, using I<a horrible hack> to get entries from the
250             address book.
251              
252             A much cleaner interface would be to use L<App::Addex::AddressBook::Apple>,
253             which uses L<Mac::Glue> to access the address book. Unfortunately, Mac::Glue
254             does not work in many recent builds of Perl, and will cease to work as the
255             Carbon API is killed off.
256              
257             The AppleScript adapter builds an AppleScript program that prints out a dump of
258             relevant address book entries, then runs it, then parses its output. The
259             format of the intermediate form may change for all kinds of crazy reasons.
260              
261             =head1 AUTHOR
262              
263             Ricardo Signes <rjbs@cpan.org>
264              
265             =head1 COPYRIGHT AND LICENSE
266              
267             This software is copyright (c) 2010 by Ricardo Signes.
268              
269             This is free software; you can redistribute it and/or modify it under
270             the same terms as the Perl 5 programming language system itself.
271              
272             =cut