File Coverage

lib/Haineko/SMTPD/Relay/SendGrid.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::SendGrid;
2 1     1   7258 use parent 'Haineko::SMTPD::Relay';
  1         864  
  1         6  
3 1     1   71 use strict;
  1         2  
  1         102  
4 1     1   7 use warnings;
  1         2  
  1         78  
5 1     1   2045 use Furl;
  0            
  0            
6             use Try::Tiny;
7             use Time::Piece;
8             use Haineko::JSON;
9             use Haineko::SMTPD::Response;
10             use Encode;
11              
12             use constant 'SENDGRID_ENDPOINT' => 'sendgrid.com';
13             use constant 'SENDGRID_APIVERSION' => '';
14              
15             sub new {
16             my $class = shift;
17             my $argvs = { @_ };
18              
19             $argvs->{'time'} ||= Time::Piece->new;
20             $argvs->{'sleep'} ||= 5;
21             $argvs->{'timeout'} ||= 30;
22             return bless $argvs, __PACKAGE__;
23             }
24              
25             sub sendmail {
26             my $self = shift;
27              
28             if( ! $self->{'username'} || ! $self->{'password'} ) {
29             # API-USER(username) or API-KEY(password) is empty
30             my $r = {
31             'code' => 400,
32             'host' => SENDGRID_ENDPOINT,
33             'port' => 443,
34             'rcpt' => $self->{'rcpt'},
35             'error' => 1,
36             'mailer' => 'SendGrid',
37             'message' => [ 'Empty API-USER or API-KEY' ],
38             'command' => 'POST',
39             };
40             $self->response( Haineko::SMTPD::Response->new( %$r ) );
41             return 0
42             }
43              
44             my $sendgridep = sprintf( "https://%s/api/mail.send.json", SENDGRID_ENDPOINT );
45             my $parameters = {
46             'to' => $self->{'rcpt'},
47             'from' => $self->{'mail'},
48             'date' => $self->{'head'}->{'Date'},
49             'subject' => $self->{'head'}->{'Subject'},
50             'headers' => q(),
51             'api_key' => $self->{'password'} // q(),
52             'api_user' => $self->{'username'} // q(),
53             'fromname' => $self->{'head'}->{'From'},
54             'x-smtpapi' => q(),
55             };
56              
57             my $usedheader = [ 'Date', 'Subject', 'From' ];
58             my $jsonheader = {};
59             my $identifier = [ split( '@', $self->{'head'}->{'Message-Id'} ) ]->[0];
60              
61             for my $e ( keys %{ $self->{'head'} } ) {
62             # Prepare email headers except headers which begin with ``X-''
63             next unless $e =~ m/\AX-/;
64             $jsonheader->{ $e } = $self->{'head'}->{ $e };
65             }
66             $jsonheader->{'X-Haineko-QueueId'} = $identifier;
67             $jsonheader->{'X-Haineko-Message-Id'} = $self->{'head'}->{'Message-Id'};
68             $parameters->{'headers'} = Haineko::JSON->dumpjson( $jsonheader );
69              
70             $jsonheader = { 'unique_args' => { 'queueid' => $identifier } };
71             $parameters->{'x-smtpapi'} = Haineko::JSON->dumpjson( $jsonheader );
72             $parameters->{'text'} = Encode::encode( 'UTF-8', ${ $self->{'body'} } );
73             $parameters->{'text'} .= qq(\n\n);
74              
75              
76             my $methodargv = {
77             'agent' => $self->{'ehlo'},
78             'timeout' => $self->{'timeout'},
79             'ssl_opts' => { 'SSL_verify_mode' => 0 }
80             };
81             my $httpclient = Furl->new( %$methodargv );
82             my $htresponse = undef;
83             my $retryuntil = $self->{'retry'} || 0;
84             my $smtpstatus = 0;
85              
86             my $sendmailto = sub {
87             $htresponse = $httpclient->post( $sendgridep, undef, $parameters );
88              
89             return 0 unless defined $htresponse;
90             return 0 unless $htresponse->is_success;
91              
92             $smtpstatus = 1;
93             return 1;
94             };
95              
96             while(1) {
97             last if $sendmailto->();
98             last if $retryuntil == 0;
99              
100             $retryuntil--;
101             sleep $self->{'sleep'};
102             }
103              
104             if( defined $htresponse ) {
105             # Check the response from SendGrid API
106             my $htcontents = undef;
107             my $nekoparams = {
108             'code' => $htresponse->code,
109             'host' => SENDGRID_ENDPOINT,
110             'port' => 443,
111             'rcpt' => $self->{'rcpt'},
112             'error' => $htresponse->is_success ? 0 : 1,
113             'mailer' => 'SendGrid',
114             'message' => [ $htresponse->message ],
115             'command' => 'POST',
116             };
117              
118             try {
119             # SendGrid respond contents as a JSON
120             $htcontents = Haineko::JSON->loadjson( $htresponse->body );
121              
122             if( $htcontents->{'message'} eq 'error' ) {
123             push @{ $nekoparams->{'message'} }, @{ $htcontents->{'errors'} };
124             }
125              
126             } catch {
127             # It was not JSON
128             require Haineko::E;
129             $nekoparams->{'error'} = 1;
130             $nekoparams->{'message'} = [ Haineko::E->new( $htresponse->body )->text ];
131             push @{ $nekoparams->{'message'} }, Haineko::E->new( $_ )->text;
132             };
133              
134             $self->response( Haineko::SMTPD::Response->new( %$nekoparams ) );
135             }
136              
137             return $smtpstatus;
138             }
139              
140             sub getbounce {
141             # as of 15 Nov., this method is not called.
142             my $self = shift;
143              
144             return 0 if( ! $self->{'username'} || ! $self->{'password'} );
145              
146             my $sendgridep = sprintf( "https://%s/api/bounces.get.json", SENDGRID_ENDPOINT );
147             my $timepiece1 = gmtime;
148             my $yesterday1 = Time::Piece->new( $timepiece1->epoch - 86400 );
149             my $parameters = {
150             'date' => 1,
151             'days' => 1,
152             'email' => $self->{'rcpt'},
153             'limit' => 1,
154             'api_key' => $self->{'password'} // q(),
155             'api_user' => $self->{'username'} // q(),
156             'start_date' => $yesterday1->ymd('-'),
157             };
158              
159             my $methodargv = {
160             'agent' => $self->{'ehlo'},
161             'timeout' => $self->{'timeout'},
162             'ssl_opts' => { 'SSL_verify_mode' => 0 }
163             };
164             my $httpclient = Furl->new( %$methodargv );
165             my $htresponse = undef;
166             my $retryuntil = $self->{'retry'} || 0;
167             my $httpstatus = 0;
168              
169             my $getbounced = sub {
170             $htresponse = $httpclient->post( $sendgridep, undef, $parameters );
171              
172             return 0 unless defined $htresponse;
173             return 0 unless $htresponse->is_success;
174              
175             $httpstatus = 1;
176             return 1;
177             };
178              
179             while(1) {
180             last if $getbounced->();
181             last if $retryuntil == 0;
182              
183             $retryuntil--;
184             sleep $self->{'sleep'};
185             }
186              
187             if( defined $htresponse ) {
188             # Check the response of getting bounce from SendGrid API
189             my $htcontents = undef;
190             my $nekoparams = undef;
191              
192             try {
193             # SendGrid respond contents as a JSON
194             $htcontents = Haineko::JSON->loadjson( $htresponse->body );
195             $nekoparams = {
196             'message' => [ $htcontents->[0]->{'reason'} ],
197             'command' => 'POST',
198             };
199              
200             } catch {
201             # It was not JSON
202             require Haineko::E;
203             $nekoparams->{'error'} = 1;
204             $nekoparams->{'message'} = [ Haineko::E->new( $htresponse->body )->text ];
205             push @{ $nekoparams->{'message'} }, Haineko::E->new( $_ )->text;
206             };
207             $self->response( Haineko::SMTPD::Response->p( %$nekoparams ) );
208             }
209             return $httpstatus;
210             }
211              
212             1;
213             __END__