File Coverage

blib/lib/Mail/Milter/Authentication/Handler/XGoogleDKIM.pm
Criterion Covered Total %
statement 155 212 73.1
branch 32 60 53.3
condition 4 24 16.6
subroutine 19 22 86.3
pod 1 9 11.1
total 211 327 64.5


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::XGoogleDKIM;
2 7     7   3706 use strict;
  7         18  
  7         186  
3 7     7   37 use warnings;
  7         14  
  7         170  
4 7     7   29 use base 'Mail::Milter::Authentication::Handler';
  7         14  
  7         693  
5             our $VERSION = '20191206'; # VERSION
6              
7 7     7   48 use Data::Dumper;
  7         14  
  7         378  
8 7     7   39 use English qw{ -no_match_vars };
  7         11  
  7         97  
9 7     7   3273 use Sys::Syslog qw{:standard :macros};
  7         18  
  7         1649  
10              
11 7     7   1514 use Mail::DKIM;
  7         481  
  7         174  
12 7     7   1603 use Mail::DKIM::Verifier;
  7         156361  
  7         174  
13 7     7   48 use Mail::DKIM::DNS;
  7         14  
  7         130  
14 7     7   40 use Mail::AuthenticationResults::Header::Entry;
  7         15  
  7         157  
15 7     7   39 use Mail::AuthenticationResults::Header::SubEntry;
  7         17  
  7         115  
16 7     7   35 use Mail::AuthenticationResults::Header::Comment;
  7         13  
  7         12077  
