File Coverage

blib/lib/Mail/Milter/Authentication/Handler/SPF.pm
Criterion Covered Total %
statement 102 131 77.8
branch 19 36 52.7
condition 2 6 33.3
subroutine 15 17 88.2
pod 1 8 12.5
total 139 198 70.2


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::SPF;
2 26     26   10131 use strict;
  26         86  
  26         764  
3 26     26   146 use warnings;
  26         63  
  26         714  
4 26     26   391 use base 'Mail::Milter::Authentication::Handler';
  26         65  
  26         3021  
5             our $VERSION = '20191206'; # VERSION
6              
7 26     26   177 use Sys::Syslog qw{:standard :macros};
  26         67  
  26         8347  
8 26     26   204 use Mail::AuthenticationResults::Header::Entry;
  26         74  
  26         702  
9 26     26   134 use Mail::AuthenticationResults::Header::SubEntry;
  26         60  
  26         547  
10 26     26   141 use Mail::AuthenticationResults::Header::Comment;
  26         66  
  26         861  
11              
12 26     26   163 use Mail::SPF;
  26         58  
  26         27902  
13              
14             sub default_config {
15             return {
16 0     0 0 0 'hide_received-spf_header' => 0,
17             'hide_none' => 0,
18             'best_guess' => 0,
19             };
20             }
21              
22             sub grafana_rows {
23 0     0 0 0 my ( $self ) = @_;
24 0         0 my @rows;
25 0         0 push @rows, $self->get_json( 'SPF_metrics' );
26 0         0 return \@rows;
27             }
28              
29             sub setup_callback {
30 111     111 0 384 my ( $self ) = @_;
31              
32             $self->set_object_maker( 'spf_server' , sub {
33 34     34   110 my ( $self, $name ) = @_;
34 34         84 my $thischild = $self->{'thischild'};
35 34         215 $self->dbgout( 'Object created', $name, LOG_DEBUG );
36 34         84 my $object;
37 34         91 eval {
38 34         317 my $resolver = $self->get_object('resolver');
39 34         354 $object = Mail::SPF::Server->new(
40             'hostname' => $self->get_my_hostname(),
41             'dns_resolver' => $resolver,
42             );
43             };
44 34 50       17119 if ( my $error = $@ ) {
45 0         0 $self->handle_exception( $error );
46 0         0 $self->log_error( 'SPF Object Setup Error ' . $error );
47             }
48 34         240 $thischild->{'object'}->{$name} = {
49             'object' => $object,
50             'destroy' => 0,
51             };
52 111         1595 });
53 111         823 return;
54             }
55              
56             sub register_metrics {
57             return {
58 25     25 1 156 'spf_total' => 'The number of emails processed for SPF',
59             };
60             }
61              
62             sub wrap_header {
63 50     50 0 189 my ( $self, $value ) = @_;
64 50         399 $value =~ s/ /\n /;
65 50         352 $value =~ s/\) /\)\n /;
66 50         351 $value =~ s/; /;\n /g;
67 50         160 return $value;
68             }
69              
70             sub helo_callback {
71 82     82 0 311 my ( $self, $helo_host ) = @_;
72 82         377 $self->{'failmode'} = 0;
73 82         249 $self->{'helo_name'} = $helo_host;
74 82         235 return;
75             }
76              
77             sub envfrom_callback {
78              
79             # On MAILFROM
80             #...
81 82     82 0 345 my ( $self, $env_from ) = @_;
82 82         447 my $config = $self->handler_config();
83 82 100       401 return if ( $self->is_local_ip_address() );
84 74 100       350 return if ( $self->is_trusted_ip_address() );
85 72 50       324 return if ( $self->is_authenticated() );
86              
87 72         370 my $spf_server = $self->get_object('spf_server');
88 72 50       323 if ( ! $spf_server ) {
89 0         0 $self->log_error( 'SPF Setup Error' );
90 0         0 $self->metric_count( 'spf_total', { 'result' => 'error' } );
91 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( 'temperror' );
92 0         0 $self->add_auth_header($header);
93 0         0 return;
94             }
95              
96 72         244 my $scope = 'mfrom';
97              
98 72 100       302 $env_from = q{} if $env_from eq '<>';
99              
100 72         233 my $identity;
101             my $domain;
102 72 100       273 if ( !$env_from ) {
103 5         17 $identity = $self->{'helo_name'};
104 5         15 $domain = $identity;
105 5         18 $scope = 'helo';
106             }
107             else {
108 67         622 $identity = $self->get_address_from($env_from);
109 67         616 $domain = $self->get_domain_from($identity);
110             }
111              
112 72 50       289 if ( !$identity ) {
113 0         0 $identity = $self->{'helo_name'};
114 0         0 $domain = $identity;
115 0         0 $scope = 'helo';
116             }
117              
118 72         212 eval {
119             my $spf_request = Mail::SPF::Request->new(
120             'versions' => [1],
121             'scope' => $scope,
122             'identity' => $identity,
123             'ip_address' => $self->ip_address(),
124 72         519 'helo_identity' => $self->{'helo_name'},
125             );
126              
127 50         65629 my $spf_result = $spf_server->process($spf_request);
128              
129 50         1131607 my $result_code = $spf_result->code();
130              
131             # Best Guess SPF based on org domain
132             # ToDo report this in both metrics and AR header
133 50         227 my $auth_domain;
134 50 50       256 if ( $result_code eq 'none' ) {
135 0 0       0 if ( $config->{'best_guess'} ) {
136 0 0       0 if ( $self->is_handler_loaded( 'DMARC' ) ) {
137 0         0 my $dmarc_handler = $self->get_handler('DMARC');
138 0         0 my $dmarc_object = $dmarc_handler->get_dmarc_object();
139 0 0       0 if ( $domain ) {
140 0         0 my $org_domain = eval{ $dmarc_object->get_organizational_domain( $domain ); };
  0         0  
141 0         0 $self->handle_exception( $@ );
142 0 0       0 if ( $org_domain ne $domain ) {
143 0         0 $auth_domain = $org_domain;
144             $spf_request = Mail::SPF::Request->new(
145             'versions' => [1],
146             'scope' => $scope,
147             'identity' => $identity,
148             'authority_domain' => $org_domain,
149             'ip_address' => $self->ip_address(),
150 0         0 'helo_identity' => $self->{'helo_name'},
151             );
152 0         0 $spf_result = $spf_server->process($spf_request);
153 0         0 $result_code = $spf_result->code();
154             }
155             }
156             }
157             }
158             }
159              
160 50         572 $self->metric_count( 'spf_total', { 'result' => $result_code } );
161              
162 50         536 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( $result_code );
163 50 50       4357 if ( $auth_domain ) {
164 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'policy.authdomain' )->safe_set_value( $auth_domain ) );
165             }
166 50         324 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.mailfrom' )->safe_set_value( $self->get_address_from( $env_from ) ) );
167 50         4401 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.helo' )->safe_set_value( $self->{ 'helo_name' } ) );
168 50 50 33     4574 if ( !( $config->{'hide_none'} && $result_code eq 'none' ) ) {
169 50         453 $self->add_auth_header($header);
170             }
171              
172             # Set for DMARC
173 50         236 $self->{'dmarc_domain'} = $domain;
174 50         231 $self->{'dmarc_scope'} = $scope;
175 50         183 $self->{'dmarc_result'} = $result_code;
176              
177 50         323 $self->dbgout( 'SPFCode', $result_code, LOG_INFO );
178              
179 50 50       233 if ( !( $config->{'hide_received-spf_header'} ) ) {
180 50 50 33     281 if ( !( $config->{'hide_none'} && $result_code eq 'none' ) ) {
181 50         604 my $result_header = $spf_result->received_spf_header();
182 50         40006 my ( $header, $value ) = split( ': ', $result_header, 2 );
183 50         323 $value = $self->wrap_header( $value );
184 50         477 $self->prepend_header( $header, $value );
185 50         188 $self->dbgout( 'SPFHeader', $result_header, LOG_DEBUG );
186             }
187             }
188             };
189 72 100       367 if ( my $error = $@ ) {
190 22         118 $self->handle_exception( $error );
191 22         119 $self->log_error( 'SPF Error ' . $error );
192 22         68 $self->{'failmode'} = 1;
193 22         151 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'spf' )->safe_set_value( 'temperror' );
194 22         1795 $self->add_auth_header($header);
195 22         114 $self->metric_count( 'spf_total', { 'result' => 'error' } );
196             }
197              
198 72         279 return;
199             }
200              
201             sub close_callback {
202 97     97 0 275 my ( $self ) = @_;
203 97         275 delete $self->{'dmarc_domain'};
204 97         257 delete $self->{'dmarc_scope'};
205 97         234 delete $self->{'dmarc_result'};
206 97         243 delete $self->{'failmode'};
207 97         368 delete $self->{'helo_name'};
208 97         252 return;
209             }
210              
211             1;
212              
213             __END__
214              
215             =pod
216              
217             =encoding UTF-8
218              
219             =head1 NAME
220              
221             Mail::Milter::Authentication::Handler::SPF
222              
223             =head1 VERSION
224              
225             version 20191206
226              
227             =head1 DESCRIPTION
228              
229             Implements the SPF standard checks.
230              
231             =head1 CONFIGURATION
232              
233             "SPF" : { | Config for the SPF Module
234             "hide_received-spf_header" : 0, | Do not add the "Received-SPF" header
235             "hide_none" : 0, | Hide auth line if the result is 'none'
236             | if not hidden at all
237             "best_guess" : 0 | Fallback to Org domain for SPF checks
238             | if result is none.
239             },
240              
241             =head1 AUTHOR
242              
243             Marc Bradshaw <marc@marcbradshaw.net>
244              
245             =head1 COPYRIGHT AND LICENSE
246              
247             This software is copyright (c) 2018 by Marc Bradshaw.
248              
249             This is free software; you can redistribute it and/or modify it under
250             the same terms as the Perl 5 programming language system itself.
251              
252             =cut