File Coverage

blib/lib/Mail/Milter/Authentication/Handler/ReturnOK.pm
Criterion Covered Total %
statement 24 214 11.2
branch 0 62 0.0
condition 0 6 0.0
subroutine 8 18 44.4
pod 1 7 14.2
total 33 307 10.7


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::ReturnOK;
2 1     1   844 use strict;
  1         3  
  1         23  
3 1     1   4 use warnings;
  1         2  
  1         21  
4 1     1   7 use base 'Mail::Milter::Authentication::Handler';
  1         2  
  1         68  
5             our $VERSION = '20191206'; # VERSION
6              
7 1     1   7 use Net::DNS;
  1         1  
  1         80  
8 1     1   6 use Sys::Syslog qw{:standard :macros};
  1         3  
  1         223  
9 1     1   7 use Mail::AuthenticationResults::Header::Entry;
  1         2  
  1         16  
10 1     1   11 use Mail::AuthenticationResults::Header::SubEntry;
  1         2  
  1         25  
11 1     1   5 use Mail::AuthenticationResults::Header::Comment;
  1         1  
  1         1652  
12              
13             sub default_config {
14 0     0 0   return {};
15             }
16              
17             sub grafana_rows {
18 0     0 0   my ( $self ) = @_;
19 0           my @rows;
20 0           push @rows, $self->get_json( 'ReturnOK_metrics' );
21 0           return \@rows;
22             }
23              
24             sub register_metrics {
25             return {
26 0     0 1   'returnok_total' => 'The number of emails processed for ReturnOK',
27             };
28             }
29              
30             sub _check_address {
31 0     0     my ( $self, $address, $type ) = @_;
32              
33 0           my $email = $self->get_address_from( $address );
34              
35 0 0         if ( ! $email ) {
36 0           $self->log_error( "ReturnOK: No Address for $type" );
37             }
38              
39 0           my $domain = lc $self->get_domain_from( $email );
40              
41 0           $self->_check_domain ( $domain, $type );
42              
43 0           return;
44             }
45              
46             sub _check_domain_rr {
47 0     0     my ( $self, $domain, $rrtype ) = @_;
48 0           my $resolver = $self->get_object('resolver');
49 0           my $return = {
50             'result' => 0,
51             'error' => '',
52             'values' => [],
53             };
54 0           eval {
55 0           my $packet = $resolver->query( $domain, $rrtype );
56 0 0         if ($packet) {
57 0           foreach my $rr ( $packet->answer ) {
58 0 0         next unless $rr->type eq $rrtype;
59 0           $return->{ 'result' } = 1;
60 0 0         push @{$return->{'values'}}, $rr->exchange if $rrtype eq 'MX';
  0            
61 0 0         push @{$return->{'values'}}, $rr->address if $rrtype eq 'A';
  0            
62 0 0         push @{$return->{'values'}}, $rr->address if $rrtype eq 'AAAA';
  0            
63             ## TODO Check the returned record is in fact valid for its type
64             }
65             }
66             else {
67 0           my $error = $resolver->errorstring;
68 0 0         if ( $error ) {
69 0           $return->{ 'error' } = $error;
70             }
71             }
72             };
73 0 0         if ( my $error = $@ ) {
74 0           $self->handle_exception( $error );
75 0           $return->{ 'error' } = 'lookup_error';
76 0           $self->log_error( "ReturnOK: Domain lookup fatal error $error for $domain $rrtype" );
77             }
78 0           return $return;
79             }
80              
81             sub _check_domain {
82 0     0     my ( $self, $domain, $type ) = @_;
83              
84 0 0         return if exists $self->{ 'done' }->{ join(':',$domain,$type) };
85 0           $self->{ 'done' }->{ join(':',$domain,$type) } = 1;
86              
87 0           my $metrics = {};
88 0           my @details;
89              
90 0 0         if ( ! $domain ) {
91 0           $self->log_error( "ReturnOK: No Domain for $type" );
92 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'none' );
93 0           $metrics->{ $type . '_result' } = 'none';
94 0           $self->dbgout( 'ReturnOKCheck', 'none', LOG_DEBUG );
95 0           $self->add_auth_header($header);
96 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
97 0           return;
98             }
99              
100 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $type . '.domain' )->safe_set_value( $domain );
101              
102             # Get Org domain and check that if different.
103 0           my $is_org = -1;
104 0           my $org_domain;
105 0 0         if ( $self->is_handler_loaded( 'DMARC' ) ) {
106 0           my $dmarc_handler = $self->get_handler('DMARC');
107 0           my $dmarc_object = $dmarc_handler->get_dmarc_object();
108 0 0         if ( $domain ) {
109 0           $org_domain = eval{ $dmarc_object->get_organizational_domain( $domain ); };
  0            
110 0           $self->handle_exception( $@ );
111 0 0         if ( $org_domain eq $domain ) {
112 0           $is_org = 1;
113 0           $metrics->{ $type . '_is_org_domain' } = 'yes';
114             }
115             else {
116 0           $is_org = 0;
117 0           $metrics->{ $type . '_is_org_domain' } = 'no';
118 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.org_domain' )->safe_set_value( $org_domain );
119             }
120 0 0         push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.is_org' )->safe_set_value( $is_org ? 'yes' : 'no' );
121             }
122             }
123              
124 0           my $lookup_mx = $self->_check_domain_rr( $domain, 'MX' );
125 0 0         if ( $lookup_mx->{ 'error' } ) {
126 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.mx_error' )->safe_set_value( $lookup_mx->{ 'error' } );
127             }
128              
129             # If MX passed then that's it, stop checking
130 0 0         if ( $lookup_mx->{ 'result' } == 1 ) {
131 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'pass' );
132 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'MX Records found: '.join(',',@{$lookup_mx->{'values'}}) );
  0            
