File Coverage

blib/lib/Mail/Qmail/Filter/DMARC.pm
Criterion Covered Total %
statement 11 40 27.5
branch 0 8 0.0
condition 0 3 0.0
subroutine 4 5 80.0
pod 1 1 100.0
total 16 57 28.0


line stmt bran cond sub pod time code
1 1     1   788 use 5.014;
  1         3  
2 1     1   5 use warnings;
  1         1  
  1         214  
3              
4              
5             our $VERSION = '1.21';
6              
7             shift =~ s/.*\@//r;
8             }
9              
10             my ( $key, $value, @additional_checks ) = @_;
11             return unless defined $value && length $value;
12             $_->($value) or return for @additional_checks;
13             $key => $value;
14             }
15              
16             require Mail::DMARC::Base;
17             Mail::DMARC::Base->new->is_valid_domain(shift);
18             }
19              
20             require Mail::SPF;
21             my $request = Mail::SPF::Request->new(@_);
22             state $server = Mail::SPF::Server->new;
23             $server->process($request);
24             }
25              
26             use namespace::clean;
27              
28             use Mo qw(coerce default);
29             extends 'Mail::Qmail::Filter';
30              
31 1     1   7 has 'dry_run';
  1         1  
  1         5  
32             has 'reject_text' => 'Failed DMARC test.';
33 1     1   203  
  1         2  
  1         4  
