File Coverage

blib/lib/Net/iContact.pm
Criterion Covered Total %
statement 79 231 34.2
branch 8 70 11.4
condition 0 2 0.0
subroutine 20 30 66.6
pod 9 9 100.0
total 116 342 33.9


line stmt bran cond sub pod time code
1             package Net::iContact;
2             # vim: set expandtab tabstop=4 shiftwidth=4 autoindent smartindent:
3 14     14   353427 use warnings;
  14         39  
  14         462  
4 14     14   93 use strict;
  14         28  
  14         687  
5              
6 14     14   78 use Carp qw/carp croak/;
  14         29  
  14         958  
7 14     14   80 use Digest::MD5 qw/md5_hex/;
  14         29  
  14         713  
8 14     14   89090 use HTTP::Request::Common qw/PUT GET/;
  14         486724  
  14         1283  
9 14     14   16362 use LWP::UserAgent;
  14         8972996  
  14         505  
10 14     14   22969 use XML::Bare;
  14         316308  
  14         954  
11 14     14   160 use Data::Dumper;
  14         29  
  14         965  
12              
13             ### Not importing; don't want XML::Generator's AUTOLOAD
14             require XML::Generator;
15              
16 14     14   85 use constant API_BASE => 'http://api.icontact.com/icp/core/api/v1.0/';
  14         27  
  14         125876  
