File Coverage

blib/lib/Mail/Milter/Authentication/Handler/SPF.pm
Criterion Covered Total %
statement 91 129 70.5
branch 23 40 57.5
condition 6 6 100.0
subroutine 13 15 86.6
pod 1 8 12.5
total 134 198 67.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::SPF;
2 37     37   9452 use 5.20.0;
  37         231  
3 37     37   294 use strict;
  37         127  
  37         944  
4 37     37   354 use warnings;
  37         131  
  37         1108  
5 37     37   287 use Mail::Milter::Authentication::Pragmas;
  37         151  
  37         353  
6             # ABSTRACT: Handler class for SPF
7             our $VERSION = '3.20230629'; # VERSION
8 37     37   9419 use base 'Mail::Milter::Authentication::Handler';
  37         130  
  37         4381  
9 37     37   414 use Mail::SPF;
  37         128  
  37         64048  
10              
11             sub default_config {
12             return {
13 0     0 0 0 'hide_received-spf_header' => 0,
14             'hide_none' => 0,
15             'best_guess' => 0,
16             };
17             }
18              
19             sub grafana_rows {
20 0     0 0 0 my ( $self ) = @_;
21 0         0 my @rows;
22 0         0 push @rows, $self->get_json( 'SPF_metrics' );
23 0         0 return \@rows;
24             }
25              
26             sub setup_callback {
27 142     142 0 680 my ( $self ) = @_;
28              
29             $self->set_object_maker( 'spf_server' , sub {
30 56     56   198 my ( $self, $name ) = @_;
31 56         183 my $thischild = $self->{'thischild'};
32 56         433 $self->dbgout( 'Object created', $name, LOG_DEBUG );
33 56         246 my $object;
34 56         301 eval {
35 56         631 my $resolver = $self->get_object('resolver');
36 56         675 $object = Mail::SPF::Server->new(
37             'hostname' => $self->get_my_hostname(),
38             'dns_resolver' => $resolver,
39             );
40             };
41 56 50       29107 if ( my $error = $@ ) {
42 0         0 $self->handle_exception( $error );
43 0         0 $self->log_error( 'SPF Object Setup Error ' . $error );
44             }
45 56         427 $thischild->{'object'}->{$name} = {
46             'object' => $object,
47             'destroy' => 0,
48             };
49 142         3291 });
50             }
51              
52             sub register_metrics {
53             return {
54 37     37 1 296 'spf_total' => 'The number of emails processed for SPF',
55             };
56             }
57              
58             sub wrap_header {
59 100     100 0 407 my ( $self, $value ) = @_;
60 100         803 $value =~ s/ /\n /;
61 100         793 $value =~ s/\) /\)\n /;
62 100         810 $value =~ s/; /;\n /g;
63 100         383 return $value;
64             }
65              
66             sub helo_callback {
67 114     114 0 644 my ( $self, $helo_host ) = @_;
68 114         615 $self->{'failmode'} = 0;
69 114         544 $self->{'helo_name'} = $helo_host;
70             }
71              
72             sub envfrom_callback {
73              
74             # On MAILFROM
75             #...
76 114     114 0 737 my ( $self, $env_from ) = @_;
77 114         750 my $config = $self->handler_config();
78 114 100       995 return if ( $self->is_local_ip_address() );
79 106 100       766 return if ( $self->is_trusted_ip_address() );
80 104 50       814 return if ( $self->is_authenticated() );
81              
82 104         992 my $spf_server = $self->get_object('spf_server');
83 104 50       540 if ( ! $spf_server ) {
84 0         0 $self->log_error( 'SPF Setup Error' );
85 0         0 $self->metric_count( 'spf_total', { 'result' => 'error' } );
86 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( 'temperror' );
87 0         0 $self->add_auth_header($header);
88 0         0 return;
89             }
90              
91 104         484 my $scope = 'mfrom';
92              
93 104 100       461 $env_from = q{} if $env_from eq '<>';
94              
95 104         335 my $identity;
96             my $domain;
97 104 100       387 if ( !$env_from ) {
98 5         21 $identity = $self->{'helo_name'};
99 5         14 $domain = $identity;
100 5         30 $scope = 'helo';
101             }
102             else {
103 99         798 $identity = $self->get_address_from($env_from);
104 99         805 $domain = $self->get_domain_from($identity);
105             }
106              
107 104 50       630 if ( !$identity ) {
108 0         0 $identity = $self->{'helo_name'};
109 0         0 $domain = $identity;
110 0         0 $scope = 'helo';
111             }
112              
113 104         294 eval {
114             my $spf_request = Mail::SPF::Request->new(
115             'versions' => [1],
116             'scope' => $scope,
117             'identity' => $identity,
118             'ip_address' => $self->ip_address(),
119 104         856 'helo_identity' => $self->{'helo_name'},
120             );
121              
122 104         139236 my $spf_result = $spf_server->process($spf_request);
123 104         1523024 my $spf_results = $self->get_object('spf_results');
124 104 50       650 $spf_results = [] if ! $spf_results;
125 104         385 push @$spf_results, $spf_result;
126 104         882 $self->set_object('spf_results',$spf_results,1);
127              
128 104         809 my $result_code = $spf_result->code();
129              
130             # Best Guess SPF based on org domain
131             # ToDo report this in both metrics and AR header
132 104         341 my $auth_domain;
133 104 100       567 if ( $result_code eq 'none' ) {
134 14 50       75 if ( $config->{'best_guess'} ) {
135 0 0       0 if ( $self->is_handler_loaded( 'DMARC' ) ) {
136 0         0 my $dmarc_handler = $self->get_handler('DMARC');
137 0         0 my $dmarc_object = $dmarc_handler->get_dmarc_object();
138 0 0       0 if ( $domain ) {
139 0         0 my $org_domain = eval{ $dmarc_object->get_organizational_domain( $domain ); };
  0         0  
140 0         0 $self->handle_exception( $@ );
141 0 0       0 if ( $org_domain ne $domain ) {
142 0         0 $auth_domain = $org_domain;
143             $spf_request = Mail::SPF::Request->new(
144             'versions' => [1],
145             'scope' => $scope,
146             'identity' => $identity,
147             'authority_domain' => $org_domain,
148             'ip_address' => $self->ip_address(),
149 0         0 'helo_identity' => $self->{'helo_name'},
150             );
151 0         0 $spf_result = $spf_server->process($spf_request);
152 0         0 my $spf_results = $self->get_object('spf_results');
153 0 0       0 $spf_results = [] if ! $spf_results;
154 0         0 push @$spf_results, $spf_result;
155 0         0 $self->set_object('spf_results',$spf_results,1);
156 0         0 $result_code = $spf_result->code();
157             }
158             }
159             }
160             }
161             }
162              
163 104         1087 $self->metric_count( 'spf_total', { 'result' => $result_code } );
164              
165 104         1894 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( $result_code );
166 104 50       9954 if ( $auth_domain ) {
167 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.authdomain' )->safe_set_value( $auth_domain ) );
168             }
169 104         1028 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.mailfrom' )->safe_set_value( $self->get_address_from( $env_from ) ) );
170 104         10337 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.helo' )->safe_set_value( $self->{ 'helo_name' } ) );
171 104 100 100     11235 if ( !( $config->{'hide_none'} && $result_code eq 'none' ) ) {
172 100         871 $self->add_auth_header($header);
173             }
174              
175             # Set for DMARC
176 104         543 $self->{'dmarc_domain'} = $domain;
177 104         461 $self->{'dmarc_scope'} = $scope;
178 104         439 $self->{'dmarc_result'} = $result_code;
179              
180 104         583 $self->dbgout( 'SPFCode', $result_code, LOG_DEBUG );
181              
182 104 50       944 if ( !( $config->{'hide_received-spf_header'} ) ) {
183 104 100 100     1143 if ( !( $config->{'hide_none'} && $result_code eq 'none' ) ) {
184 100         986 my $result_header = $spf_result->received_spf_header();
185 100         88925 my ( $header, $value ) = split( ': ', $result_header, 2 );
186 100         710 $value = $self->wrap_header( $value );
187 100         989 $self->prepend_header( $header, $value );
188 100         565 $self->dbgout( 'SPFHeader', $result_header, LOG_DEBUG );
189             }
190             }
191             };
192 104 50       1117 if ( my $error = $@ ) {
193 0         0 $self->handle_exception( $error );
194 0         0 $self->log_error( 'SPF Error ' . $error );
195 0         0 $self->{'failmode'} = 1;
196 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( 'temperror' );
197 0         0 $self->add_auth_header($header);
198 0         0 $self->metric_count( 'spf_total', { 'result' => 'error' } );
199             }
200             }
201              
202             sub close_callback {
203 118     118 0 369 my ( $self ) = @_;
204 118         449 delete $self->{'dmarc_domain'};
205 118         341 delete $self->{'dmarc_scope'};
206 118         324 delete $self->{'dmarc_result'};
207 118         336 delete $self->{'failmode'};
208 118         332 delete $self->{'helo_name'};
209 118         626 $self->destroy_object('spf_results');
210             }
211              
212             1;
213              
214             __END__
215              
216             =pod
217              
218             =encoding UTF-8
219              
220             =head1 NAME
221              
222             Mail::Milter::Authentication::Handler::SPF - Handler class for SPF
223              
224             =head1 VERSION
225              
226             version 3.20230629
227              
228             =head1 DESCRIPTION
229              
230             Implements the SPF standard checks.
231              
232             =head1 CONFIGURATION
233              
234             "SPF" : { | Config for the SPF Module
235             "hide_received-spf_header" : 0, | Do not add the "Received-SPF" header
236             "hide_none" : 0, | Hide auth line if the result is 'none'
237             | if not hidden at all
238             "best_guess" : 0 | Fallback to Org domain for SPF checks
239             | if result is none.
240             },
241              
242             =head1 AUTHOR
243              
244             Marc Bradshaw <marc@marcbradshaw.net>
245              
246             =head1 COPYRIGHT AND LICENSE
247              
248             This software is copyright (c) 2020 by Marc Bradshaw.
249              
250             This is free software; you can redistribute it and/or modify it under
251             the same terms as the Perl 5 programming language system itself.
252              
253             =cut