File Coverage

blib/lib/WWW/Postmark.pm
Criterion Covered Total %
statement 67 78 85.9
branch 40 62 64.5
condition 13 22 59.0
subroutine 11 13 84.6
pod 3 3 100.0
total 134 178 75.2


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