File Coverage

blib/lib/Mail/Milter/Authentication/Handler/AlignedFrom.pm
Criterion Covered Total %
statement 93 93 100.0
branch 28 28 100.0
condition 3 3 100.0
subroutine 13 13 100.0
pod 1 7 14.2
total 138 144 95.8


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