34             my $self = shift;
35             my $message = $self->message;
36              
37             require Mail::DKIM::Verifier; # lazy load because filter might be skipped
38             my $dkim = Mail::DKIM::Verifier->new;
39             $dkim->PRINT( $message->body =~ s/\cM?\cJ/\cM\cJ/gr );
40 0     0 1   $dkim->CLOSE;
41 0           $self->debug( 'DKIM result' => $dkim->result );
42              
43 0           if ( $dkim->result ne 'pass' ) {
44 0            
45 0           $self->debug( 'Remote IP' => $ENV{TCPREMOTEIP} );
46 0            
47 0           my %spf_query = ( ip_address => $ENV{TCPREMOTEIP} );
48              
49 0 0         $self->debug( helo => $spf_query{helo_identity} = $message->helo );
50              
51 0           my $header_from = $message->header_from;
52             my $header_from_domain;
53 0           if ($header_from) {
54             $self->debug( 'RFC5322.From' => $spf_query{identity} =
55 0           $header_from->address );
56             $header_from_domain = $header_from->host;
57 0           $spf_query{scope} = 'mfrom';
58 0           }
59 0 0         else {
60             $spf_query{scope} = 'helo';
61 0            
62 0           # identity required by Mail::SPF:
63 0           $spf_query{identity} = $spf_query{helo_identity};
64             }
65              
66 0           $self->debug( 'SPF result' => my $spf_result = spf_query(%spf_query) );
67             $message->add_header( $spf_result->received_spf_header );
68              
69 0           require Mail::DMARC::PurePerl;
70             my $dmarc_text = (
71             my $dmarc_result = Mail::DMARC::PurePerl->new(
72 0           source_ip => $ENV{TCPREMOTEIP},
73 0           envelope_to => domain( ( $message->to )[0] ),
74             if_set(
75 0           envelope_from => domain( $message->from ),
76             \&is_valid_domain
77             ),
78             if_set(
79             header_from => $header_from_domain,
80             \&is_valid_domain
81             ),
82             dkim => $dkim,
83             spf => {
84             if_set( domain => $header_from_domain ),
85             scope => $spf_query{scope},
86             result => $spf_result->code,
87             },
88             )->validate
89             )->result;
90             $self->debug( 'DMARC result' => $dmarc_text );
91             $message->add_header("DMARC-Status: $dmarc_text");
92 0            
93             if ( $dmarc_result->result ne 'pass' ) {
94             my $disposition = $dmarc_result->disposition;
95             $self->debug( 'DMARC disposition' => $disposition );
96 0           $self->reject( $self->reject_text )
97 0           if $disposition eq 'reject' && !$self->dry_run;
98             }
99 0 0         }
100 0           }
101 0            
102 0 0 0       1;
103              
104              
105             =head1 NAME
106              
107             Mail::Qmail::Filter::DMARC -
108             verify DMARC policy of mail message
109              
110             =head1 SYNOPSIS
111              
112             use Mail::Qmail::Filter;
113              
114             Mail::Qmail::Filter->new->add_filters(
115             '::DMARC' => {
116             skip_if_relayclient => 1,
117             },
118             '::Queue',
119             )->run;
120              
121             =head1 DESCRIPTION
122              
123             This L<Mail::Qmail::Filter> plugin verifies if the incoming e-mail message
124             conforms to the DMARC policy of its sender domain:
125              
126             =over 4
127              
128             =item 1.
129              
130             The plugin is skipped if imported with feature C<:skip_for_relayclient>
131             and the message comes from a relay client.
132              
133             =item 2.
134              
135             We check if the message contains a valid DKIM signature
136             matching the domain of the C<From:> header field.
137             If this is the case, the e-mail is passed on.
138              
139             =item 3.
140              
141             If not, a SPF check is done, and a C<Received-SPF:> header field is added to
142             the message.
143             Then we check if the message is aligned with its sender's DMARC policy.
144             A C<DMARC-Status:> header field is added.
145              
146             If the message does not align to the policy, the policy advises to reject such
147             messages and when the plugin is C<use>d with the C<:reject> feature or the
148             environment variable C<DMARC_REJECT> is set to a true value, the message will
149             be rejected with C<554 Failed DMARC test.>
150              
151             =back
152              
153             Please note: This only works for valid sender addresses.
154             If the message has no valid RFC5322.From, this filter will I<not> reject
155             the message, because L<Mail::DMARC> does not like invalid sender addresses.
156             If you also happen to not like these, please use
157             L<Mail::Qmail::Filter::ValidateFrom> and/or
158             L<Mail::Qmail::Filter::ValidateSender> I<before> this filter.
159              
160             =head1 OPTIONAL PARAMETERS
161              
162             =head2 dry_run
163              
164             When set to a true value, the message is only marked, not rejected.
165              
166             =head2 reject_text
167              
168             Reply text to send to the client when the message is rejected.
169              
170             Default: C<Failed DMARC test.>
171              
172             =head1 SEE ALSO
173              
174             L<Mail::Qmail::Filter/COMMON PARAMETERS FOR ALL FILTERS>,
175             L<Mail::Qmail::Filter::ValidateFrom>, L<Mail::Qmail::Filter::ValidateSender>
176              
177             =head1 LICENSE AND COPYRIGHT
178              
179             Copyright 2019 Martin Sluka.
180              
181             This module is free software; you can redistribute it and/or modify it
182             under the terms of the the Artistic License (2.0). You may obtain a
183             copy of the full license at:
184              
185             L<http://www.perlfoundation.org/artistic_license_2_0>
186              
187             Any use, modification, and distribution of the Standard or Modified
188             Versions is governed by this Artistic License. By using, modifying or
189             distributing the Package, you accept this license. Do not use, modify,
190             or distribute the Package, if you do not accept this license.
191              
192             If your Modified Version has been derived from a Modified Version made
193             by someone other than you, you are nevertheless required to ensure that
194             your Modified Version complies with the requirements of this license.
195              
196             This license does not grant you the right to use any trademark, service
197             mark, tradename, or logo of the Copyright Holder.
198              
199             This license includes the non-exclusive, worldwide, free-of-charge
200             patent license to make, have made, use, offer to sell, sell, import and
201             otherwise transfer the Package with respect to any patent claims
202             licensable by the Copyright Holder that are necessarily infringed by the
203             Package. If you institute patent litigation (including a cross-claim or
204             counterclaim) against any party alleging that the Package constitutes
205             direct or contributory patent infringement, then this Artistic License
206             to you shall terminate on the date that such litigation is filed.
207              
208             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
209             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
210             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
211             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
212             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
213             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
214             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
215             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
216              
217             =cut