| 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 |  | 41019 | use strict; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 73 |  | 
| 6 | 3 |  |  | 3 |  | 10 | use warnings; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 63 |  | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 3 |  |  | 3 |  | 10 | use Carp; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 164 |  | 
| 9 | 3 |  |  | 3 |  | 1458 | use Email::Valid; | 
|  | 3 |  |  |  |  | 271316 |  | 
|  | 3 |  |  |  |  | 97 |  | 
| 10 | 3 |  |  | 3 |  | 1916 | use HTTP::Tiny; | 
|  | 3 |  |  |  |  | 22236 |  | 
|  | 3 |  |  |  |  | 102 |  | 
| 11 | 3 |  |  | 3 |  | 1334 | use JSON::MaybeXS qw/encode_json decode_json/; | 
|  | 3 |  |  |  |  | 16939 |  | 
|  | 3 |  |  |  |  | 151 |  | 
| 12 | 3 |  |  | 3 |  | 15 | use File::Basename; | 
|  | 3 |  |  |  |  | 4 |  | 
|  | 3 |  |  |  |  | 153 |  | 
| 13 | 3 |  |  | 3 |  | 1252 | use File::MimeInfo; | 
|  | 3 |  |  |  |  | 9902 |  | 
|  | 3 |  |  |  |  | 163 |  | 
| 14 | 3 |  |  | 3 |  | 1266 | use MIME::Base64 qw/encode_base64/; | 
|  | 3 |  |  |  |  | 1399 |  | 
|  | 3 |  |  |  |  | 2767 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | our $VERSION = "1.000001"; | 
| 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 | 197843 | my ($class, $token, $use_ssl) = @_; | 
| 85 |  |  |  |  |  |  |  | 
| 86 | 3 | 100 |  |  |  | 513 | carp "You have not provided a Postmark API token, you will not be able to send emails." | 
| 87 |  |  |  |  |  |  | unless $token; | 
| 88 |  |  |  |  |  |  |  | 
| 89 | 3 |  | 50 |  |  | 25 | $use_ssl ||= 0; | 
| 90 | 3 | 50 |  |  |  | 11 | $use_ssl = 1 if $use_ssl; | 
| 91 |  |  |  |  |  |  |  | 
| 92 | 3 |  |  |  |  | 31 | 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"DIAGNOSTICS"> 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 | 42304 | my ($self, %params) = @_; | 
| 180 |  |  |  |  |  |  |  | 
| 181 |  |  |  |  |  |  | # do we have an API token? | 
| 182 |  |  |  |  |  |  | croak "You have not provided a Postmark API token, you cannot send emails" | 
| 183 | 8 | 100 |  |  |  | 119 | unless $self->{token}; | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | # make sure there's a from address | 
| 186 |  |  |  |  |  |  | croak "You must provide a valid 'from' address in the format 'address\@domain.tld', or 'Your Name '." | 
| 187 | 7 | 50 | 33 |  |  | 115 | unless $params{from} && Email::Valid->address($params{from}); | 
| 188 |  |  |  |  |  |  |  | 
| 189 |  |  |  |  |  |  | # make sure there's at least on to address | 
| 190 |  |  |  |  |  |  | croak $self->_recipient_error('to') | 
| 191 | 7 | 50 |  |  |  | 3258 | unless $params{to}; | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | # validate all 'to' addresses | 
| 194 | 7 |  |  |  |  | 27 | $self->_validate_recipients('to', $params{to}); | 
| 195 |  |  |  |  |  |  |  | 
| 196 |  |  |  |  |  |  | # make sure there's a subject | 
| 197 |  |  |  |  |  |  | croak "You must provide a mail subject." | 
| 198 | 7 | 50 |  |  |  | 18 | unless $params{subject}; | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | # make sure there's a mail body | 
| 201 |  |  |  |  |  |  | croak "You must provide a mail body." | 
| 202 | 7 | 50 | 66 |  |  | 89 | unless $params{body} or $params{html} or $params{text}; | 
|  |  |  | 66 |  |  |  |  | 
| 203 |  |  |  |  |  |  |  | 
| 204 |  |  |  |  |  |  | # if cc and/or bcc are provided, validate them | 
| 205 | 6 | 100 |  |  |  | 14 | if ($params{cc}) { | 
| 206 | 1 |  |  |  |  | 3 | $self->_validate_recipients('cc', $params{cc}); | 
| 207 |  |  |  |  |  |  | } | 
| 208 | 6 | 50 |  |  |  | 13 | 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 |  |  |  | 13 | if ($params{reply_to}) { | 
| 214 |  |  |  |  |  |  | croak "You must provide a valid reply-to address, in the format 'address\@domain.tld', or 'Some Name '." | 
| 215 | 0 | 0 |  |  |  | 0 | unless Email::Valid->address($params{reply_to}); | 
| 216 |  |  |  |  |  |  | } | 
| 217 |  |  |  |  |  |  |  | 
| 218 |  |  |  |  |  |  | # parse the body param, unless html or text are present | 
| 219 | 6 | 50 | 66 |  |  | 26 | unless ($params{html} || $params{text}) { | 
| 220 | 5 |  |  |  |  | 10 | my $body = delete $params{body}; | 
| 221 | 5 | 100 | 66 |  |  | 28 | if ($body =~ m/^\/i && $body =~ m!\$!i) { | 
| 222 |  |  |  |  |  |  | # this is an HTML message | 
| 223 | 2 |  |  |  |  | 4 | $params{html} = $body; | 
| 224 |  |  |  |  |  |  | } else { | 
| 225 |  |  |  |  |  |  | # this is a test message | 
| 226 | 3 |  |  |  |  | 6 | $params{text} = $body; | 
| 227 |  |  |  |  |  |  | } | 
| 228 |  |  |  |  |  |  | } | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | # all's well, let's try an send this | 
| 231 |  |  |  |  |  |  |  | 
| 232 |  |  |  |  |  |  | # create the message data structure | 
| 233 |  |  |  |  |  |  | my $msg = { | 
| 234 |  |  |  |  |  |  | From => $params{from}, | 
| 235 |  |  |  |  |  |  | To => $params{to}, | 
| 236 |  |  |  |  |  |  | Subject => $params{subject}, | 
| 237 | 6 |  |  |  |  | 24 | }; | 
| 238 |  |  |  |  |  |  |  | 
| 239 | 6 | 100 |  |  |  | 18 | $msg->{HtmlBody} = $params{html} if $params{html}; | 
| 240 | 6 | 100 |  |  |  | 18 | $msg->{TextBody} = $params{text} if $params{text}; | 
| 241 | 6 | 100 |  |  |  | 13 | $msg->{Cc} = $params{cc} if $params{cc}; | 
| 242 | 6 | 50 |  |  |  | 14 | $msg->{Bcc} = $params{bcc} if $params{bcc}; | 
| 243 | 6 | 50 |  |  |  | 16 | $msg->{Tag} = $params{tag} if $params{tag}; | 
| 244 | 6 | 50 |  |  |  | 12 | $msg->{ReplyTo} = $params{reply_to} if $params{reply_to}; | 
| 245 | 6 | 50 |  |  |  | 9 | $msg->{TrackOpens} = 1 if $params{track_opens}; | 
| 246 |  |  |  |  |  |  |  | 
| 247 | 6 | 100 | 66 |  |  | 30 | 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 |  |  |  |  | 1 | foreach (@{$params{attachments}}) { | 
|  | 1 |  |  |  |  | 4 |  | 
| 251 | 2 |  |  |  |  | 358 | my ($buf, $content); | 
| 252 |  |  |  |  |  |  |  | 
| 253 | 2 |  | 33 |  |  | 85 | open FILE, $_ | 
| 254 |  |  |  |  |  |  | || croak "Failed opening attachment $_: $!"; | 
| 255 |  |  |  |  |  |  |  | 
| 256 | 2 |  |  |  |  | 27 | while (read FILE, $buf, 60*57) { | 
| 257 | 8 |  |  |  |  | 103 | $content .= encode_base64($buf); | 
| 258 |  |  |  |  |  |  | } | 
| 259 |  |  |  |  |  |  |  | 
| 260 | 2 |  |  |  |  | 46 | close FILE; | 
| 261 |  |  |  |  |  |  |  | 
| 262 | 2 |  | 100 |  |  | 3 | push(@{$msg->{Attachments} ||= []}, { | 
|  | 2 |  |  |  |  | 86 |  | 
| 263 |  |  |  |  |  |  | Name => basename($_), | 
| 264 |  |  |  |  |  |  | ContentType => mimetype($_), | 
| 265 |  |  |  |  |  |  | Content => $content | 
| 266 |  |  |  |  |  |  | }); | 
| 267 |  |  |  |  |  |  | } | 
| 268 |  |  |  |  |  |  | } | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | # create and send the request | 
| 271 |  |  |  |  |  |  | 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 | 6 | 100 |  |  |  | 307 | content => encode_json($msg), | 
| 281 |  |  |  |  |  |  | } | 
| 282 |  |  |  |  |  |  | ); | 
| 283 |  |  |  |  |  |  |  | 
| 284 |  |  |  |  |  |  | # analyze the response | 
| 285 | 6 | 100 |  |  |  | 1890573 | if ($res->{success}) { | 
| 286 |  |  |  |  |  |  | # woooooooooooooeeeeeeeeeeee | 
| 287 | 5 |  |  |  |  | 173 | return decode_json($res->{content}); | 
| 288 |  |  |  |  |  |  | } else { | 
| 289 | 1 | 50 |  |  |  | 10 | if ($msg->{Attachments}) { | 
| 290 | 0 |  |  |  |  | 0 | print STDERR $res->{content}; | 
| 291 |  |  |  |  |  |  | } | 
| 292 | 1 |  |  |  |  | 6 | croak "Failed sending message: ".$self->_analyze_response($res); | 
| 293 |  |  |  |  |  |  | } | 
| 294 |  |  |  |  |  |  | } | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | =head2 spam_score( $raw_email, [ $options ] ) | 
| 297 |  |  |  |  |  |  |  | 
| 298 |  |  |  |  |  |  | Use Postmark's SpamAssassin API to determine the spam score of an email | 
| 299 |  |  |  |  |  |  | message. You need to provide the raw email text to this method, with all | 
| 300 |  |  |  |  |  |  | headers intact. If C<$options> is 'long' (the default), this method | 
| 301 |  |  |  |  |  |  | will return a hash-ref with a 'report' key, containing the full | 
| 302 |  |  |  |  |  |  | SpamAssasin report, and a 'score' key, containing the spam score. If | 
| 303 |  |  |  |  |  |  | C<$options> is 'short', only the spam score will be returned (directly, not | 
| 304 |  |  |  |  |  |  | in a hash-ref). | 
| 305 |  |  |  |  |  |  |  | 
| 306 |  |  |  |  |  |  | If the API returns an error, this method will croak. | 
| 307 |  |  |  |  |  |  |  | 
| 308 |  |  |  |  |  |  | Note that the SpamAssassin API is currently HTTP only, there is no HTTPS | 
| 309 |  |  |  |  |  |  | interface, so the C option to the C method is ignored here. | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  | For more information about this API, go to L. | 
| 312 |  |  |  |  |  |  |  | 
| 313 |  |  |  |  |  |  | =cut | 
| 314 |  |  |  |  |  |  |  | 
| 315 |  |  |  |  |  |  | sub spam_score { | 
| 316 | 2 |  |  | 2 | 1 | 1594 | my ($self, $raw_email, $options) = @_; | 
| 317 |  |  |  |  |  |  |  | 
| 318 | 2 | 50 |  |  |  | 9 | croak 'You must provide the raw email text to spam_score().' | 
| 319 |  |  |  |  |  |  | unless $raw_email; | 
| 320 |  |  |  |  |  |  |  | 
| 321 | 2 |  | 100 |  |  | 7 | $options ||= 'long'; | 
| 322 |  |  |  |  |  |  |  | 
| 323 | 2 |  |  |  |  | 51 | my $res = $ua->request( | 
| 324 |  |  |  |  |  |  | 'POST', | 
| 325 |  |  |  |  |  |  | 'http://spamcheck.postmarkapp.com/filter', | 
| 326 |  |  |  |  |  |  | { | 
| 327 |  |  |  |  |  |  | headers => { | 
| 328 |  |  |  |  |  |  | 'Accept' => 'application/json', | 
| 329 |  |  |  |  |  |  | 'Content-Type' => 'application/json', | 
| 330 |  |  |  |  |  |  | }, | 
| 331 |  |  |  |  |  |  | content => encode_json({ | 
| 332 |  |  |  |  |  |  | email => $raw_email, | 
| 333 |  |  |  |  |  |  | options => $options, | 
| 334 |  |  |  |  |  |  | }), | 
| 335 |  |  |  |  |  |  | } | 
| 336 |  |  |  |  |  |  | ); | 
| 337 |  |  |  |  |  |  |  | 
| 338 |  |  |  |  |  |  | # analyze the response | 
| 339 | 2 | 50 |  |  |  | 65079998 | if ($res->{success}) { | 
| 340 |  |  |  |  |  |  | # doesn't mean we have succeeded, an error may have been returned | 
| 341 | 2 |  |  |  |  | 41 | my $ret = decode_json($res->{content}); | 
| 342 | 2 | 50 |  |  |  | 47 | if ($ret->{success}) { | 
| 343 | 2 | 100 |  |  |  | 40 | return $options eq 'long' ? $ret : $ret->{score}; | 
| 344 |  |  |  |  |  |  | } else { | 
| 345 | 0 |  |  |  |  | 0 | croak "Postmark spam score API returned error: ".$ret->{message}; | 
| 346 |  |  |  |  |  |  | } | 
| 347 |  |  |  |  |  |  | } else { | 
| 348 | 0 |  |  |  |  | 0 | croak "Failed determining spam score: $res->{content}"; | 
| 349 |  |  |  |  |  |  | } | 
| 350 |  |  |  |  |  |  | } | 
| 351 |  |  |  |  |  |  |  | 
| 352 |  |  |  |  |  |  | ################################## | 
| 353 |  |  |  |  |  |  | ##      INTERNAL METHODS        ## | 
| 354 |  |  |  |  |  |  | ################################## | 
| 355 |  |  |  |  |  |  |  | 
| 356 |  |  |  |  |  |  | sub _validate_recipients { | 
| 357 | 8 |  |  | 8 |  | 10 | my ($self, $field, $param) = @_; | 
| 358 |  |  |  |  |  |  |  | 
| 359 |  |  |  |  |  |  | # split all addresses | 
| 360 | 8 |  |  |  |  | 24 | my @ads = split(/, ?/, $param); | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  | # make sure there are no more than twenty | 
| 363 | 8 | 50 |  |  |  | 19 | croak $self->_recipient_error($field) | 
| 364 |  |  |  |  |  |  | if scalar @ads > 20; | 
| 365 |  |  |  |  |  |  |  | 
| 366 |  |  |  |  |  |  | # validate them | 
| 367 | 8 |  |  |  |  | 16 | foreach (@ads) { | 
| 368 | 11 | 50 |  |  |  | 676 | croak $self->_recipient_error($field) | 
| 369 |  |  |  |  |  |  | unless Email::Valid->address($_); | 
| 370 |  |  |  |  |  |  | } | 
| 371 |  |  |  |  |  |  |  | 
| 372 |  |  |  |  |  |  | # all's well | 
| 373 | 8 |  |  |  |  | 1790 | return 1; | 
| 374 |  |  |  |  |  |  | } | 
| 375 |  |  |  |  |  |  |  | 
| 376 |  |  |  |  |  |  | sub _recipient_error { | 
| 377 | 0 |  |  | 0 |  | 0 | my ($self, $field) = @_; | 
| 378 |  |  |  |  |  |  |  | 
| 379 | 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."; | 
| 380 |  |  |  |  |  |  | } | 
| 381 |  |  |  |  |  |  |  | 
| 382 |  |  |  |  |  |  | sub _analyze_response { | 
| 383 | 1 |  |  | 1 |  | 3 | my ($self, $res) = @_; | 
| 384 |  |  |  |  |  |  |  | 
| 385 |  |  |  |  |  |  | return $res->{status} == 401 ? 'Missing or incorrect API Key header.' : | 
| 386 |  |  |  |  |  |  | $res->{status} == 422 ? $self->_extract_error($res->{content}) : | 
| 387 | 1 | 0 |  |  |  | 204 | $res->{status} == 500 ? 'Postmark service error. The service might be down.' : | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 50 |  |  |  |  |  | 
| 388 |  |  |  |  |  |  | "Unknown HTTP error code $res->{status}."; | 
| 389 |  |  |  |  |  |  | } | 
| 390 |  |  |  |  |  |  |  | 
| 391 |  |  |  |  |  |  | sub _extract_error { | 
| 392 | 0 |  |  | 0 |  |  | my ($self, $content) = @_; | 
| 393 |  |  |  |  |  |  |  | 
| 394 | 0 |  |  |  |  |  | my $msg = decode_json($content); | 
| 395 |  |  |  |  |  |  |  | 
| 396 | 0 |  |  |  |  |  | my %errors = ( | 
| 397 |  |  |  |  |  |  | 10	=> 'Bad or missing API token', | 
| 398 |  |  |  |  |  |  | 300	=> 'Invalid email request', | 
| 399 |  |  |  |  |  |  | 400	=> 'Sender signature not found', | 
| 400 |  |  |  |  |  |  | 401	=> 'Sender signature not confirmed', | 
| 401 |  |  |  |  |  |  | 402	=> 'Invalid JSON', | 
| 402 |  |  |  |  |  |  | 403	=> 'Incompatible JSON', | 
| 403 |  |  |  |  |  |  | 405	=> 'Not allowed to send', | 
| 404 |  |  |  |  |  |  | 406	=> 'Inactive recipient', | 
| 405 |  |  |  |  |  |  | 409	=> 'JSON required', | 
| 406 |  |  |  |  |  |  | 410	=> 'Too many batch messages', | 
| 407 |  |  |  |  |  |  | 411	=> 'Forbidden attachment type' | 
| 408 |  |  |  |  |  |  | ); | 
| 409 |  |  |  |  |  |  |  | 
| 410 | 0 |  | 0 |  |  |  | my $code_msg = $errors{$msg->{ErrorCode}} || "Unknown Postmark error code $msg->{ErrorCode}"; | 
| 411 |  |  |  |  |  |  |  | 
| 412 | 0 |  |  |  |  |  | return $code_msg . ': '. $msg->{Message}; | 
| 413 |  |  |  |  |  |  | } | 
| 414 |  |  |  |  |  |  |  | 
| 415 |  |  |  |  |  |  | =head1 DIAGNOSTICS | 
| 416 |  |  |  |  |  |  |  | 
| 417 |  |  |  |  |  |  | The following exceptions are thrown by this module: | 
| 418 |  |  |  |  |  |  |  | 
| 419 |  |  |  |  |  |  | =over | 
| 420 |  |  |  |  |  |  |  | 
| 421 |  |  |  |  |  |  | =item C<< "You have not provided a Postmark API token, you cannot send emails" >> | 
| 422 |  |  |  |  |  |  |  | 
| 423 |  |  |  |  |  |  | This means you haven't provided the C subroutine your Postmark API token. | 
| 424 |  |  |  |  |  |  | Using the Postmark API requires an API token, received when registering to their | 
| 425 |  |  |  |  |  |  | service via their website. | 
| 426 |  |  |  |  |  |  |  | 
| 427 |  |  |  |  |  |  | =item C<< "You must provide a mail subject." >> | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | This error means you haven't given the C method a subject for your email | 
| 430 |  |  |  |  |  |  | message. Messages sent with this module must have a subject. | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | =item C<< "You must provide a mail body." >> | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  | This error means you haven't given the C method a body for your email | 
| 435 |  |  |  |  |  |  | message. Messages sent with this module must have content. | 
| 436 |  |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  | =item C<< "You must provide a valid 'from' address in the format 'address\@domain.tld', or 'Your Name '." >> | 
| 438 |  |  |  |  |  |  |  | 
| 439 |  |  |  |  |  |  | This error means the address (or one of the addresses) you're trying to send | 
| 440 |  |  |  |  |  |  | an email to with the C method is not a valid email address (in the sense | 
| 441 |  |  |  |  |  |  | that it I be an email address, not in the sense that the email address does not | 
| 442 |  |  |  |  |  |  | exist (For example, "asdf" is not a valid email address). | 
| 443 |  |  |  |  |  |  |  | 
| 444 |  |  |  |  |  |  | =item C<< "You must provide a valid reply-to address, in the format 'address\@domain.tld', or 'Some Name '." >> | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | This error, when providing the C parameter to the C method, | 
| 447 |  |  |  |  |  |  | means the C value is not a valid email address. | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  | =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." >> | 
| 450 |  |  |  |  |  |  |  | 
| 451 |  |  |  |  |  |  | Like the above two error messages, but for other email fields such as C and C. | 
| 452 |  |  |  |  |  |  |  | 
| 453 |  |  |  |  |  |  | =item C<< "Failed sending message: %s" >> | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | This error is thrown when sending an email fails. The error message should | 
| 456 |  |  |  |  |  |  | include the actual reason for the failure. Usually, the error is returned by | 
| 457 |  |  |  |  |  |  | the Postmark API. For a list of errors returned by Postmark and their meaning, | 
| 458 |  |  |  |  |  |  | take a look at L. | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | =item C<< "Unknown Postmark error code %s" >> | 
| 461 |  |  |  |  |  |  |  | 
| 462 |  |  |  |  |  |  | This means Postmark returned an error code that this module does not | 
| 463 |  |  |  |  |  |  | recognize. The error message should include the error code. If you find | 
| 464 |  |  |  |  |  |  | that error code in L, | 
| 465 |  |  |  |  |  |  | it probably means this is a new error code this module does not know about yet, | 
| 466 |  |  |  |  |  |  | so please open an appropriate bug report. | 
| 467 |  |  |  |  |  |  |  | 
| 468 |  |  |  |  |  |  | =item C<< "Unknown HTTP error code %s." >> | 
| 469 |  |  |  |  |  |  |  | 
| 470 |  |  |  |  |  |  | This means the Postmark API returned an unexpected HTTP status code. The error | 
| 471 |  |  |  |  |  |  | message should include the status code returned. | 
| 472 |  |  |  |  |  |  |  | 
| 473 |  |  |  |  |  |  | =item C<< "Failed opening attachment %s: %s" >> | 
| 474 |  |  |  |  |  |  |  | 
| 475 |  |  |  |  |  |  | This error means C was unable to open a file attachment you have | 
| 476 |  |  |  |  |  |  | supplied for reading. This could be due to permission problem or the file not | 
| 477 |  |  |  |  |  |  | existing. The full error message should detail the exact cause. | 
| 478 |  |  |  |  |  |  |  | 
| 479 |  |  |  |  |  |  | =item C<< "You must provide the raw email text to spam_score()." >> | 
| 480 |  |  |  |  |  |  |  | 
| 481 |  |  |  |  |  |  | This error means you haven't passed the C method the | 
| 482 |  |  |  |  |  |  | requried raw email text. | 
| 483 |  |  |  |  |  |  |  | 
| 484 |  |  |  |  |  |  | =item C<< "Postmark spam score API returned error: %s" >> | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | This error means the spam score API failed parsing your raw email | 
| 487 |  |  |  |  |  |  | text. The error message should include the actual reason for the failure. | 
| 488 |  |  |  |  |  |  | This would be an I API error. I API errors will | 
| 489 |  |  |  |  |  |  | be thrown with the next error message. | 
| 490 |  |  |  |  |  |  |  | 
| 491 |  |  |  |  |  |  | =item C<< "Failed determining spam score: %s" >> | 
| 492 |  |  |  |  |  |  |  | 
| 493 |  |  |  |  |  |  | This error means the spam score API returned an HTTP error. The error | 
| 494 |  |  |  |  |  |  | message should include the actual error message returned. | 
| 495 |  |  |  |  |  |  |  | 
| 496 |  |  |  |  |  |  | =back | 
| 497 |  |  |  |  |  |  |  | 
| 498 |  |  |  |  |  |  | =head1 CONFIGURATION AND ENVIRONMENT | 
| 499 |  |  |  |  |  |  |  | 
| 500 |  |  |  |  |  |  | C requires no configuration files or environment variables. | 
| 501 |  |  |  |  |  |  |  | 
| 502 |  |  |  |  |  |  | =head1 DEPENDENCIES | 
| 503 |  |  |  |  |  |  |  | 
| 504 |  |  |  |  |  |  | C B on the following CPAN modules: | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | =over | 
| 507 |  |  |  |  |  |  |  | 
| 508 |  |  |  |  |  |  | =item * L | 
| 509 |  |  |  |  |  |  |  | 
| 510 |  |  |  |  |  |  | =item * L | 
| 511 |  |  |  |  |  |  |  | 
| 512 |  |  |  |  |  |  | =item * L | 
| 513 |  |  |  |  |  |  |  | 
| 514 |  |  |  |  |  |  | =item * L | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  | =item * L | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | =item * L | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | =back | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | C recommends L for parsing JSON (the Postmark API | 
| 523 |  |  |  |  |  |  | is JSON based). If installed, L will automatically load L | 
| 524 |  |  |  |  |  |  | or L. For SSL support, L and L will also be | 
| 525 |  |  |  |  |  |  | needed. | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | =head1 INCOMPATIBILITIES WITH OTHER MODULES | 
| 528 |  |  |  |  |  |  |  | 
| 529 |  |  |  |  |  |  | None reported. | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | =head1 BUGS AND LIMITATIONS | 
| 532 |  |  |  |  |  |  |  | 
| 533 |  |  |  |  |  |  | No bugs have been reported. | 
| 534 |  |  |  |  |  |  |  | 
| 535 |  |  |  |  |  |  | Please report any bugs or feature requests to | 
| 536 |  |  |  |  |  |  | C, or through the web interface at | 
| 537 |  |  |  |  |  |  | L. | 
| 538 |  |  |  |  |  |  |  | 
| 539 |  |  |  |  |  |  | =head1 AUTHOR | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | Ido Perlmuter | 
| 542 |  |  |  |  |  |  |  | 
| 543 |  |  |  |  |  |  | With help from: Casimir Loeber. | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | =head1 LICENSE AND COPYRIGHT | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | Copyright 2017 Ido Perlmuter | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | Licensed under the Apache License, Version 2.0 (the "License"); | 
| 550 |  |  |  |  |  |  | you may not use this file except in compliance with the License. | 
| 551 |  |  |  |  |  |  | You may obtain a copy of the License at | 
| 552 |  |  |  |  |  |  |  | 
| 553 |  |  |  |  |  |  | http://www.apache.org/licenses/LICENSE-2.0 | 
| 554 |  |  |  |  |  |  |  | 
| 555 |  |  |  |  |  |  | Unless required by applicable law or agreed to in writing, software | 
| 556 |  |  |  |  |  |  | distributed under the License is distributed on an "AS IS" BASIS, | 
| 557 |  |  |  |  |  |  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
| 558 |  |  |  |  |  |  | See the License for the specific language governing permissions and | 
| 559 |  |  |  |  |  |  | limitations under the License. | 
| 560 |  |  |  |  |  |  |  | 
| 561 |  |  |  |  |  |  | =cut | 
| 562 |  |  |  |  |  |  |  | 
| 563 |  |  |  |  |  |  | 1; | 
| 564 |  |  |  |  |  |  | __END__ |