17              
18             sub default_config {
19             return {
20 0     0 0 0 'hide_none' => 0,
21             };
22             }
23              
24             sub grafana_rows {
25 0     0 0 0 my ( $self ) = @_;
26 0         0 my @rows;
27 0         0 push @rows, $self->get_json( 'XGoogleDKIM_metrics' );
28 0         0 return \@rows;
29             }
30              
31             sub register_metrics {
32             return {
33 6     6 1 33 'xgoogledkim_total' => 'The number of emails processed for X-Google-DKIM',
34             };
35             }
36              
37             sub envfrom_callback {
38 50     50 0 206 my ( $self, $env_from ) = @_;
39 50         185 $self->{'failmode'} = 0;
40 50         159 $self->{'headers'} = [];
41 50         166 $self->{'has_dkim'} = 0;
42 50         137 $self->{'carry'} = q{};
43 50         314 $self->destroy_object('xgdkim');
44 50         140 return;
45             }
46              
47             sub header_callback {
48 581     581 0 1825 my ( $self, $header, $value, $original ) = @_;
49 581 50       1810 return if ( $self->{'failmode'} );
50 581         1118 my $EOL = "\015\012";
51 581         2051 my $dkim_chunk = $original . $EOL;
52 581         5196 $dkim_chunk =~ s/\015?\012/$EOL/g;
53              
54 581 100       2169 if ( lc($header) eq 'dkim-signature' ) {
55 32         118 $dkim_chunk = 'X-Orig-' . $dkim_chunk;
56             }
57 581 50       1577 if ( lc($header) eq 'domainkey-signature' ) {
58 0         0 $dkim_chunk = 'X-Orig-' . $dkim_chunk;
59             }
60 581         1143 push @{$self->{'headers'}} , $dkim_chunk;
  581         1788  
61              
62             # Add Google signatures to the mix.
63             # Is this wise?
64 581 100       1648 if ( $header eq 'X-Google-DKIM-Signature' ) {
65 32         150 my $x_dkim_chunk = 'DKIM-Signature: ' . $value . $EOL;
66 32         481 $x_dkim_chunk =~ s/\015?\012/$EOL/g;
67 32         79 push @{$self->{'headers'}} , $x_dkim_chunk;
  32         122  
68 32         99 $self->{'has_dkim'} = 1;
69 32         240 my ($domain) = $value =~ /d=([^;]*);/;
70 32         215 my ($selector) = $value =~ /s=([^;]*);/;
71 32         219 my $resolver = $self->get_object('resolver');
72 32 50 33     263 if ( $selector && $domain ) {
73 32         126 my $lookup = $selector.'._domainkey.'.$domain;
74 32         203 $resolver->bgsend( $lookup, 'TXT' );
75 32         39033 $self->dbgout( 'DNSEarlyLookup', "$lookup TXT", LOG_DEBUG );
76             }
77             }
78              
79 581         1711 return;
80             }
81              
82             sub eoh_callback {
83 50     50 0 186 my ($self) = @_;
84 50 50       218 return if ( $self->{'failmode'} );
85 50         261 my $config = $self->handler_config();
86              
87 50 100       203 if ( $self->{'has_dkim'} == 0 ) {
88 18         152 $self->metric_count( 'xgoogledkim_total', { 'result' => 'none' } );
89 18         129 $self->dbgout( 'XGoogleDKIMResult', 'No X-Google-DKIM headers', LOG_INFO );
90 18 50       86 if ( !( $config->{'hide_none'} ) ) {
91 18         116 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'none' );
92 18         1224 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
93 18         5460 $self->add_auth_header( $header );
94             }
95 18         99 delete $self->{'headers'};
96             }
97             else {
98              
99 32         92 my $dkim;
100 32         78 eval {
101 32         177 $dkim = Mail::DKIM::Verifier->new();
102             # The following requires Mail::DKIM > 0.4
103 32 50       1464 if ( $Mail::DKIM::VERSION >= 0.4 ) {
104 32         140 my $resolver = $self->get_object('resolver');
105 32         164 Mail::DKIM::DNS::resolver($resolver);
106             }
107 32         273 $self->set_object('xgdkim', $dkim, 1);
108             };
109 32 50       164 if ( my $error = $@ ) {
110 0         0 $self->handle_exception( $error );
111 0         0 $self->log_error( 'XGoogleDKIM Setup Error ' . $error );
112 0         0 $self->{'failmode'} = 1;
113 0         0 $self->_check_error( $error );
114 0         0 $self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
115 0         0 delete $self->{'headers'};
116 0         0 return;
117             }
118              
119 32         80 eval {
120             $dkim->PRINT( join q{},
121 32         136 @{ $self->{'headers'} },
  32         452  
122             "\015\012",
123             );
124             };
125 32 50       81278 if ( my $error = $@ ) {
126 0         0 $self->handle_exception( $error );
127 0         0 $self->log_error( 'XGoogleDKIM Headers Error ' . $error );
128 0         0 $self->{'failmode'} = 1;
129 0         0 $self->_check_error( $error );
130 0         0 $self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
131             }
132              
133 32         185 delete $self->{'headers'};
134             }
135              
136 50         211 $self->{'carry'} = q{};
137              
138 50         166 return;
139             }
140              
141             sub body_callback {
142 50     50 0 215 my ( $self, $body_chunk ) = @_;
143 50 50       217 return if ( $self->{'failmode'} );
144 50 100       240 return if ( $self->{'has_dkim'} == 0 );
145 32         104 my $EOL = "\015\012";
146              
147 32         58 my $dkim_chunk;
148 32 50       130 if ( $self->{'carry'} ne q{} ) {
149 0         0 $dkim_chunk = $self->{'carry'} . $body_chunk;
150 0         0 $self->{'carry'} = q{};
151             }
152             else {
153 32         91 $dkim_chunk = $body_chunk;
154             }
155              
156 32 50       154 if ( substr( $dkim_chunk, -1 ) eq "\015" ) {
157 0         0 $self->{'carry'} = "\015";
158 0         0 $dkim_chunk = substr( $dkim_chunk, 0, -1 );
159             }
160              
161 32         656 $dkim_chunk =~ s/\015?\012/$EOL/g;
162              
163 32         152 my $dkim = $self->get_object('xgdkim');
164 32         73 eval {
165 32         149 $dkim->PRINT( $dkim_chunk );
166             };
167 32 50       4332 if ( my $error = $@ ) {
168 0         0 $self->handle_exception( $error );
169 0         0 $self->log_error( 'XGoogleDKIM Body Error ' . $error );
170 0         0 $self->{'failmode'} = 1;
171 0         0 $self->_check_error( $error );
172 0         0 $self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
173             }
174 32         111 return;
175             }
176              
177             sub eom_callback {
178 50     50 0 154 my ($self) = @_;
179              
180 50 100       221 return if ( $self->{'has_dkim'} == 0 );
181 32 50       141 return if ( $self->{'failmode'} );
182              
183 32         152 my $config = $self->handler_config();
184              
185 32         137 my $dkim = $self->get_object('xgdkim');
186              
187 32         96 eval {
188 32         226 $dkim->PRINT( $self->{'carry'} );
189 32         682 $dkim->CLOSE();
190              
191 32         114901 my $dkim_result = $dkim->result;
192 32         310 my $dkim_result_detail = $dkim->result_detail;
193              
194 32         448 $self->metric_count( 'xgoogledkim_total', { 'result' => $dkim_result } );
195              
196 32         202 $self->dbgout( 'XGoogleDKIMResult', $dkim_result_detail, LOG_INFO );
197              
198 32 50       157 if ( !$dkim->signatures() ) {
199 0 0 0     0 if ( !( $config->{'hide_none'} && $dkim_result eq 'none' ) ) {
200 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( $dkim_result );
201 0         0 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
202 0         0 $self->add_auth_header( $header );
203             }
204             }
205 32         373 foreach my $signature ( $dkim->signatures() ) {
206              
207 32         325 my $otype = ref $signature;
208 32 50       163 my $type =
    50          
209             $otype eq 'Mail::DKIM::DkSignature' ? 'domainkeys'
210             : $otype eq 'Mail::DKIM::Signature' ? 'dkim'
211             : 'dkim';
212 32         202 $self->dbgout( 'XGoogleDKIMSignatureType', $type, LOG_DEBUG );
213              
214 32         157 $self->dbgout( 'XGoogleDKIMSignatureIdentity', $signature->identity, LOG_DEBUG );
215 32         169 $self->dbgout( 'XGoogleDKIMSignatureResult', $signature->result_detail, LOG_DEBUG );
216 32         135 my $signature_result = $signature->result();
217 32         322 my $signature_result_detail = $signature->result_detail();
218              
219 32 50       365 if ( $signature_result eq 'invalid' ) {
220 0 0       0 if ( $signature_result_detail =~ /DNS query timeout for (.*) at / ) {
221 0         0 my $timeout_domain = $1;
222 0         0 $self->log_error( "TIMEOUT DETECTED: in XGoogleDKIM result: $timeout_domain" );
223 0         0 $signature_result_detail = "DNS query timeout for $timeout_domain";
224             }
225             }
226              
227 32         79 my $result_comment = q{};
228 32 100 66     225 if ( $signature_result ne 'pass' and $signature_result ne 'none' ) {
229 8         154 $signature_result_detail =~ /$signature_result \((.*)\)/;
230 8 50       45 if ( $1 ) {
231 8         26 $result_comment = $1 . ', ';
232             }
233             }
234 32 50 33     175 if (
235             !(
236             $config->{'hide_none'} && $signature_result eq 'none'
237             )
238             )
239             {
240              
241 32         80 my $key_data = q{};
242 32         71 eval {
243 32         120 my $key = $signature->get_public_key();
244 32         385 $key_data = $key->size() . '-bit ' . $key->type() . ' key';
245             };
246              
247 32         855 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( $signature_result );
248 32         2265 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $result_comment . $key_data ) );
249 32         10888 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.d' )->safe_set_value( $signature->domain() ) );
250 32         3568 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.i' )->safe_set_value( $signature->identity() ) );
251 32         3854 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.b' )->safe_set_value( substr( $signature->data(), 0, 8 ) ) );
252 32         3270 $self->add_auth_header($header);
253             }
254             }
255              
256             };
257 32 50       169 if ( my $error = $@ ) {
258 0         0 $self->handle_exception( $error );
259             # Also in DMARC module
260 0         0 $self->log_error( 'XGoogleDKIM EOM Error ' . $error );
261 0         0 $self->{'failmode'} = 1;
262 0         0 $self->_check_error( $error );
263 0         0 $self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
264 0         0 return;
265             }
266             }
267              
268             sub close_callback {
269 81     81 0 226 my ( $self ) = @_;
270 81         257 delete $self->{'failmode'};
271 81         199 delete $self->{'headers'};
272 81         197 delete $self->{'body'};
273 81         212 delete $self->{'carry'};
274 81         185 delete $self->{'has_dkim'};
275 81         394 $self->destroy_object('xgdkim');
276 81         262 return;
277             }
278              
279             sub _check_error {
280 0     0     my ( $self, $error ) = @_;
281 0 0 0       if ( $error =~ /^DNS error: query timed out/
    0 0        
      0        
      0        
282             or $error =~ /^DNS query timeout/
283             ){
284 0           $self->log_error( 'Temp XGoogleDKIM Error - ' . $error );
285 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'temperror' );
286 0           $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'dns timeout' ) );
287 0           $self->add_auth_header( $header );
288             }
289             elsif ( $error =~ /^no domain to fetch policy for$/
290             or $error =~ /^policy syntax error$/
291             or $error =~ /^empty domain label/
292             or $error =~ /^invalid name /
293             ){
294 0           $self->log_error( 'Perm XGoogleDKIM Error - ' . $error );
295 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'perlerror' );
296 0           $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'syntax or domain error' ) );
297 0           $self->add_auth_header( $header );
298             }
299             else {
300 0           $self->log_error( 'Unexpected XGoogleDKIM Error - ' . $error );
301 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'temperror' );
302 0           $self->add_auth_header( $header );
303             # Fill these in as they occur, but for unknowns err on the side of caution
304             # and tempfail/exit
305 0           $self->exit_on_close();
306 0           $self->tempfail_on_error();
307             }
308 0           return;
309             }
310              
311             1;
312              
313             __END__
314              
315             =pod
316              
317             =encoding UTF-8
318              
319             =head1 NAME
320              
321             Mail::Milter::Authentication::Handler::XGoogleDKIM
322              
323             =head1 VERSION
324              
325             version 20191206
326              
327             =head1 DESCRIPTION
328              
329             Module for validation of X-Google-DKIM signatures.
330              
331             =head1 CONFIGURATION
332              
333             "XGoogleDKIM" : { | Config for the X-Google-DKIM Module
334             "hide_none" : 0, | Hide auth line if the result is 'none'
335             },
336              
337             =head1 AUTHOR
338              
339             Marc Bradshaw <marc@marcbradshaw.net>
340              
341             =head1 COPYRIGHT AND LICENSE
342              
343             This software is copyright (c) 2018 by Marc Bradshaw.
344              
345             This is free software; you can redistribute it and/or modify it under
346             the same terms as the Perl 5 programming language system itself.
347              
348             =cut