133 0           $metrics->{ $type . '_result' } = 'pass';
134 0           $metrics->{ $type . '_has_mx' } = 'yes';
135 0           $self->dbgout( 'ReturnOKCheck', 'pass', LOG_DEBUG );
136 0           foreach my $detail ( @details ) {
137 0           $header->add_child( $detail );
138             }
139 0           $self->add_auth_header($header);
140 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
141 0           return;
142             }
143 0           $metrics->{ $type . '_has_mx' } = 'no';
144              
145 0           my $lookup_a = $self->_check_domain_rr( $domain, 'A' );
146 0 0         if ( $lookup_a->{ 'error' } ) {
147 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.a_error' )->safe_set_value( $lookup_a->{ 'error' } );
148             }
149 0           my $lookup_aaaa = $self->_check_domain_rr( $domain, 'AAAA' );
150 0 0         if ( $lookup_aaaa->{ 'error' } ) {
151 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.aaaa_error' )->safe_set_value( $lookup_aaaa->{ 'error' } );
152             }
153              
154             # If we have an A or AAAA recoed then consider this a warn.
155 0 0 0       if ( $lookup_a->{ 'result' } || $lookup_aaaa->{ 'result' } ) {
156 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'warn' );
157 0           $metrics->{ $type . '_result' } = 'warn';
158 0           $self->dbgout( 'ReturnOKCheck', 'warn', LOG_DEBUG );
159 0 0         if ( $lookup_a->{ 'result' } == 1 ) {
160 0           $metrics->{ $type . '_has_a' } = 'yes';
161 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'A Records found: '.join(',',@{$lookup_a->{'values'}}) );
  0            
162             }
163             else {
164 0           $metrics->{ $type . '_has_a' } = 'no';
165             }
166 0 0         if ( $lookup_aaaa->{ 'result' } == 1 ) {
167 0           $metrics->{ $type . '_has_aaaa' } = 'yes';
168 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'AAAA Records found: '.join(',',@{$lookup_aaaa->{'values'}}) );
  0            
169             }
170             else {
171 0           $metrics->{ $type . '_has_aaaa' } = 'no';
172             }
173 0           foreach my $detail ( @details ) {
174 0           $header->add_child( $detail );
175             }
176 0           $self->add_auth_header($header);
177 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
178 0           return;
179             }
180 0           $metrics->{ $type . '_has_a' } = 'no';
181 0           $metrics->{ $type . '_has_aaaa' } = 'no';
182              
183 0 0         if ( $is_org == 0 ) {
184             # We have DMARC to look this up, have done so, and found that we are NOT the org domain, so recheck at the org domain
185              
186 0           my $lookup_mx = $self->_check_domain_rr( $org_domain, 'MX' );
187 0 0         if ( $lookup_mx->{ 'error' } ) {
188 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.org_mx_error' )->safe_set_value( $lookup_mx->{ 'error' } );
189             }
190              
191             # If MX passed then that's it, stop checking
192 0 0         if ( $lookup_mx->{ 'result' } == 1 ) {
193 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'warn' );
194 0           $self->dbgout( 'ReturnOKCheck', 'warn', LOG_DEBUG );
195 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'Org Domain MX Records found: '.join(',',@{$lookup_mx->{'values'}}) );
  0            
