File Coverage

blib/lib/Mail/Milter/Authentication/Handler/AlignedFrom.pm
Criterion Covered Total %
statement 100 100 100.0
branch 28 28 100.0
condition 3 3 100.0
subroutine 14 14 100.0
pod 1 7 14.2
total 146 152 96.0


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::AlignedFrom;
2 3     3   2441 use strict;
  3         11  
  3         96  
3 3     3   21 use warnings;
  3         13  
  3         96  
4 3     3   17 use base 'Mail::Milter::Authentication::Handler';
  3         4  
  3         356  
5             our $VERSION = '20191206'; # VERSION
6              
7 3     3   1526 use Net::DNS;
  3         6417  
  3         263  
8 3     3   27 use Sys::Syslog qw{:standard :macros};
  3         6  
  3         884  
9 3     3   23 use Mail::AuthenticationResults::Header::Entry;
  3         6  
  3         76  
10 3     3   22 use Mail::AuthenticationResults::Header::Comment;
  3         11  
  3         2090  
11              
12             sub default_config {
13 1     1 0 1709 return {};
14             }
15              
16             sub grafana_rows {
17 1     1 0 3830 my ( $self ) = @_;
18 1         2 my @rows;
19 1         11 push @rows, $self->get_json( 'AlignedFrom_metrics' );
20 1         6 return \@rows;
21             }
22              
23             sub register_metrics {
24             return {
25 2     2 1 14 'alignedfrom_total' => 'The number of emails processed for AlignedFrom',
26             };
27             }
28              
29             sub envfrom_callback {
30 44     44 0 121 my ( $self, $env_from ) = @_;
31              
32 44 100       147 $env_from = q{} if $env_from eq '<>';
33              
34             # Defaults
35 44         123 $self->{ 'from_header_count' } = 0;
36 44         106 $self->{ 'envfrom_count' } = 0;
37 44         145 $self->{ 'smtp_address' } = q{};
38 44         100 $self->{ 'smtp_domain' } = q{};
39 44         109 $self->{ 'header_address' } = q{};
40 44         107 $self->{ 'header_domain' } = q{};
41              
42 44         234 my $emails = $self->get_addresses_from( $env_from );
43 44         140 foreach my $email ( @$emails ) {
44 50 100       143 next if ! $email;
45 44         140 $self->{ 'envfrom_count' } = $self->{ 'envfrom_count' } + 1;
46             # More than 1 here? we set to error in eom callback.!
47 44         149 $self->{ 'smtp_address'} = lc $email;
48 44         192 $self->{ 'smtp_domain'} = lc $self->get_domain_from( $email );
49             }
50              
51 44         158 return;
52             }
53              
54             sub header_callback {
55 136     136 0 364 my ( $self, $header, $value ) = @_;
56              
57 136 100       478 return if lc $header ne 'from';
58              
59 44         145 my $emails = $self->get_addresses_from( $value );
60              
61 44         101 my $found_domains = {};
62              
63              
64 44         118 foreach my $email ( @$emails ) {
65 50 100       137 next if ! $email;
66 48         162 $self->{ 'header_address'} = lc $email;
67 48         160 my $domain = lc $self->get_domain_from( $email );
68 48         123 $self->{ 'header_domain'} = $domain;
69 48         194 $found_domains->{ $domain } = $1;
70             }
71              
72             # We don't consider finding 2 addresses at the same domain in a header to be 2 separate entries
73             # for alignment checking, only count them as one.
74 44         215 foreach my $domain ( sort keys %$found_domains ) {
75 44         163 $self->{ 'from_header_count' } = $self->{ 'from_header_count' } + 1;
76             # If there are more than 1 then the result will be set to error in the eom callback
77             # Multiple from headers should always set the result to error.
78             }
79              
80 44         180 return;
81             }
82              
83             sub close_callback {
84 2     2 0 6 my ( $self ) = @_;
85 2         6 delete $self->{ 'from_header_count' };
86 2         6 delete $self->{ 'header_address' };
87 2         5 delete $self->{ 'header_domain' };
88 2         5 delete $self->{ 'smtp_address' };
89 2         5 delete $self->{ 'smtp_domain' };
90 2         5 return;
91             }
92              
93             # error = multiple from headers present
94             # null = no addresses present
95             # null_smtp = no smtp address present
96             # null_header = no header address present
97             # pass = addresses match
98             # domain_pass = domains match
99             # orgdomain_pass = domains in same orgdomain
100              
101             sub eom_callback {
102 44     44 0 130 my ( $self ) = @_;
103              
104 44         141 my $result;
105             my $comment;
106              
107 44 100 100     444 if ( $self->{ 'from_header_count' } > 1 ) {
    100          
    100          
    100          
    100          
    100          
    100          
108 6         14 $result = 'error';
109 6         16 $comment = 'Multiple addresses in header';
110             }
111              
112             elsif ( $self->{ 'envfrom_count' } > 1 ) {
113 6         18 $result = 'error';
114 6         15 $comment = 'Multiple addresses in envelope';
115             }
116              
117             elsif ( ( ! $self->{ 'smtp_domain' } ) && ( ! $self->{ 'header_domain' } ) ) {
118 2         7 $result = 'null';
119 2         6 $comment = 'No domains found';
120             }
121              
122             elsif ( ! $self->{ 'smtp_domain' } ) {
123 4         13 $result = 'null_smtp';
124 4         13 $comment = 'No envelope domain';
125             }
126              
127             elsif ( ! $self->{ 'header_domain' } ) {
128 4         10 $result = 'null_header';
129 4         10 $comment = 'No header domain';
130             }
131              
132             elsif ( $self->{ 'smtp_address' } eq $self->{ 'header_address' } ) {
133 10         30 $result = 'pass';
134 10         24 $comment = 'Address match';
135             }
136              
137             elsif ( $self->{ 'smtp_domain' } eq $self->{ 'header_domain' } ) {
138 4         13 $result = 'domain_pass';
139 4         9 $comment = 'Domain match';
140             }
141              
142             else {
143              
144             # Get Org domain and check that if different.
145 8 100       58 if ( $self->is_handler_loaded( 'DMARC' ) ) {
146 4         21 my $dmarc_handler = $self->get_handler('DMARC');
147 4         30 my $dmarc_object = $dmarc_handler->get_dmarc_object();
148 4         13 my $org_smtp_domain = eval{ $dmarc_object->get_organizational_domain( $self->{ 'smtp_domain' } ); };
  4         384  
149 4         528 $self->handle_exception( $@ );
150 4         10 my $org_header_domain = eval{ $dmarc_object->get_organizational_domain( $self->{ 'header_domain' } ); };
  4         16  
151 4         258 $self->handle_exception( $@ );
152              
153 4 100       13 if ( $org_smtp_domain eq $org_header_domain ) {
154 3         8 $result = 'orgdomain_pass';
155 3         7 $comment = 'Domain org match';
156             }
157              
158             else {
159 1         4 $result = 'fail';
160             }
161              
162             }
163              
164             else {
165 4         8 $result = 'fail';
166             }
167              
168             }
169              
170 44         218 $self->dbgout( 'AlignedFrom', $result, LOG_DEBUG );
171 44         422 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-aligned-from' )->safe_set_value( $result );
172 44 100       3583 if ( $comment ) {
173 39         152 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $comment ) );
174             }
175 44         12597 $self->add_auth_header( $header );
176              
177 44         257 $self->metric_count( 'alignedfrom_total', { 'result' => $result } );
178              
179 44         116 return;
180             }
181              
182             1;
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Mail::Milter::Authentication::Handler::AlignedFrom
193              
194             =head1 VERSION
195              
196             version 20191206
197              
198             =head1 DESCRIPTION
199              
200             Check that Mail From and Header From addresses are in alignment.
201              
202             =head1 CONFIGURATION
203              
204             No configuration options exist for this handler.
205              
206             =head1 AUTHOR
207              
208             Marc Bradshaw <marc@marcbradshaw.net>
209              
210             =head1 COPYRIGHT AND LICENSE
211              
212             This software is copyright (c) 2018 by Marc Bradshaw.
213              
214             This is free software; you can redistribute it and/or modify it under
215             the same terms as the Perl 5 programming language system itself.
216              
217             =cut