File Coverage

blib/lib/WWW/Postmark.pm
Criterion Covered Total %
statement 86 97 88.6
branch 42 64 65.6
condition 18 30 60.0
subroutine 14 16 87.5
pod 3 3 100.0
total 163 210 77.6


line stmt bran cond sub pod time code
1             package WWW::Postmark;
2              
3             # ABSTRACT: API for the Postmark mail service for web applications.
4              
5 3     3   44019 use strict;
  3         6  
  3         90  
6 3     3   11 use warnings;
  3         3  
  3         67  
7              
8 3     3   12 use Carp;
  3         4  
  3         171  
9 3     3   1440 use Email::Valid;
  3         281522  
  3         107  
10 3     3   1923 use HTTP::Tiny;
  3         64484  
  3         155  
11 3     3   1572 use JSON::MaybeXS qw/encode_json decode_json/;
  3         18942  
  3         169  
12 3     3   20 use File::Basename;
  3         4  
  3         156  
13 3     3   1352 use File::MimeInfo;
  3         12016  
  3         152  
14 3     3   1511 use MIME::Base64 qw/encode_base64/;
  3         1602  
  3         2923  
15              
16             our $VERSION = "1.000000";
17             $VERSION = eval $VERSION;
18              
19             my $ua = HTTP::Tiny->new(timeout => 45);
20              
21             =encoding utf-8
22              
23             =head1 NAME
24              
25             WWW::Postmark - API for the Postmark mail service for web applications.
26              
27             =head1 SYNOPSIS
28              
29             use WWW::Postmark;
30              
31             my $api = WWW::Postmark->new('api_token');
32            
33             # or, if you want to use SSL
34             my $api = WWW::Postmark->new('api_token', 1);
35              
36             # send an email
37             $api->send(from => 'me@domain.tld', to => 'you@domain.tld, them@domain.tld',
38             subject => 'an email message', body => "hi guys, what's up?");
39              
40             =head1 DESCRIPTION
41              
42             The WWW::Postmark module provides a simple API for the Postmark web service,
43             that provides email sending facilities for web applications. Postmark is
44             located at L. It is a paid service that charges
45             according the amount of emails you send, and requires signing up in order
46             to receive an API token.
47              
48             You can send emails either through HTTP or HTTPS with SSL encryption. You
49             can send your emails to multiple recipients at once (but there's a 20
50             recipients limit). If WWW::Postmark receives a successful response from
51             the Postmark service, it will return a true value; otherwise it will die.
52              
53             To make it clear, Postmark is not an email marketing service for sending
54             email campaigns or newsletters to multiple subscribers at once. It's meant
55             for sending emails from web applications in response to certain events,
56             like someone signing up to your website.
57              
58             Postmark provides a test API token that doesn't really send the emails.
59             The token is 'POSTMARK_API_TEST', and you can use it for testing purposes
60             (the tests in this distribution use this token).
61              
62             Besides sending emails, this module also provides support for Postmark's
63             spam score API, which allows you to get a SpamAssassin report for an email
64             message. See documentation for the C method for more info.
65              
66             =head1 METHODS
67              
68             =head2 new( [ $api_token, $use_ssl] )
69              
70             Creates a new instance of this class, with a Postmark API token that you've
71             received from the Postmark app. By default, requests are made through HTTP;
72             if you want to send them with SSL encryption, pass a true value for
73             C<$use_ssl>.
74              
75             If you do not provide an API token, you will only be able to use Postmark's
76             spam score API (you will not be able to send emails).
77              
78             Note that in order to use SSL, C requires certain dependencies
79             to be installed. See L for more information.
80              
81             =cut
82              
83             sub new {
84 3     3 1 649 my ($class, $token, $use_ssl) = @_;
85              
86 3 100       517 carp "You have not provided a Postmark API token, you will not be able to send emails."
87             unless $token;
88              
89 3   50     22 $use_ssl ||= 0;
90 3 50       12 $use_ssl = 1 if $use_ssl;
91              
92 3         21 bless { token => $token, use_ssl => $use_ssl }, $class;
93             }
94              
95             =head2 send( %params )
96              
97             Receives a hash representing the email message that should be sent and
98             attempts to send it through the Postmark service. If the message was
99             successfully sent, a hash reference of Postmark's response is returned
100             (refer to L);
101             otherwise, this method will croak with an approriate error message (see
102             L for a full list).
103              
104             The following keys are required when using this method:
105              
106             =over
107              
108             =item * from
109              
110             The email address of the sender. Either pass the email address itself
111             in the format 'mail_address@domain.tld' or also provide a name, like
112             'My Name '.
113              
114             =item * to
115              
116             The email address(es) of the recipient(s). You can use both formats as in
117             'to', but here you can give multiple addresses. Use a comma to separate
118             them. Note, however, that Postmark limits this to 20 recipients and sending
119             will fail if you attempt to send to more than 20 addresses.
120              
121             =item * subject
122              
123             The subject of your message.
124              
125             =item * body
126              
127             The body of your message. This could be plain text, or HTML. If you want
128             to send HTML, be sure to open with '' and close with ''. This
129             module will look for these tags in order to find out whether you're sending
130             a text message or an HTML message.
131              
132             Since version 0.3, however, you can explicitly specify the type of your
133             message, and also send both plain text and HTML. To do so, use the C
134             and/or C attributes. Their presence will override C.
135              
136             =item * html
137              
138             Instead of using C you can also specify the HTML content directly.
139              
140             =item * text
141              
142             ... or the plain text part of the email.
143              
144             =back
145              
146             You can optionally supply the following parameters as well:
147              
148             =over
149              
150             =item * cc, bcc
151              
152             Same rules as the 'to' parameter.
153              
154             =item * tag
155              
156             Can be used to label your mail messages according to different categories,
157             so you can analyze statistics of your mail sendings through the Postmark service.
158              
159             =item * attachments
160              
161             An array-ref with paths of files to attach to the email. C will
162             automatically determine the MIME types of these files and encode their contents
163             to base64 as Postmark requires.
164              
165             =item * reply_to
166              
167             Will force recipients of your email to send their replies to this mail
168             address when replying to your email.
169              
170             =item * track_opens
171              
172             Set to a true value to enable Postmark's open tracking functionality.
173              
174             =back
175              
176             =cut
177              
178             sub send {
179 8     8 1 62890 my ($self, %params) = @_;
180              
181             # do we have an API token?
182 8 100       124 croak "You have not provided a Postmark API token, you cannot send emails"
183             unless $self->{token};
184              
185             # make sure there's a from address
186 7 50 33     84 croak "You must provide a valid 'from' address in the format 'address\@domain.tld', or 'Your Name '."
187             unless $params{from} && Email::Valid->address($params{from});
188              
189             # make sure there's at least on to address
190 7 50       4495 croak $self->_recipient_error('to')
191             unless $params{to};
192              
193             # validate all 'to' addresses
194 7         29 $self->_validate_recipients('to', $params{to});
195              
196             # make sure there's a subject
197 7 50       28 croak "You must provide a mail subject."
198             unless $params{subject};
199              
200             # make sure there's a mail body
201 7 100 100     149 croak "You must provide a mail body."
      66        
202             unless $params{body} or $params{html} or $params{text};
203              
204             # if cc and/or bcc are provided, validate them
205 6 100       19 if ($params{cc}) {
206 1         6 $self->_validate_recipients('cc', $params{cc});
207             }
208 6 50       23 if ($params{bcc}) {
209 0         0 $self->_validate_recipients('bcc', $params{bcc});
210             }
211              
212             # if reply_to is provided, validate it
213 6 50       18 if ($params{reply_to}) {
214 0 0       0 croak "You must provide a valid reply-to address, in the format 'address\@domain.tld', or 'Some Name '."
215             unless Email::Valid->address($params{reply_to});
216             }
217              
218             # parse the body param, unless html or text are present
219 6 100 66     38 unless ($params{html} || $params{text}) {
220 5         14 my $body = delete $params{body};
221 5 100 66     36 if ($body =~ m/^\/i && $body =~ m!\$!i) {
222             # this is an HTML message
223 2         7 $params{html} = $body;
224             } else {
225             # this is a test message
226 3         8 $params{text} = $body;
227             }
228             }
229              
230             # all's well, let's try an send this
231              
232             # create the message data structure
233 6         39 my $msg = {
234             From => $params{from},
235             To => $params{to},
236             Subject => $params{subject},
237             };
238              
239 6 100       36 $msg->{HtmlBody} = $params{html} if $params{html};
240 6 100       23 $msg->{TextBody} = $params{text} if $params{text};
241 6 100       17 $msg->{Cc} = $params{cc} if $params{cc};
242 6 50       18 $msg->{Bcc} = $params{bcc} if $params{bcc};
243 6 50       15 $msg->{Tag} = $params{tag} if $params{tag};
244 6 50       11 $msg->{ReplyTo} = $params{reply_to} if $params{reply_to};
245 6 50       15 $msg->{TrackOpens} = 1 if $params{track_opens};
246              
247 6 100 66     28 if ($params{attachments} && ref $params{attachments} eq 'ARRAY') {
248             # for every file, we need to determine its MIME type and
249             # create a base64 representation of its content
250 1         2 foreach (@{$params{attachments}}) {
  1         5  
251 2         478 my ($buf, $content);
252              
253 2   33     97 open FILE, $_
254             || croak "Failed opening attachment $_: $!";
255              
256 2         40 while (read FILE, $buf, 60*57) {
257 8         189 $content .= encode_base64($buf);
258             }
259              
260 2         39 close FILE;
261              
262 2   100     5 push(@{$msg->{Attachments} ||= []}, {
  2         128  
263             Name => basename($_),
264             ContentType => mimetype($_),
265             Content => $content
266             });
267             }
268             }
269              
270             # create and send the request
271 6 100       472 my $res = $ua->request(
272             'POST',
273             'http' . ($self->{use_ssl} ? 's' : '') . '://api.postmarkapp.com/email',
274             {
275             headers => {
276             'Accept' => 'application/json',
277             'Content-Type' => 'application/json',
278             'X-Postmark-Server-Token' => $self->{token},
279             },
280             content => encode_json($msg),
281             }
282             );
283              
284             # analyze the response
285 6 100       1968925 if ($res->{success}) {
286             # woooooooooooooeeeeeeeeeeee
287 5         169 return decode_json($res->{content});
288             } else {
289 1         7 croak "Failed sending message: ".$self->_analyze_response($res);
290             }
291             }
292              
293             =head2 spam_score( $raw_email, [ $options ] )
294              
295             Use Postmark's SpamAssassin API to determine the spam score of an email
296             message. You need to provide the raw email text to this method, with all
297             headers intact. If C<$options> is 'long' (the default), this method
298             will return a hash-ref with a 'report' key, containing the full
299             SpamAssasin report, and a 'score' key, containing the spam score. If
300             C<$options> is 'short', only the spam score will be returned (directly, not
301             in a hash-ref).
302              
303             If the API returns an error, this method will croak.
304              
305             Note that the SpamAssassin API is currently HTTP only, there is no HTTPS
306             interface, so the C option to the C method is ignored here.
307              
308             For more information about this API, go to L.
309              
310             =cut
311              
312             sub spam_score {
313 2     2 1 1990 my ($self, $raw_email, $options) = @_;
314              
315 2 50       9 croak 'You must provide the raw email text to spam_score().'
316             unless $raw_email;
317              
318 2   100     8 $options ||= 'long';
319              
320 2         61 my $res = $ua->request(
321             'POST',
322             'http://spamcheck.postmarkapp.com/filter',
323             {
324             headers => {
325             'Accept' => 'application/json',
326             'Content-Type' => 'application/json',
327             },
328             content => encode_json({
329             email => $raw_email,
330             options => $options,
331             }),
332             }
333             );
334              
335             # analyze the response
336 2 50       2213085 if ($res->{success}) {
337             # doesn't mean we have succeeded, an error may have been returned
338 2         36 my $ret = decode_json($res->{content});
339 2 50       51 if ($ret->{success}) {
340 2 100       31 return $options eq 'long' ? $ret : $ret->{score};
341             } else {
342 0         0 croak "Postmark spam score API returned error: ".$ret->{message};
343             }
344             } else {
345 0         0 croak "Failed determining spam score: $res->{content}";
346             }
347             }
348              
349             ##################################
350             ## INTERNAL METHODS ##
351             ##################################
352              
353             sub _validate_recipients {
354 8     8   20 my ($self, $field, $param) = @_;
355              
356             # split all addresses
357 8         40 my @ads = split(/, ?/, $param);
358              
359             # make sure there are no more than twenty
360 8 50       26 croak $self->_recipient_error($field)
361             if scalar @ads > 20;
362              
363             # validate them
364 8         23 foreach (@ads) {
365 11 50       1331 croak $self->_recipient_error($field)
366             unless Email::Valid->address($_);
367             }
368              
369             # all's well
370 8         3118 return 1;
371             }
372              
373             sub _recipient_error {
374 0     0   0 my ($self, $field) = @_;
375              
376 0         0 return "You must provide a valid '$field' address or addresses, in the format 'address\@domain.tld', or 'Some Name '. If you're sending to multiple addresses, separate them with commas. You can send up to 20 maximum addresses.";
377             }
378              
379             sub _analyze_response {
380 1     1   4 my ($self, $res) = @_;
381              
382 1 0       253 return $res->{status} == 401 ? 'Missing or incorrect API Key header.' :
    0          
    50          
383             $res->{status} == 422 ? $self->_extract_error($res->{content}) :
384             $res->{status} == 500 ? 'Postmark service error. The service might be down.' :
385             "Unknown HTTP error code $res->{status}.";
386             }
387              
388             sub _extract_error {
389 0     0     my ($self, $content) = @_;
390              
391 0           my $msg = decode_json($content);
392              
393 0           my %errors = (
394             10 => 'Bad or missing API token',
395             300 => 'Invalid email request',
396             400 => 'Sender signature not found',
397             401 => 'Sender signature not confirmed',
398             402 => 'Invalid JSON',
399             403 => 'Incompatible JSON',
400             405 => 'Not allowed to send',
401             406 => 'Inactive recipient',
402             409 => 'JSON required',
403             410 => 'Too many batch messages',
404             411 => 'Forbidden attachment type'
405             );
406              
407 0   0       my $code_msg = $errors{$msg->{ErrorCode}} || "Unknown Postmark error code $msg->{ErrorCode}";
408              
409 0           return $code_msg . ': '. $msg->{Message};
410             }
411              
412             =head1 DIAGNOSTICS
413              
414             The following exceptions are thrown by this module:
415              
416             =over
417              
418             =item C<< "You have not provided a Postmark API token, you cannot send emails" >>
419              
420             This means you haven't provided the C subroutine your Postmark API token.
421             Using the Postmark API requires an API token, received when registering to their
422             service via their website.
423              
424             =item C<< "You must provide a mail subject." >>
425              
426             This error means you haven't given the C method a subject for your email
427             message. Messages sent with this module must have a subject.
428              
429             =item C<< "You must provide a mail body." >>
430              
431             This error means you haven't given the C method a body for your email
432             message. Messages sent with this module must have content.
433              
434             =item C<< "You must provide a valid 'from' address in the format 'address\@domain.tld', or 'Your Name '." >>
435              
436             This error means the address (or one of the addresses) you're trying to send
437             an email to with the C method is not a valid email address (in the sense
438             that it I be an email address, not in the sense that the email address does not
439             exist (For example, "asdf" is not a valid email address).
440              
441             =item C<< "You must provide a valid reply-to address, in the format 'address\@domain.tld', or 'Some Name '." >>
442              
443             This error, when providing the C parameter to the C method,
444             means the C value is not a valid email address.
445              
446             =item C<< "You must provide a valid '%s' address or addresses, in the format 'address\@domain.tld', or 'Some Name '. If you're sending to multiple addresses, separate them with commas. You can send up to 20 maximum addresses." >>
447              
448             Like the above two error messages, but for other email fields such as C and C.
449              
450             =item C<< "Failed sending message: %s" >>
451              
452             This error is thrown when sending an email fails. The error message should
453             include the actual reason for the failure. Usually, the error is returned by
454             the Postmark API. For a list of errors returned by Postmark and their meaning,
455             take a look at L.
456              
457             =item C<< "Unknown Postmark error code %s" >>
458              
459             This means Postmark returned an error code that this module does not
460             recognize. The error message should include the error code. If you find
461             that error code in L,
462             it probably means this is a new error code this module does not know about yet,
463             so please open an appropriate bug report.
464              
465             =item C<< "Unknown HTTP error code %s." >>
466              
467             This means the Postmark API returned an unexpected HTTP status code. The error
468             message should include the status code returned.
469              
470             =item C<< "Failed opening attachment %s: %s" >>
471              
472             This error means C was unable to open a file attachment you have
473             supplied for reading. This could be due to permission problem or the file not
474             existing. The full error message should detail the exact cause.
475              
476             =item C<< "You must provide the raw email text to spam_score()." >>
477              
478             This error means you haven't passed the C method the
479             requried raw email text.
480              
481             =item C<< "Postmark spam score API returned error: %s" >>
482              
483             This error means the spam score API failed parsing your raw email
484             text. The error message should include the actual reason for the failure.
485             This would be an I API error. I API errors will
486             be thrown with the next error message.
487              
488             =item C<< "Failed determining spam score: %s" >>
489              
490             This error means the spam score API returned an HTTP error. The error
491             message should include the actual error message returned.
492              
493             =back
494              
495             =head1 CONFIGURATION AND ENVIRONMENT
496            
497             C requires no configuration files or environment variables.
498              
499             =head1 DEPENDENCIES
500              
501             C B on the following CPAN modules:
502              
503             =over
504              
505             =item * L
506              
507             =item * L
508              
509             =item * L
510              
511             =item * L
512              
513             =item * L
514              
515             =item * L
516              
517             =back
518              
519             C recommends L for parsing JSON (the Postmark API
520             is JSON based). If installed, L will automatically load L
521             or L. For SSL support, L and L will also be
522             needed.
523              
524             =head1 INCOMPATIBILITIES WITH OTHER MODULES
525              
526             None reported.
527              
528             =head1 BUGS AND LIMITATIONS
529              
530             No bugs have been reported.
531              
532             Please report any bugs or feature requests to
533             C, or through the web interface at
534             L.
535              
536             =head1 AUTHOR
537              
538             Ido Perlmuter
539              
540             With help from: Casimir Loeber.
541              
542             =head1 LICENSE AND COPYRIGHT
543              
544             Copyright (c) 2010-2015, Ido Perlmuter C<< ido@ido50.net >>.
545              
546             This module is free software; you can redistribute it and/or
547             modify it under the same terms as Perl itself, either version
548             5.8.1 or any later version. See L
549             and L.
550              
551             The full text of the license can be found in the
552             LICENSE file included with this module.
553              
554             =head1 DISCLAIMER OF WARRANTY
555              
556             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
557             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
558             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
559             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
560             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
561             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
562             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
563             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
564             NECESSARY SERVICING, REPAIR, OR CORRECTION.
565              
566             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
567             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
568             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
569             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
570             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
571             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
572             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
573             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
574             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
575             SUCH DAMAGES.
576              
577             =cut
578              
579             1;
580             __END__