196 0           foreach my $detail ( @details ) {
197 0           $metrics->{ $type . '_result' } = 'warn';
198 0           $metrics->{ $type . '_has_org_mx' } = 'yes';
199 0           $header->add_child( $detail );
200             }
201 0           $self->add_auth_header($header);
202 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
203 0           return;
204             }
205 0           $metrics->{ $type . '_has_org_mx' } = 'no';
206              
207 0           my $lookup_a = $self->_check_domain_rr( $org_domain, 'A' );
208 0 0         if ( $lookup_a->{ 'error' } ) {
209 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.org_a_error' )->safe_set_value( $lookup_a->{ 'error' } );
210             }
211 0           my $lookup_aaaa = $self->_check_domain_rr( $org_domain, 'AAAA' );
212 0 0         if ( $lookup_aaaa->{ 'error' } ) {
213 0           push @details, Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.org_aaaa_error' )->safe_set_value( $lookup_aaaa->{ 'error' } );
214             }
215              
216             # If we have an A or AAAA recoed then consider this a warn.
217 0 0 0       if ( $lookup_a->{ 'result' } || $lookup_aaaa->{ 'result' } ) {
218 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'warn' );
219 0           $metrics->{ $type . '_result' } = 'warn';
220 0           $self->dbgout( 'ReturnOKCheck', 'warn', LOG_DEBUG );
221 0 0         if ( $lookup_a->{ 'result' } == 1 ) {
222 0           $metrics->{ $type . '_has_org_a' } = 'yes';
223 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'Org Domain A Records found: '.join(',',@{$lookup_a->{'values'}}) );
  0            
224             }
225             else {
226 0           $metrics->{ $type . '_has_org_a' } = 'no';
227             }
228 0 0         if ( $lookup_aaaa->{ 'result' } == 1 ) {
229 0           $metrics->{ $type . '_has_org_aaaa' } = 'yes';
230 0           push @details, Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'Org Domain AAAA Records found: '.join(',',@{$lookup_aaaa->{'values'}}) );
  0            
231             }
232             else {
233 0           $metrics->{ $type . '_has_org_aaaa' } = 'no';
234             }
235 0           foreach my $detail ( @details ) {
236 0           $header->add_child( $detail );
237             }
238 0           $self->add_auth_header($header);
239 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
240 0           return;
241             }
242 0           $metrics->{ $type . '_has_org_a' } = 'no';
243 0           $metrics->{ $type . '_has_org_aaaa' } = 'no';
244              
245             }
246              
247             # We got here, we fail!
248 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-return-mx' )->safe_set_value( 'fail' );
249 0           $metrics->{ $type . '_result' } = 'fail';
250 0           $self->dbgout( 'ReturnOKCheck', 'fail', LOG_DEBUG );
251 0           foreach my $detail ( @details ) {
252 0           $header->add_child( $detail );
253             }
254 0           $self->add_auth_header($header);
255              
256 0           push @{ $self->{ 'metrics' } }, $metrics;
  0            
257 0           return;
258             }
259              
260             sub envfrom_callback {
261 0     0 0   my ( $self, $env_from ) = @_;
262              
263 0           $self->{ 'metrics' } = [];
264 0           $self->{ 'done' } = {};
265              
266 0 0         $env_from = q{} if $env_from eq '<>';
267 0           my $addresses = $self->get_addresses_from( $env_from );
268 0           foreach my $address ( @$addresses ) {
269 0           $self->_check_address( $address, 'smtp' );
270             }
271              
272 0           return;
273             }
274              
275             sub header_callback {
276 0     0 0   my ( $self, $header, $value ) = @_;
277              
278 0 0         if ( $header eq 'From' ) {
279 0           my $addresses = $self->get_addresses_from( $value );
280 0           foreach my $address ( @$addresses ) {
281 0           $self->_check_address( $address, 'header' );
282             }
283             }
284              
285 0           return;
286             }
287              
288             sub close_callback {
289 0     0 0   my ( $self ) = @_;
290 0           delete $self->{ 'done' };
291 0           delete $self->{ 'metrics' };
292 0           return;
293             }
294              
295             sub eom_callback {
296 0     0 0   my ( $self ) = @_;
297              
298 0           my $metrics = $self->{ 'metrics' };
299              
300 0           foreach my $metric ( @$metrics ) {
301 0           $self->metric_count( 'returnok_total', $metric );
302             }
303              
304 0           return;
305             }
306              
307             1;
308              
309             __END__
310              
311             =pod
312              
313             =encoding UTF-8
314              
315             =head1 NAME
316              
317             Mail::Milter::Authentication::Handler::ReturnOK
318              
319             =head1 VERSION
320              
321             version 20191206
322              
323             =head1 DESCRIPTION
324              
325             Check that return addresses have valid MX records.
326              
327             =head1 CONFIGURATION
328              
329             No configuration options exist for this handler.
330              
331             =head1 AUTHOR
332              
333             Marc Bradshaw <marc@marcbradshaw.net>
334              
335             =head1 COPYRIGHT AND LICENSE
336              
337             This software is copyright (c) 2018 by Marc Bradshaw.
338              
339             This is free software; you can redistribute it and/or modify it under
340             the same terms as the Perl 5 programming language system itself.
341              
342             =cut