File Coverage

blib/lib/Mail/Milter/Authentication/Handler/Sanitize.pm
Criterion Covered Total %
statement 85 95 89.4
branch 22 32 68.7
condition n/a
subroutine 12 14 85.7
pod 1 10 10.0
total 120 151 79.4


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::Sanitize;
2 25     25   13907 use strict;
  25         69  
  25         785  
3 25     25   138 use warnings;
  25         62  
  25         707  
4 25     25   134 use base 'Mail::Milter::Authentication::Handler';
  25         86  
  25         3251  
5             our $VERSION = '20191206'; # VERSION
6              
7 25     25   174 use Sys::Syslog qw{:standard :macros};
  25         56  
  25         33722  
8              
9             sub default_config {
10             return {
11 0     0 0 0 'hosts_to_remove' => [ 'example.com', 'example.net' ],
12             'remove_headers' => 'yes',
13             };
14             }
15              
16             sub grafana_rows {
17 0     0 0 0 my ( $self ) = @_;
18 0         0 my @rows;
19 0         0 push @rows, $self->get_json( 'Sanitize_metrics' );
20 0         0 return \@rows;
21             }
22              
23             sub register_metrics {
24             return {
25 24     24 1 585 'sanitize_remove_total' => 'The number Authentication Results headers removed',
26             };
27             }
28              
29             sub is_hostname_mine {
30 8     8 0 26 my ( $self, $check_hostname ) = @_;
31 8         31 my $config = $self->handler_config();
32              
33 8 50       31 return 0 if ! defined $check_hostname;
34              
35 8         57 my $hostname = $self->get_my_hostname();
36 8         48 my ($check_for) = $hostname =~ /^[^\.]+\.(.*)/;
37              
38 8 50       34 if ( exists( $config->{'hosts_to_remove'} ) ) {
39 8         23 foreach my $remove_hostname ( @{ $config->{'hosts_to_remove'} } ) {
  8         57  
40 8 50       54 if (
41             substr( lc $check_hostname, ( 0 - length($remove_hostname) ) ) eq
42             lc $remove_hostname )
43             {
44 8         37 return 1;
45             }
46             }
47             }
48              
49 0 0       0 if (
50             substr( lc $check_hostname, ( 0 - length($check_for) ) ) eq
51             lc $check_for )
52             {
53 0         0 return 1;
54             }
55 0         0 return 0;
56             }
57              
58             sub remove_auth_header {
59 8     8 0 23 my ( $self, $index ) = @_;
60 8         81 $self->metric_count( 'sanitize_remove_total', {'header'=>'authentication-results'} );
61 8 100       44 if ( !exists( $self->{'remove_auth_headers'} ) ) {
62 2         17 $self->{'remove_auth_headers'} = [];
63             }
64 8         21 push @{ $self->{'remove_auth_headers'} }, $index;
  8         28  
65 8         18 return;
66             }
67              
68             sub get_headers_to_remove {
69 749     749 0 1781 my ( $self ) = @_;
70 749         2650 my @headers = qw{ X-Disposition-Quarantine };
71 749         3128 return \@headers;
72             }
73              
74             sub envfrom_callback {
75 60     60 0 250 my ( $self, $env_from ) = @_;
76 60         272 delete $self->{'auth_result_header_index'};
77 60         170 delete $self->{'remove_auth_headers'};
78              
79 60         166 my $headers = {};
80 60         178 foreach my $header ( sort @{ $self->get_headers_to_remove() } ) {
  60         278  
81 60         595 $headers->{ lc $header } = {
82             'index' => 0,
83             'silent' => 1,
84             };
85             }
86 60         268 $self->{'header_hash'} = $headers;
87              
88 60         219 return;
89             }
90              
91             sub header_callback {
92 659     659 0 2139 my ( $self, $header, $value ) = @_;
93 659         2403 my $config = $self->handler_config();
94              
95 659 100       1952 return if ( $self->is_trusted_ip_address() );
96 629 50       2296 return if ( lc $config->{'remove_headers'} eq 'no' );
97              
98             # Sanitize Authentication-Results headers
99 629 100       1806 if ( lc $header eq 'authentication-results' ) {
100 8 100       37 if ( !exists $self->{'auth_result_header_index'} ) {
101 2         8 $self->{'auth_result_header_index'} = 0;
102             }
103             $self->{'auth_result_header_index'} =
104 8         26 $self->{'auth_result_header_index'} + 1;
105 8         72 my ($domain_part) = $value =~ /^([^;]*);/;
106 8         32 $domain_part =~ s/ +//g;
107 8 50       33 if ( $self->is_hostname_mine($domain_part) ) {
108 8         46 $self->remove_auth_header( $self->{'auth_result_header_index'} );
109 8 50       43 if ( lc $config->{'remove_headers'} ne 'silent' ) {
110 8         38 my $forged_header =
111             '(Received Authentication-Results header removed by '
112             . $self->get_my_hostname()
113             . ')' . "\n"
114             . ' '
115             . $value;
116 8         129 $self->append_header( 'X-Received-Authentication-Results',
117             $forged_header );
118             }
119             }
120             }
121              
122             # Sanitize other headers
123 629         1211 foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) {
  629         1820  
124 629 100       2368 next if ( lc $remove_header ne lc $header );
125 4         32 $self->{'header_hash'}->{ lc $header }->{'index'} = $self->{'header_hash'}->{ lc $header }->{'index'} + 1;
126 4         31 $self->metric_count( 'sanitize_remove_total', {'header'=> lc $header} );
127              
128 4 50       38 if ( ! $self->{'header_hash'}->{ lc $header }->{'silent'} ) {
129 0         0 my $forged_header =
130             '(Received ' . $remove_header . ' header removed by '
131             . $self->get_my_hostname()
132             . ')' . "\n"
133             . ' '
134             . $value;
135 0         0 $self->append_header( 'X-Received-' . $remove_header,
136             $forged_header );
137             }
138             }
139              
140 629         1863 return;
141             }
142              
143             sub eom_callback {
144 60     60 0 224 my ($self) = @_;
145 60         299 my $config = $self->handler_config();
146 60 50       367 return if ( lc $config->{'remove_headers'} eq 'no' );
147              
148 60 100       259 if ( exists( $self->{'remove_auth_headers'} ) ) {
149 2         7 foreach my $index ( reverse @{ $self->{'remove_auth_headers'} } ) {
  2         11  
150 8         53 $self->dbgout( 'RemoveAuthHeader', $index, LOG_DEBUG );
151 8         77 $self->change_header( 'Authentication-Results', $index, q{} );
152             }
153             }
154              
155 60         174 foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) {
  60         255  
156 60         327 my $max_index = $self->{'header_hash'}->{ lc $remove_header }->{'index'};
157 60 100       244 if ( $max_index ) {
158 2         14 for ( my $index = $max_index; $index > 0; $index-- ) {
159 4         29 $self->dbgout( 'RemoveHeader', "$remove_header $index", LOG_DEBUG );
160 4         17 $self->change_header( $remove_header, $index, q{} );
161             }
162             }
163             }
164              
165 60         232 return;
166             }
167              
168             sub close_callback {
169 96     96 0 293 my ( $self ) = @_;
170 96         291 delete $self->{'remove_auth_headers'};
171 96         230 delete $self->{'auth_result_header_index'};
172 96         379 delete $self->{'header_hash'};
173 96         290 return;
174             }
175              
176             1;
177              
178             __END__
179              
180             =pod
181              
182             =encoding UTF-8
183              
184             =head1 NAME
185              
186             Mail::Milter::Authentication::Handler::Sanitize
187              
188             =head1 VERSION
189              
190             version 20191206
191              
192             =head1 DESCRIPTION
193              
194             Remove unauthorized (forged) Authentication-Results headers from processed email.
195              
196             =head1 CONFIGURATION
197              
198             "Sanitize" : { | Config for the Sanitize Module
199             | Remove conflicting Auth-results headers from inbound mail
200             "hosts_to_remove" : [ | Hostnames (including subdomains thereof) for which we
201             "example.com", | want to remove existing authentication results headers.
202             "example.net"
203             ],
204             "remove_headers" : "yes" | Remove headers with conflicting host names (as defined above)
205             | "no" : do not remove
206             | "yes" : remove and add a header for each one
207             | "silent" : remove silently
208             | Does not run for trusted IP address connections
209             }
210              
211             =head1 AUTHOR
212              
213             Marc Bradshaw <marc@marcbradshaw.net>
214              
215             =head1 COPYRIGHT AND LICENSE
216              
217             This software is copyright (c) 2018 by Marc Bradshaw.
218              
219             This is free software; you can redistribute it and/or modify it under
220             the same terms as the Perl 5 programming language system itself.
221              
222             =cut