File Coverage

lib/Haineko/SMTPD/Relay/Mandrill.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Haineko::SMTPD::Relay::Mandrill;
2 1     1   5017 use parent 'Haineko::SMTPD::Relay';
  1         310  
  1         5  
3 1     1   47 use strict;
  1         2  
  1         117  
4 1     1   5 use warnings;
  1         2  
  1         26  
5 1     1   1838 use Furl;
  0            
  0            
6             use Try::Tiny;
7             use Time::Piece;
8             use Haineko::JSON;
9             use Haineko::SMTPD::Response;
10             use Email::MIME;
11             use Encode;
12             use Class::Accessor::Lite;
13              
14             use constant 'MANDRILL_ENDPOINT' => 'mandrillapp.com';
15             use constant 'MANDRILL_APIVERSION' => '1.0';
16              
17             my $rwaccessors = [
18             'queueid', # (String) Queue ID on Mandrill API
19             ];
20             Class::Accessor::Lite->mk_accessors( @$rwaccessors );
21              
22             sub new {
23             my $class = shift;
24             my $argvs = { @_ };
25              
26             $argvs->{'time'} ||= Time::Piece->new;
27             $argvs->{'sleep'} ||= 5;
28             $argvs->{'timeout'} ||= 30;
29             $argvs->{'queueid'} = undef;
30             return bless $argvs, __PACKAGE__;
31             }
32              
33             sub sendmail {
34             my $self = shift;
35              
36             if( not $self->{'password'} ) {
37             # API-KEY(password) is empty
38             my $r = {
39             'code' => 400,
40             'host' => MANDRILL_ENDPOINT,
41             'port' => 443,
42             'rcpt' => $self->{'rcpt'},
43             'error' => 1,
44             'mailer' => 'Mandrill',
45             'message' => [ 'Empty API-KEY' ],
46             'command' => 'POST',
47             };
48             $self->response( Haineko::SMTPD::Response->new( %$r ) );
49             return 0
50             }
51              
52             # * All API calls should be made with HTTP POST.
53             # * You can consider any non-200 HTTP response code an error
54             # - the returned data will contain more detailed information
55             # * All methods are accessed via: https://mandrillapp.com/api/1.0/SOME-METHOD.OUTPUT_FORMAT
56             my $mandrillep = sprintf( "https://%s/api/%s/messages/send-raw.json", MANDRILL_ENDPOINT, MANDRILL_APIVERSION );
57             my $timestamp1 = Time::Piece->new;
58             my $headerlist = [];
59             my $emencoding = uc( $self->{'attr'}->{'charset'} || 'UTF-8' );
60             my $mimeparams = {
61             'body' => Encode::encode( $emencoding, ${ $self->{'body'} } ),
62             'attributes' => $self->{'attr'},
63             };
64             utf8::decode $mimeparams->{'body'} unless utf8::is_utf8 $mimeparams->{'body'} ;
65              
66             for my $e ( @{ $self->{'head'}->{'Received'} } ) {
67             # Convert email headers
68             push @$headerlist, 'Received' => $e;
69             }
70             push @$headerlist, 'To' => $self->{'rcpt'};
71              
72             for my $e ( keys %{ $self->{'head'} } ) {
73             # Make email headers except ``Received'' and ``MIME-Version''
74             next if $e eq 'Received';
75             next if $e eq 'MIME-Version';
76              
77             if( ref $self->{'head'}->{ $e } eq 'ARRAY' ) {
78              
79             for my $f ( @{ $self->{'head'}->{ $e } } ) {
80             push @$headerlist, $e => $f;
81             }
82             }
83             else {
84             push @$headerlist, $e => $self->{'head'}->{ $e };
85             }
86             }
87             $mimeparams->{'header'} = $headerlist;
88              
89             my $mimeobject = Email::MIME->create( %$mimeparams );
90             my $parameters = {
91             'key' => $self->{'password'},
92             'to' => [ $self->{'rcpt'} ],
93             'from_email' => $self->{'mail'},
94             'raw_message' => $mimeobject->as_string,
95             };
96              
97             my $methodargv = {
98             'agent' => $self->{'ehlo'},
99             'timeout' => $self->{'timeout'},
100             'ssl_opts' => { 'SSL_verify_mode' => 0 }
101             };
102             my $httpclient = Furl->new( %$methodargv );
103             my $htrequest1 = Haineko::JSON->dumpjson( $parameters );
104             my $htresponse = undef;
105             my $retryuntil = $self->{'retry'} || 0;
106             my $smtpstatus = 0;
107              
108             my $sendmailto = sub {
109             $htresponse = $httpclient->post( $mandrillep, undef, $htrequest1 );
110              
111             return 0 unless defined $htresponse;
112             return 0 unless $htresponse->is_success;
113              
114             $smtpstatus = 1;
115             return 1;
116             };
117              
118             while(1) {
119             last if $sendmailto->();
120             last if $retryuntil == 0;
121              
122             $retryuntil--;
123             sleep $self->{'sleep'};
124             }
125              
126             if( defined $htresponse ) {
127             # Check the response from Mandrill API
128             my $htcontents = undef;
129             my $nekoparams = {
130             'code' => $htresponse->code,
131             'host' => MANDRILL_ENDPOINT,
132             'port' => 443,
133             'rcpt' => $self->{'rcpt'},
134             'error' => $htresponse->is_success ? 0 : 1,
135             'mailer' => 'Mandrill',
136             'message' => [ $htresponse->message ],
137             'command' => 'POST',
138             };
139              
140             try {
141             # Mandrill respond contents as a JSON
142             $htcontents = Haineko::JSON->loadjson( $htresponse->body );
143             my $v = [];
144              
145             if( ref $htcontents eq 'HASH' ) {
146             # Example Error Response JSON
147             # {
148             # "status": "error",
149             # "code": 12,
150             # "name": "Unknown_Subaccount",
151             # "message": "No subaccount exists with the id 'customer-123'"
152             # }
153             for my $e ( 'status', 'code', 'name', 'message' ) {
154             # Parse error response
155             next unless exists $htcontents->{ $e };
156             next unless defined $htcontents->{ $e };
157             push @$v, sprintf( "%s: s", $e, $htcontents->{ $e } );
158             }
159             $self->{'queueid'} = q();
160              
161             } elsif( ref $htcontents eq 'ARRAY' ){
162             # Example Response JSON
163             # [
164             # {
165             # "email": "recipient.email@example.com",
166             # "status": "sent",
167             # "reject_reason": "hard-bounce",
168             # "_id": "abc123abc123abc123abc123"
169             # }
170             # ]
171             while( my $r = shift @{ $htcontents } ) {
172             # Parse each response
173             for my $e ( '_id', 'status', 'reject_reason' ) {
174             next unless exists $r->{ $e };
175             next unless defined $r->{ $e };
176             push @$v, sprintf( "%s: %s", $e, $r->{ $e } );
177             $self->{'queueid'} ||= $r->{ $e } if $e eq '_id';
178             }
179             }
180             }
181             push @{ $nekoparams->{'message'} }, @$v;
182            
183             } catch {
184             # It was not JSON
185             require Haineko::E;
186             $nekoparams->{'error'} = 1;
187             $nekoparams->{'message'} = [ Haineko::E->new( $htresponse->body )->text ];
188             push @{ $nekoparams->{'message'} }, Haineko::E->new( $_ )->text;
189             };
190             $self->response( Haineko::SMTPD::Response->new( %$nekoparams ) );
191             }
192              
193             return $smtpstatus;
194             }
195              
196             sub getbounce {
197             my $self = shift;
198              
199             return 0 unless $self->{'password'};
200              
201             my $mandrillep = sprintf( "https://%s/api/%s/messages/search.json", MANDRILL_ENDPOINT, MANDRILL_APIVERSION );
202             my $parameters = {
203             'key' => $self->{'password'},
204             'query' => $self->{'rcpt'},
205             'date_from' => $self->{'time'}->ymd('-'),
206             'date_to' => $self->{'time'}->ymd('-'),
207             'senders' => [ $self->{'mail'} ],
208             'api_keys' => [ $self->{'password'} ],
209             'limit' => 2,
210             };
211              
212             my $methodargv = {
213             'agent' => $self->{'ehlo'},
214             'timeout' => $self->{'timeout'},
215             'ssl_opts' => { 'SSL_verify_mode' => 0 }
216             };
217             my $httpclient = Furl->new( %$methodargv );
218             my $htrequest1 = Haineko::JSON->dumpjson( $parameters );
219             my $htresponse = undef;
220             my $retryuntil = $self->{'retry'} || 0;
221             my $httpstatus = 0;
222              
223             my $getbounced = sub {
224             $htresponse = $httpclient->post( $mandrillep, undef, $htrequest1 );
225              
226             return 0 unless defined $htresponse;
227             return 0 unless $htresponse->is_success;
228              
229             $httpstatus = 1;
230             return 1;
231             };
232              
233             while(1) {
234             last if $getbounced->();
235             last if $retryuntil == 0;
236              
237             $retryuntil--;
238             sleep $self->{'sleep'};
239             }
240              
241             if( defined $htresponse ) {
242             # Check the response of getting bounce from Mandrill API
243             my $htcontents = undef;
244             my $nekoparams = undef;
245              
246             eval { $htcontents = Haineko::JSON->loadjson( $htresponse->body ) };
247              
248             while(1) {
249             last if $@;
250             last unless ref $htcontents eq 'ARRAY';
251             last unless scalar @$htcontents;
252              
253             while( my $r = shift @$htcontents ) {
254             next unless ref $r eq 'HASH';
255             next if $r->{'_id'} ne $self->{'queueid'};
256             $nekoparams = {
257             'message' => [ $r->{'diag'}, $r->{'bounce_description'} ],
258             'command' => 'POST',
259             };
260             last;
261             }
262             $self->response( Haineko::SMTPD::Response->p( %$nekoparams ) );
263             last;
264             }
265             }
266             return $httpstatus;
267             }
268              
269             1;
270             __END__