File Coverage

lib/Sailthru/Client.pm
Criterion Covered Total %
statement 177 189 93.6
branch 14 24 58.3
condition 2 9 22.2
subroutine 33 34 97.0
pod 13 21 61.9
total 239 277 86.2


line stmt bran cond sub pod time code
1             package Sailthru::Client;
2              
3 5     5   132252 use strict;
  5         5  
  5         120  
4 5     5   14 use warnings;
  5         5  
  5         93  
5              
6 5     5   19 use Carp;
  5         5  
  5         237  
7 5     5   2290 use JSON::XS;
  5         16968  
  5         210  
8 5     5   2696 use LWP::UserAgent;
  5         108788  
  5         134  
9 5     5   25 use Digest::MD5 qw( md5_hex );
  5         5  
  5         264  
10 5     5   2229 use Params::Validate qw( :all );
  5         30953  
  5         702  
11 5     5   452 use Readonly;
  5         2016  
  5         183  
12 5     5   17 use URI;
  5         4  
  5         7300  
13              
14             our $VERSION = '2.3.0';
15             Readonly my $API_URI => 'https://api.sailthru.com/';
16              
17             #
18             # public api
19             #
20              
21             # args:
22             #
23             # * api_key - scalar
24             # * secret - scalar
25             # * timeout - scalar (optional)
26             sub new {
27 5     5 1 1259 my ( $class, $api_key, $secret, $timeout ) = @_;
28 5         48 my $self = {
29             api_key => $api_key,
30             secret => $secret,
31             ua => LWP::UserAgent->new,
32             last_rate_limit_info_ref => {}
33             };
34 5 50       10771 $self->{ua}->timeout($timeout) if $timeout;
35 5         26 $self->{ua}->default_header( 'User-Agent' => "Sailthru API Perl Client $VERSION" );
36 5         207 return bless $self, $class;
37             }
38              
39             # args:
40             #
41             # * template_name - scalar
42             # * email - scalar
43             # * vars - hashref (optional)
44             # * options - hashref (optional)
45             # * schedule_time - scalar (optional)
46             sub send {
47 2     2 1 1726 my $self = shift;
48 2         54 my @params = validate_pos(
49             @_,
50             { type => SCALAR },
51             { type => SCALAR },
52             { type => HASHREF, default => {} },
53             { type => HASHREF, default => {} },
54             { type => SCALAR, default => undef }
55             );
56 2         9 my ( $template_name, $email, $vars, $options, $schedule_time ) = @params;
57 2         1 my $data = {};
58 2         4 $data->{template} = $template_name;
59 2         3 $data->{email} = $email;
60 2 100       1 $data->{vars} = $vars if keys %{$vars};
  2         7  
61 2 100       1 $data->{options} = $options if keys %{$options};
  2         6  
62 2 100       4 $data->{schedule_time} = $schedule_time if $schedule_time;
63 2         4 return $self->api_post( 'send', $data );
64             }
65              
66             # args:
67             # * send_id - scalar
68             sub get_send {
69 2     2 1 1615 my $self = shift;
70 2         39 my @params = validate_pos( @_, { type => SCALAR } );
71 2         5 my ($send_id) = @params;
72 2         6 return $self->api_get( 'send', { send_id => $send_id } );
73             }
74              
75             # args:
76             # * email - scalar
77             sub get_email {
78 3     3 1 953 my $self = shift;
79 3         42 my @params = validate_pos( @_, { type => SCALAR } );
80 3         8 my ($email) = @params;
81 3         9 return $self->api_get( 'email', { email => $email } );
82             }
83              
84             # args:
85             # * email - scalar
86             # * vars - hashref (optional)
87             # * lists - hashref (optional)
88             # * templates - hashref (optional)
89             sub set_email {
90 3     3 1 1828 my $self = shift;
91 3         47 my @params = validate_pos(
92             @_,
93             { type => SCALAR },
94             { type => HASHREF, default => {} },
95             { type => HASHREF, default => {} },
96             { type => HASHREF, default => {} }
97             );
98 3         20 my ( $email, $vars, $lists, $templates ) = @params;
99 3         5 my $data = {};
100 3         4 $data->{email} = $email;
101 3 100       3 $data->{vars} = $vars if keys %{$vars};
  3         10  
102 3 100       3 $data->{lists} = $lists if keys %{$lists};
  3         8  
103 3 100       3 $data->{templates} = $templates if keys %{$templates};
  3         8  
104 3         6 return $self->api_post( 'email', $data );
105             }
106              
107             # args:
108             # * name - scalar
109             # * list - scalar
110             # * schedule_time - scalar
111             # * from_name - scalar
112             # * from_email - scalar
113             # * subject - scalar
114             # * content_html - scalar
115             # * content_text - scalar
116             # * options - hashref (optional)
117             sub schedule_blast {
118 3     3 1 2841 my $self = shift;
119 3         70 my @params = validate_pos(
120             @_,
121             { type => SCALAR },
122             { type => SCALAR },
123             { type => SCALAR },
124             { type => SCALAR },
125             { type => SCALAR },
126             { type => SCALAR },
127             { type => SCALAR },
128             { type => SCALAR },
129             { type => HASHREF, default => {} }
130             );
131 3         13 my ( $name, $list, $schedule_time, $from_name, $from_email, $subject, $content_html, $content_text, $options ) =
132             @params;
133             # initialize our data hash by copying the contents of the options hash
134 3         3 my $data = { %{$options} };
  3         7  
135 3         5 $data->{name} = $name;
136 3         3 $data->{list} = $list;
137 3         3 $data->{schedule_time} = $schedule_time;
138 3         6 $data->{from_name} = $from_name;
139 3         4 $data->{from_email} = $from_email;
140 3         2 $data->{subject} = $subject;
141 3         4 $data->{content_html} = $content_html;
142 3         4 $data->{content_text} = $content_text;
143 3         5 return $self->api_post( 'blast', $data );
144             }
145              
146             # args:
147             # * template_name - scalar
148             # * list - scalar
149             # * schedule_time - scalar
150             # * options - hashref (optional)
151             sub schedule_blast_from_template {
152 2     2 1 2113 my $self = shift;
153 2         29 my @params = validate_pos(
154             @_,
155             { type => SCALAR },
156             { type => SCALAR },
157             { type => SCALAR },
158             { type => HASHREF, default => {} },
159             );
160 2         7 my ( $template_name, $list, $schedule_time, $options ) = @params;
161             # initialize our data hash by copying the contents of the options hash
162 2         1 my $data = { %{$options} };
  2         5  
163 2         3 $data->{copy_template} = $template_name;
164 2         2 $data->{list} = $list;
165 2         2 $data->{schedule_time} = $schedule_time;
166 2         4 return $self->api_post( 'blast', $data );
167              
168             }
169              
170             # args:
171             # * blast_id - scalar
172             sub get_blast {
173 2     2 1 1245 my $self = shift;
174 2         22 my @params = validate_pos( @_, { type => SCALAR } );
175 2         6 my ($blast_id) = @params;
176 2         6 return $self->api_get( 'blast', { blast_id => $blast_id } );
177             }
178              
179             # args:
180             # * template_name - scalar
181             sub get_template {
182 2     2 1 897 my $self = shift;
183 2         20 my @params = validate_pos( @_, { type => SCALAR } );
184 2         6 my ($template_name) = @params;
185 2         5 return $self->api_get( 'template', { template => $template_name } );
186             }
187              
188             # args:
189             # * action - scalar
190             # * data - hashref
191             sub api_get {
192 10     10 1 1008 my $self = shift;
193 10         100 my @params = validate_pos( @_, { type => SCALAR }, { type => HASHREF } );
194 10         24 my ( $action, $data ) = @params;
195 10         34 return $self->_api_request( $action, $data, 'GET' );
196             }
197              
198             # args:
199             # * action - scalar
200             # * data - hashref
201             # * TODO: optional binary_key arg
202             sub api_post {
203 15     15 1 7806 my $self = shift;
204 15         118 my @params = validate_pos( @_, { type => SCALAR }, { type => HASHREF } );
205 15         28 my ( $action, $data ) = @params;
206 15         31 return $self->_api_request( $action, $data, 'POST' );
207             }
208              
209             # args:
210             # * action - scalar
211             # * data - hashref
212             sub api_delete {
213 1     1 1 1762 my $self = shift;
214 1         14 my @params = validate_pos( @_, { type => SCALAR }, { type => HASHREF } );
215 1         3 my ( $action, $data ) = @params;
216 1         3 return $self->_api_request( $action, $data, 'DELETE' );
217             }
218              
219             # args:
220             #
221             # * action - scalar
222             # * method - scalar
223             sub get_last_rate_limit_info {
224 0     0 1 0 my $self = shift;
225 0         0 my @params = validate_pos(
226             @_,
227             { type => SCALAR },
228             { type => SCALAR }
229             );
230 0         0 my ( $action, $method ) = @params;
231 0         0 $method = uc($method);
232              
233 0 0 0     0 if (exists $self->{last_rate_limit_info_ref}->{$action} and $self->{last_rate_limit_info_ref}->{$action}->{$method}) {
234 0         0 return $self->{last_rate_limit_info_ref}->{$action}->{$method};
235             }
236              
237 0         0 return;
238             }
239              
240             #
241             # private helper methods
242             #
243              
244             # args:
245             # * action - scalar
246             # * data - hashref
247             # * request_type - scalar
248             sub _api_request {
249 4     4   5 my $self = shift;
250 4         31 my @params = validate_pos( @_, { type => SCALAR }, { type => HASHREF }, { type => SCALAR } );
251 4         10 my ( $action, $data, $request_type ) = @params;
252 4         7 my $payload = $self->_prepare_json_payload($data);
253 4         13 my $action_uri = $API_URI . $action;
254 4         20 my $response = $self->_http_request( $action_uri, $payload, $request_type );
255              
256             # update rate limit information
257 4 0 33     227 if ( defined $response->header('x-rate-limit-limit') and
      33        
258             defined $response->header('x-rate-limit-remaining') and
259             defined $response->header('x-rate-limit-reset') ) {
260 0         0 $self->{last_rate_limit_info_ref}->{$action}->{$request_type} = {
261             limit => $response->header('x-rate-limit-limit'),
262             remaining => $response->header('x-rate-limit-remaining'),
263             reset => $response->header('x-rate-limit-reset')
264             };
265             }
266              
267 4         207 return decode_json( $response->content );
268             }
269              
270             # args:
271             # * uri - scalar
272             # * data - hashref
273             # * method - scalar
274             sub _http_request {
275 1     1   0 my $self = shift;
276 1         13 my @params = validate_pos( @_, { type => SCALAR }, { type => HASHREF }, { type => SCALAR } );
277 1         2 my ( $uri, $data, $method ) = @params;
278 1         3 $uri = URI->new($uri);
279 1         6197 my $response;
280 1 50       14 if ( $method eq 'GET' ) {
    0          
    0          
281 1         6 $uri->query_form($data);
282 1         214 $response = $self->{ua}->get($uri);
283             }
284             elsif ( $method eq 'POST' ) {
285 0         0 $response = $self->{ua}->post( $uri, $data );
286             }
287             elsif ( $method eq 'DELETE' ) {
288 0         0 $uri->query_form($data);
289 0         0 $response = $self->{ua}->delete($uri);
290             }
291             else {
292 0         0 croak "Invalid method: $method";
293             }
294 1         537903 return $response;
295             }
296              
297             # args:
298             # * data - hashref
299             sub _prepare_json_payload {
300 4     4   4 my $self = shift;
301 4         20 my @params = validate_pos( @_, { type => HASHREF } );
302 4         7 my ($data) = @params;
303 4         3 my $payload = {};
304 4         11 $payload->{api_key} = $self->{api_key};
305 4         5 $payload->{format} = 'json';
306             # this gives us nice clean utf8 encoded json text
307 4         32 $payload->{json} = encode_json($data);
308 4         17 $payload->{sig} = $self->_get_signature_hash( $payload, $self->{secret} );
309 4         21 return $payload;
310             }
311              
312             # Every request must also generate a signature hash called sig according to the
313             # following rules:
314             #
315             # * take the string values of every parameter, including api_key
316             # * sort the values alphabetically, case-sensitively (i.e. ordered by Unicode code point)
317             # * concatenate the sorted values, and prepend this string with your shared secret
318             # * generate an MD5 hash of this string and use this as sig
319             # * now generate your URL-encoded query string from your parameters plus sig
320              
321             # args:
322             # * params - hashref
323             # * secret - scalar
324             # NOTE This internal method assumes a single level hash with values for only 'api_key', 'format', and 'json'
325             # NOTE Since we pack everything into the 'json' value this is safe and we do not need to recurse down a nested hash.
326             sub _get_signature_hash {
327 6     6   827 my $self = shift;
328 6         78 my @params = validate_pos( @_, { type => HASHREF }, { type => SCALAR } );
329 6         16 my ( $api_param_hash, $secret ) = @params;
330 6         5 my @api_param_values = values %{$api_param_hash};
  6         16  
331 6         28 my $sig_string = join '', $secret, sort @api_param_values;
332             # assumes utf8 encoded text, works fine because we use encode_json internally
333 6         33 return md5_hex($sig_string);
334             }
335              
336             ### XXX
337             ### DEPRECATED METHODS
338             ### XXX
339              
340             # args:
341             # * email - scalar
342             sub getEmail {
343 1     1 0 925 my $self = shift;
344 1         13 warnings::warnif( 'deprecated', 'getEmail is deprecated, use get_email instead' );
345 1         114 return $self->get_email(@_);
346             }
347              
348             # args:
349             # * email - scalar
350             # * vars - hashref (optional)
351             # * lists - hashref (optional)
352             # * templates - hashref (optional)
353             sub setEmail {
354 1     1 0 896 my $self = shift;
355 1         11 warnings::warnif( 'deprecated', 'setEmail is deprecated, use set_email instead' );
356 1         116 return $self->set_email(@_);
357             }
358              
359             # args:
360             # * send_id - scalar
361             sub getSend {
362 1     1 0 578 my $self = shift;
363 1         23 warnings::warnif( 'deprecated', 'getSend is deprecated, use get_send instead' );
364 1         143 return $self->get_send(@_);
365             }
366              
367             # args:
368             # * name - scalar
369             # * list - scalar
370             # * schedule_time - scalar
371             # * from_name - scalar
372             # * from_email - scalar
373             # * subject - scalar
374             # * content_html - scalar
375             # * content_text - scalar
376             # * options - hashref (optional)
377             sub scheduleBlast {
378 1     1 0 1868 my $self = shift;
379 1         11 warnings::warnif( 'deprecated', 'scheduleBlast is deprecated, use schedule_blast instead' );
380 1         117 return $self->schedule_blast(@_);
381             }
382              
383             # args:
384             # * blast_id - scalar
385             sub getBlast {
386 1     1 0 1187 my $self = shift;
387 1         12 warnings::warnif( 'deprecated', 'getBlast is deprecated, use get_blast instead' );
388 1         115 return $self->get_blast(@_);
389             }
390              
391             sub copyTemplate {
392 2     2 0 1765 my $self = shift;
393 2         36 my @params = validate_pos(
394             @_,
395             { type => SCALAR },
396             { type => SCALAR },
397             { type => SCALAR },
398             { type => SCALAR },
399             { type => SCALAR },
400             { type => SCALAR },
401             { type => HASHREF, default => {} }
402             );
403 2         8 my ( $template, $data_feed, $setup, $subject_line, $schedule_time, $list, $options ) = @params;
404 2         22 warnings::warnif( 'deprecated', 'copyTemplate is deprecated, use schedule_blast_from_template instead' );
405             # initialize our data hash by copying the contents of the options hash
406 2         223 my $data = { %{$options} };
  2         4  
407 2         3 $data->{copy_template} = $template;
408 2         2 $data->{data_feed_url} = $data_feed;
409 2         3 $data->{setup} = $setup;
410 2         2 $data->{name} = $subject_line;
411 2         2 $data->{schedule_time} = $schedule_time;
412 2         2 $data->{list} = $list;
413 2         3 return $self->api_post( 'blast', $data );
414             }
415              
416             # args:
417             # * template_name - scalar
418             sub getTemplate {
419 1     1 0 879 my $self = shift;
420 1         12 warnings::warnif( 'deprecated', 'getTemplate is deprecated, use get_template instead' );
421 1         136 return $self->get_template(@_);
422             }
423              
424             # args:
425             # * email - scalar
426             # * password - scalar
427             # * include_names - scalar (optional)
428             sub importContacts {
429 2     2 0 2056 my $self = shift;
430 2         26 my @p = validate_pos( @_, { type => SCALAR }, { type => SCALAR }, { type => SCALAR, default => 0 } );
431 2         6 my ( $email, $password, $include_names ) = @p;
432 2         21 warnings::warnif( 'deprecated',
433             'importContacts is deprecated. The contacts API has been discontinued as of August 1st, 2011.' );
434 2         226 my $data = {
435             email => $email,
436             password => $password,
437             include_names => $include_names,
438             };
439 2         5 return $self->api_post( 'contacts', $data );
440             }
441              
442             1;
443              
444             __END__