17              
18             our $VERSION = '0.02';
19             our $AUTOLOAD;
20              
21             our %ok_field;
22             for (qw|error username password api_key secret token seq debug ua|) { $ok_field{$_}++ };
23              
24             ### catchall accessor
25             sub AUTOLOAD {
26 16     16   1413 my $name = $AUTOLOAD;
27 16         84 $name =~ s/.*://;
28 16 50       68 return if $name =~ /^[A-Z]+$/;
29              
30 16 50 0     55 carp "Invalid attribute or method" and return unless $ok_field{$name};
31 16         109 return $_[0]->{$name};
32             }
33             sub new {
34 3 50   3 1 53 $#_ >= 4 or croak "Invalid number of arguments";
35              
36 3         11 my ($class_name, $user, $pass, $api_key, $secret) = @_;
37 3 50       83 my $debug = $_[5] if ($#_ == 5);
38              
39 3         35 $pass = md5_hex($pass);
40 3         45 my ($self) = { 'username' => $user,
41             'password' => $pass,
42             'api_key' => $api_key,
43             'secret' => $secret,
44             ## Token and sequence number are given to us by
45             ## the app after login
46             'token' => '',
47             'seq' => 0,
48             ## debug mode
49             'debug' => $debug,
50             ## keep one of these around
51             'ua' => LWP::UserAgent->new,
52             };
53 3         908755 bless($self, $class_name);
54 3         19 return $self;
55             }
56              
57             sub login {
58 1     1 1 6 my $self = shift;
59 1         11 my $call = 'auth/login/' . $self->username . '/' . $self->password;
60 1         34 my $root = $self->get($call, {'api_key' => $self->api_key });
61 0 0       0 return unless $root;
62              
63 0         0 $self->{'token'} = $root->{response}->{auth}->{token}->{value};
64 0         0 $self->{'seq'} = $root->{response}->{auth}->{seq}->{value};
65 0         0 return 1;
66             }
67              
68             ### Most of this code is shared with all of the GET calls.
69             ### _getcall takes a string (the RHS of `my $call =') and a coderef
70             ### (callback for processing the tree XML::Bare returns).
71             ###
72             ### It returns a subroutine that preforms the requested call.
73             sub _getcall {
74 126     126   238 my ($cstr, $munge) = @_;
75              
76 126         188 my $ret = q|sub {
77             my $self = shift;my $call = CALL;my %args = @_;
78             my $root = $self->get($call, { $self->_stdargs, %args });
79             unless ($root) {
80             warn 'Got error: ' . $self->error->{code} . ': ' . $self->error->{message} . "\n";
81             return;
82             }
83             return $munge->($root);
84             }|;
85 126         578 $ret =~ s/CALL/$cstr/;
86 126 0   0   25793 return eval $ret;
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
87             }
88              
89             ### Build the GET functions with _getcall.
90             *contacts = _getcall q("contacts"),
91             sub { _to_arrayref(shift->{response}->{contact}) };
92             *contact = _getcall q("contact/" . shift),
93             sub { _to_hashref (shift->{response}->{contact}) };
94             *subscriptions = _getcall q{"contact/" . shift() . "/subscriptions"},
95             sub { _to_subs(shift->{response}->{contact}->{subscription})};
96             *campaigns = _getcall q("campaigns"),
97             sub { _to_arrayref(shift->{response}->{campaigns}->{campaign})};
98             *campaign = _getcall q("campaign/" . shift),
99             sub { _to_hashref (shift->{response}->{campaign})};
100             *lists = _getcall q("lists"),
101             sub { _to_arrayref(shift->{response}->{lists}->{list})};
102             *list = _getcall q("list/" . shift),
103             sub { _to_hashref (shift->{response}->{list})};
104             *custom_fields = _getcall q{"contact/" . shift() . "/custom_fields"},
105             sub { _to_cfields (shift->{response}->{contact}->{custom_fields}->{custom_field})};
106             *stats = _getcall q{"message/" . shift() . "/stats"},
107             sub { _to_stats (shift->{response}->{message}->{stats})};
108              
109              
110             sub putmessage {
111 0     0 1 0 my ($self, $subject, $campaign, $text, $html) = @_;
112 0         0 my $call = 'message';
113              
114 0         0 my $X = new XML::Generator ':pretty';
115 0         0 my $xml = $X->xml($X->message(
116             $X->subject($X->xmlcdata($subject)),
117             $X->campaign($campaign),
118             $X->text_body($X->xmlcdata($text)),
119             $X->html_body($X->xmlcdata($html)),
120             ))->stringify;
121              
122 0         0 my $root = $self->put($call, { $self->_stdargs }, $xml);
123 0 0       0 return unless $root;
124              
125 0         0 return $root->{response}->{result}->{message}->{id}->{value};
126             }
127              
128             sub putcontact {
129 0     0 1 0 my ($self, $contact, $id) = @_;
130 0 0       0 my $call = 'contact' . ($id ? "/$id" : '');
131              
132 0         0 my $X = new XML::Generator ':pretty';
133 0         0 my $xml = $X->xml($X->contact(($id ? { 'id' => $id } : { }),
134 0 0       0 map {$X->$_($contact->{$_})} keys %$contact,
135             ))->stringify;
136              
137 0         0 my $root = $self->put($call, { $self->_stdargs }, $xml);
138 0 0       0 return unless $root;
139              
140 0         0 return $root->{response}->{result}->{contact}->{id}->{value};
141             }
142              
143             sub putsubscription {
144 0     0 1 0 my ($self, $contactid, $listid, $status) = @_;
145 0         0 my $call = "contact/$contactid/subscription/$listid";
146              
147 0         0 my $X = new XML::Generator ':pretty';
148 0         0 my $xml = $X->xml($X->subscription({'id' => $listid},
149             $X->status($status),
150             ))->stringify;
151              
152 0         0 my $root = $self->put($call, { $self->_stdargs }, $xml);
153             ## False on failure, 1 on success.
154 0 0       0 return unless $root;
155 0         0 return 1;
156             }
157              
158             sub gen_sig {
159 3     3 1 7 my ($self,$call,$args) = @_;
160 3         19 my $sig = $self->secret;
161 3         9 $sig .= $call;
162 3         17 for my $key (sort (keys(%$args))) {
163 3         15 $sig .= $key . $args->{$key};
164             }
165 3         26 return md5_hex($sig);
166             }
167              
168             sub gen_url {
169 2     2 1 6 my ($self,$call,$args) = @_;
170 2         8 my $url = API_BASE . $call;
171 2         11 my $sig = $self->gen_sig($call, $args);
172              
173 2         6 $url .= "/?api_sig=$sig";
174              
175 2         14 while (my ($key,$val) = each(%$args)) {
176 2         13 $url .= "&$key=$val";
177             }
178 2         7 return $url;
179             }
180              
181             sub get {
182 1     1 1 3 my ($self,$call,$args) = @_;
183              
184             ## Generate the URL and make the call
185 1         12 my $url = $self->gen_url($call, $args);
186 1 50       8 warn "GET'ing: $url\n\n" if $self->debug;
187 1         7 my $response = $self->ua->request(GET $url);
188 1 50       593005 croak "Could not make API call: GET/$call" unless $response->is_success;
189              
190 0         0 $self->{'seq'}++;
191 0         0 my $xml = $response->content;
192 0 0       0 warn "\n\nGot:\n$xml\n\n" if $self->debug;
193              
194             ## Parse it, check for errors
195 0         0 my $root = _parse($xml);
196 0 0       0 unless (_success($root)) {
197 0         0 $self->{'error'} = _get_error($root);
198 0         0 return;
199             }
200 0         0 return $root;
201             }
202              
203             sub put {
204 0     0 1 0 my ($self,$call,$args,$xml) = @_;
205 0         0 $args->{'api_put'} = $xml;
206              
207             ## Generate the URL and make the call
208 0         0 my $url = $self->gen_url($call, $args);
209 0 0       0 warn "PUT'ing: $url\ncontent:\n$xml\n\n" if $self->debug;
210 0         0 my $response = $self->ua->request(PUT $url, Content => $xml);
211 0 0       0 croak "Could not make API call: PUT/$call" unless $response->is_success;
212              
213 0         0 $self->{'seq'}++;
214 0         0 $xml = $response->content;
215 0 0       0 warn "\n\nGot:\n$xml\n" if $self->debug;
216              
217             ## Parse it, check for errors
218 0         0 my $root = _parse($xml);
219 0 0       0 unless (_success($root)) {
220 0         0 $self->{'error'} = _get_error($root);
221 0         0 return;
222             }
223              
224 0         0 return $root;
225             }
226              
227             ### The following subs are for internal use only.
228              
229             # _get_error( ROOT )
230             #
231             # Extracts the error code and message from the given XML.
232             #
233             # ROOT: an XML::Bare root node
234             #
235             # Returns a hashref. (See C).
236              
237              
238             sub _get_error {
239 1     1   411 my $root = shift;
240 1         8 return { 'code' => $root->{response}->{error_code}->{value},
241             'message' => $root->{response}->{error_message}->{value}, };
242             }
243              
244             # _parse( TEXT )
245             #
246             # Parses the XML in the first argument.
247             #
248             # TEXT: scalar string containing XML
249             #
250             # Returns an XML::Bare root node.
251              
252             sub _parse {
253 3     3   142 my $xml = new XML::Bare( text => shift );
254 3         133 return $xml->parse;
255             }
256              
257             # _success( ROOT )
258             #
259             # Check whether or not a call was successful based on the XML returned.
260             #
261             # ROOT: an XML::Bare root node
262             #
263             # Returns true on success and false on failure
264              
265             sub _success {
266 0     0   0 my $root = shift;
267 0         0 my $status = $root->{response}->{status}->{value};
268 0 0       0 return unless $status eq 'success'; # false return on failure,
269 0         0 return 1; # otherwise 1
270             }
271              
272             # _to_arrayref( NODE )
273             #
274             # Convert an XML::Bare hash tree to an array.
275             # Returns an arrayref.
276              
277             sub _to_arrayref {
278 1     1   87 my @ret;
279 1         2 my $ar = shift;
280 1 50       5 if (ref($ar) eq 'ARRAY') {
281 1         3 for my $item (@$ar) {
282 2         7 push @ret, $item->{id}->{value};
283             }
284             } else {
285 0 0       0 if (defined($ar->{id}->{value})) {
286 0         0 push @ret, $ar->{id}->{value};
287             }
288             }
289 1         6 return \@ret;
290             }
291              
292             # _to_hashref( NODE )
293             #
294             # Convert an XML::Bare hash tree to a hash.
295             # Returns a hashref.
296              
297             sub _to_hashref {
298             ## Convert the tree returned by XML::Bare into a "normal" hashref,
299             ## so keys can be accessed without excessive referenceage..
300 1     1   102 my %ret;
301 1         3 my $ar = shift;
302 1         6 while (my ($key,$val) = each(%$ar)) {
303 1 50       4 next if $key =~ /(value|pos)/;
304 1         163 $ret{$key} = $val->{value};
305             }
306 0           return \%ret;
307             }
308              
309             # _to_subs( NODE )
310             #
311             # Convert an XML::Bare hash tree to a hash. Special case for
312             # GET call `subscriptions'
313              
314             sub _to_subs {
315 0     0     my $subscription = shift;
316 0           my %ret;
317              
318 0 0         if (ref($subscription) eq 'ARRAY') {
319 0           for my $item (@$subscription) {
320 0           $ret{$item->{id}->{value}} = $item->{status}->{value};
321             }
322             } else {
323 0 0         if (defined($subscription->{id}->{value})) {
324 0           $ret{$subscription->{id}->{value}} = $subscription->{status}->{value};
325             }
326             }
327 0           return \%ret;
328             }
329              
330             # _to_cfields( NODE )
331             #
332             # Convert an XML::Bare hash tree to a hash. Special case for
333             # GET call `custom_fields'
334              
335             sub _to_cfields {
336 0     0     my %ret;
337 0           my $fields = shift;
338 0 0         if (ref($fields) eq 'ARRAY') {
339 0           for my $item (@$fields) {
340 0           $ret{$item->{name}->{value}} = {
341             'formal_name' => $item->{formal_name}->{value},
342             'value' => $item->{value}->{value},
343             'type' => $item->{type}->{value},
344             }
345             }
346             } else {
347 0 0         if (defined($fields->{name}->{value})) {
348 0           $ret{$fields->{name}->{value}} = {
349             'formal_name' => $fields->{formal_name}->{value},
350             'value' => $fields->{value}->{value},
351             'type' => $fields->{type}->{value},
352             }
353             }
354             }
355              
356 0           return \%ret;
357             }
358              
359             # _to_stats( NODE )
360             #
361             # Convert an XML::Bare hash tree to a hash. Special case for
362             # GET call `stats'
363              
364             sub _to_stats {
365 0     0     my $stats = shift;
366 0           my %ret;
367 0           for (qw/bounces released unsubscribes forwards complaints opens clicks/) {
368 0           $ret{$_} = {
369             'count' => $stats->{$_}->{count}->{value},
370             'percent' => $stats->{$_}->{percent}->{value},
371             }
372             }
373 0           $ret{opens}->{unique} = $stats->{opens}->{unique}->{value};
374 0           $ret{clicks}->{unique} = $stats->{clicks}->{unique}->{value};
375              
376 0           return \%ret;
377             }
378              
379              
380              
381             # _stdargs( SELF )
382             #
383             # Return a hash containing the standard arguments to api calls.
384             # Requires access to $self.
385              
386             sub _stdargs {
387 0     0     my $self = shift;
388 0           return ( 'api_key' => $self->api_key,
389             'api_seq' => $self->seq,
390             'api_tok' => $self->token, );
391             }
392              
393             =head1 NAME
394              
395             Net::iContact - iContact API
396              
397             =head1 VERSION
398              
399             Version 0.02
400              
401             =head1 SYNOPSIS
402              
403             use Net::iContact;
404              
405             my $api = Net::iContact->new('user', 'pass', 'key', 'secret');
406             $api->login();
407             for my $list (keys %{$api->lists}) {
408             print "ID: " . $list->{'id'} . "\n";
409             print "Name: " . $list->{'name'} . "\n";
410             }
411             ...
412              
413             =head1 ACCESSORS
414              
415             The following functions take no arguments and return the property
416             indicated in the name.
417              
418             =head2 error( )
419              
420             Returns the last error recieved, if any, as a hashref containing two
421             keys: code, and message.
422              
423             Example:
424             print "Error code: " . $api->error->{'code'};
425              
426             =head2 username( )
427              
428             Returns the username that was supplied to the constructor.
429              
430             =head2 password( )
431              
432             Returns an md5 hash of the password that was supplied to the
433             constructor.
434              
435             =head2 api_key( )
436              
437             Returns the api key.
438              
439             =head2 secret( )
440              
441             Returns the shared secret.
442              
443             =head2 token( )
444              
445             Returns the current token, if authenticated.
446              
447             =head2 seq( )
448              
449             Returns the current sequence number, if authenticated.
450              
451             =head2 new( USERNAME, PASSWORD, APIKEY, SECRET, [DEBUG] )
452              
453             The constructor takes four scalar arguments and an optional fifth:
454              
455             USERNAME: your iContact username
456             PASSWORD: your iContact password
457             APIKEY: the API key given to your application
458             SECRET: the shared secret given to your application
459             DEBUG: turns on debugging output. Optional, default is zero.
460              
461             When DEBUG is true, Net::iContact will print the URLs it calls and the
462             XML returned on STDERR.
463              
464             Example:
465             my $api = Net::iContact->new('user', 'pass', 'key', 'secret');
466              
467             =head2 login( )
468              
469             Logs into the API. Takes no arguments, returns true on success and
470             false on error.
471              
472             Example:
473             my $ret = $api->login;
474             unless ($ret) {
475             print 'Error ' . $api->error->{'code'} . ': '
476             . $api->error->{'message'} . "\n";
477             }
478              
479             =head1 API GET METHODS
480              
481             For more details on the API calls implemented below, see the API
482             documentation: L
483              
484             =head2 contacts( [FIELDS] )
485              
486             Search for contacts.
487              
488             FIELDS: optional hash of search criteria
489              
490             Returns an arrayref of all found contact IDs. If called with no
491             arguments, returns all contacts in the account.
492              
493             Example:
494             my $contacts = $api->contacts(); # get all contacts
495             ## get all contacts with @example.com email addresses and the first
496             ## name 'Steve'
497             $contacts = $api->contacts( 'email' => '*@example.com',
498             'fname' => 'Steve');
499             for my $id (@$contacts) {
500             # ...
501             }
502              
503             =head2 contact( ID )
504              
505             ID: numeric contact ID
506              
507             Returns a hashref representing the contact with the given ID.
508             See C
509              
510             Example:
511             my $contact = $api->contact($id);
512             print $contact->{fname} .' '. $contact->{lname} .' <'. $contact->{email} . ">\n";
513              
514             =head2 subscriptions( ID )
515              
516             ID: numeric contact ID
517              
518             Returns a hashref of the given contact's subscriptions.
519             See C
520              
521             =head2 custom_fields( ID )
522              
523             ID: numeric contact ID
524              
525             Returns a hashref of the given contact's custom fields.
526             See C
527              
528             =head2 campaigns( )
529              
530             Returns an arrayref of all campaign IDs defined in the account, or a
531             false value on error.
532              
533             =head2 campaign( ID )
534              
535             ID: numeric campaign ID
536              
537             Returns a hashref representing the campaign with the given ID.
538             See C.
539              
540             =head2 lists( )
541              
542             Returns an arrayref of all list IDs defined in the account, or a false
543             value on error.
544              
545             =head2 list( ID )
546              
547             ID: numeric list ID
548              
549             Returns a hashref representing the list with the given ID.
550             See C.
551              
552             =head2 stats( ID )
553              
554             ID: numeric message ID
555              
556             Returns a hashref containing stats for the given message ID.
557              
558             =head1 API PUT FUNCTIONS
559              
560             For more details on the API calls implemented below, see the API
561             documentation: L
562              
563             =head2 putmessage( SUBJECT, CAMPAIGN, TEXT_BODY, HTML_BODY )
564              
565             Create a message.
566              
567             SUBJECT: subject of the message
568             CAMPAIGN: campaign to use
569             TEXT_BODY: text part of the message
570             HTML_BODY: html part of the message
571              
572             Returns the ID of the created message on success, or a false value on
573             failure.
574              
575             =head2 putcontact( CONTACT, [ID] )
576              
577             Insert or update a contact's info.
578              
579             CONTACT: hashref of contact info
580             ID: optional contact ID
581              
582             The CONTACT hashref has the following possible keys:
583              
584             =over 4
585              
586             =item * fname
587              
588             =item * lname
589              
590             =item * email
591              
592             =item * prefix
593              
594             =item * suffix
595              
596             =item * buisness
597              
598             =item * address1
599              
600             =item * address2
601              
602             =item * city
603              
604             =item * state
605              
606             =item * zip
607              
608             =item * phone
609              
610             =item * fax
611              
612             =back
613              
614             Returns the ID of the contact on success, or a false value on failure.
615              
616             =head2 putsubscription( CONTACTID, LISTID, STATUS )
617              
618             Update a contact's subscription.
619              
620             CONTACTID: contact ID to update
621             LISTID: list ID
622             STATUS: CONTACTID's subscription to LISTID (eg 'subscribed',
623             'unsubscribed', 'deleted'...)
624              
625             =head1 MISC FUNCTIONS
626              
627             The following functions are intended for internal use, but may be useful
628             for debugging purposes.
629              
630             =head2 gen_sig( METHOD, ARGS )
631              
632             Generates an api signature.
633              
634             METHOD: scalar name of the method to be called
635             ARGS: a hashref of arguments to above method
636              
637             Returns the generated signature string.
638              
639             =head2 gen_url( METHOD, ARGS )
640              
641             Generates the URL to call, including the api_sig.
642              
643             METHOD: scalar name of the method to be called
644             ARGS: a hashref of arguments to above method
645              
646             Returns the URL generated.
647              
648             Example:
649             my $url = $api->gen_url('auth/login/' . $api->username . '/'
650             . $api->password, { 'api_key' => $api->api_key });
651              
652             =head2 get( METHOD, ARGS )
653              
654             Makes an API GET call.
655              
656             METHOD: scalar name of the method to be called
657             ARGS: a hashref of arguments to above method
658              
659             Returns the raw XML recieved from the API.
660              
661             =head2 put( METHOD, ARGS, XML )
662              
663             Makes an API PUT call.
664              
665             METHOD: scalar name of the method to be called
666             ARGS: hashref of arguments to above method
667             XML: XML to PUT
668              
669             Returns the raw XML recieved from the API.
670              
671             =head1 AUTHOR
672              
673             Ian Kilgore, C<< >>
674              
675             =head1 BUGS
676              
677             Need better documentation of return values (possibly documentation with
678             Dumper output of the return values).
679              
680             Net::iContact does not yet support authenticating to accounts with
681             multiple client folders.
682              
683             This module makes no attempt to deal with being rate-limited by the API.
684              
685             =head1 TODO
686              
687             PUT methods that are not provided at this time:
688              
689             =over 4
690              
691             =item * message/[message_id]/sending_info
692              
693             =back
694              
695             GET methods that are not provided at this time:
696              
697             =over 4
698              
699             =item * message/[id]/stats/opens
700              
701             =item * message/[id]/stats/clicks
702              
703             =item * message/[id]/stats/bounces
704              
705             =item * message/[id]/stats/unsubscribes
706              
707             =item * message/[id]/stats/forwards
708              
709             =back
710              
711             Please report any bugs or feature requests to
712             C, or through the web interface at
713             L.
714             I will be notified, and then you'll automatically be notified of progress on
715             your bug as I make changes.
716              
717             =head1 SUPPORT
718              
719             You can find documentation for this module with the perldoc command.
720              
721             perldoc Net::iContact
722              
723             You can also look for information at:
724              
725             =over 4
726              
727             =item * AnnoCPAN: Annotated CPAN documentation
728              
729             L
730              
731             =item * CPAN Ratings
732              
733             L
734              
735             =item * RT: CPAN's request tracker
736              
737             L
738              
739             =item * Search CPAN
740              
741             L
742              
743             =back
744              
745             =head1 ACKNOWLEDGEMENTS
746              
747             =head1 SEE ALSO
748              
749             L
750              
751             =head1 COPYRIGHT & LICENSE
752              
753             Copyright 2007 iContact, all rights reserved.
754              
755             This program is free software; you can redistribute it and/or modify it
756             under the same terms as Perl itself.
757              
758             =cut
759              
760             1; # End of Net::iContact