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   2318 use 5.20.0;
  3         12  
3 3     3   25 use strict;
  3         19  
  3         64  
4 3     3   17 use warnings;
  3         9  
  3         95  
5 3     3   23 use Mail::Milter::Authentication::Pragmas;
  3         11  
  3         28  
6             # ABSTRACT: Handler class for Address alignment
7             our $VERSION = '3.20230911'; # VERSION
8 3     3   700 use base 'Mail::Milter::Authentication::Handler';
  3         10  
  3         283  
9 3     3   24 use Net::DNS;
  3         7  
  3         3289  
10              
11             sub default_config {
12 1     1 0 1316 return {};
13             }
14              
15             sub grafana_rows {
16 1     1 0 3275 my ( $self ) = @_;
17 1         2 my @rows;
18 1         7 push @rows, $self->get_json( 'AlignedFrom_metrics' );
19 1         4 return \@rows;
20             }
21              
22             sub register_metrics {
23             return {
24 2     2 1 18 'alignedfrom_total' => 'The number of emails processed for AlignedFrom',
25             };
26             }
27              
28             sub envfrom_callback {
29 44     44 0 95 my ( $self, $env_from ) = @_;
30              
31 44 100       149 $env_from = q{} if $env_from eq '<>';
32              
33             # Defaults
34 44         110 $self->{ 'from_header_count' } = 0;
35 44         78 $self->{ 'envfrom_count' } = 0;
36 44         113 $self->{ 'smtp_address' } = q{};
37 44         108 $self->{ 'smtp_domain' } = q{};
38 44         82 $self->{ 'header_address' } = q{};
39 44         71 $self->{ 'header_domain' } = q{};
40              
41 44         133 my $emails = $self->get_addresses_from( $env_from );
42 44         122 foreach my $email ( @$emails ) {
43 50 100       138 next if ! $email;
44 44         130 $self->{ 'envfrom_count' } = $self->{ 'envfrom_count' } + 1;
45             # More than 1 here? we set to error in eom callback.!
46 44         131 $self->{ 'smtp_address'} = lc $email;
47 44         134 $self->{ 'smtp_domain'} = lc $self->get_domain_from( $email );
48             }
49             }
50              
51             sub header_callback {
52 136     136 0 319 my ( $self, $header, $value ) = @_;
53              
54 136 100       397 return if lc $header ne 'from';
55              
56 44         113 my $emails = $self->get_addresses_from( $value );
57              
58 44         104 my $found_domains = {};
59              
60              
61 44         106 foreach my $email ( @$emails ) {
62 50 100       124 next if ! $email;
63 48         121 $self->{ 'header_address'} = lc $email;
64 48         198 my $domain = lc $self->get_domain_from( $email );
65 48         126 $self->{ 'header_domain'} = $domain;
66 48         184 $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         229 foreach my $domain ( sort keys %$found_domains ) {
72 44         197 $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 6 my ( $self ) = @_;
80 2         5 delete $self->{ 'envfrom_count' };
81 2         6 delete $self->{ 'from_header_count' };
82 2         4 delete $self->{ 'header_address' };
83 2         6 delete $self->{ 'header_domain' };
84 2         5 delete $self->{ 'smtp_address' };
85 2         5 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 89 my ( $self ) = @_;
98              
99 44         87 my $result;
100             my $comment;
101              
102 44 100 100     351 if ( $self->{ 'from_header_count' } > 1 ) {
    100          
    100          
    100          
    100          
    100          
    100          
103 6         12 $result = 'error';
104 6         11 $comment = 'Multiple addresses in header';
105             }
106              
107             elsif ( $self->{ 'envfrom_count' } > 1 ) {
108 6         11 $result = 'error';
109 6         9 $comment = 'Multiple addresses in envelope';
110             }
111              
112             elsif ( ( ! $self->{ 'smtp_domain' } ) && ( ! $self->{ 'header_domain' } ) ) {
113 2         5 $result = 'null';
114 2         5 $comment = 'No domains found';
115             }
116              
117             elsif ( ! $self->{ 'smtp_domain' } ) {
118 4         11 $result = 'null_smtp';
119 4         9 $comment = 'No envelope domain';
120             }
121              
122             elsif ( ! $self->{ 'header_domain' } ) {
123 4         10 $result = 'null_header';
124 4         8 $comment = 'No header domain';
125             }
126              
127             elsif ( $self->{ 'smtp_address' } eq $self->{ 'header_address' } ) {
128 10         20 $result = 'pass';
129 10         16 $comment = 'Address match';
130             }
131              
132             elsif ( $self->{ 'smtp_domain' } eq $self->{ 'header_domain' } ) {
133 4         11 $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       38 if ( $self->is_handler_loaded( 'DMARC' ) ) {
141 4         40 my $dmarc_handler = $self->get_handler('DMARC');
142 4         36 my $dmarc_object = $dmarc_handler->get_dmarc_object();
143 4         10 my $org_smtp_domain = eval{ $dmarc_object->get_organizational_domain( $self->{ 'smtp_domain' } ); };
  4         47  
144 4         536 $self->handle_exception( $@ );
145 4         18 my $org_header_domain = eval{ $dmarc_object->get_organizational_domain( $self->{ 'header_domain' } ); };
  4         19  
146 4         388 $self->handle_exception( $@ );
147              
148 4 100       24 if ( $org_smtp_domain eq $org_header_domain ) {
149 3         9 $result = 'orgdomain_pass';
150 3         9 $comment = 'Domain org match';
151             }
152              
153             else {
154 1         7 $result = 'fail';
155             }
156              
157             }
158              
159             else {
160 4         8 $result = 'fail';
161             }
162              
163             }
164              
165 44         154 $self->dbgout( 'AlignedFrom', $result, LOG_DEBUG );
166 44         255 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-aligned-from' )->safe_set_value( $result );
167 44 100       2488 if ( $comment ) {
168 39         116 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $comment ) );
169             }
170 44         10585 $self->add_auth_header( $header );
171              
172 44         189 $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.20230911
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