File Coverage

blib/lib/Mail/Qmail/Filter.pm
Criterion Covered Total %
statement 34 75 45.3
branch 1 30 3.3
condition 0 15 0.0
subroutine 13 18 72.2
pod 6 6 100.0
total 54 144 37.5


line stmt bran cond sub pod time code
1 1     1   2307 use 5.014;
  1         4  
2 1     1   5 use warnings;
  1         2  
  1         51  
3              
4             package Mail::Qmail::Filter;
5              
6             our $VERSION = '1.21';
7              
8 1     1   5 use Carp qw(confess);
  1         2  
  1         47  
9 1     1   480 use FindBin ();
  1         1014  
  1         22  
10 1     1   515 use IO::Handle ();
  1         6239  
  1         28  
11 1     1   435 use Mail::Qmail::Filter::Util qw(addresses_to_hash match_address);
  1         3  
  1         67  
12 1     1   440 use MailX::Qmail::Queue::Message;
  1         3  
  1         34  
13 1     1   7 use Scalar::Util qw(blessed);
  1         1  
  1         55  
14              
15 1     1   457 use namespace::clean;
  1         15334  
  1         6  
16              
17             # Must be under namespace::clean for coercion to work:
18 1     1   730 use Mo qw(coerce default);
  1         492  
  1         5  
19              
20             my $feedback_fh; # Open ASAP before the handle gets reused:
21              
22             BEGIN {
23 1 50   1   1753 $feedback_fh = IO::Handle->new_from_fd( 4, 'w' )
24             or warn "Cannot open feedback handle: $!";
25             }
26              
27             has 'defer_only';
28             has 'feedback_fh' => $feedback_fh;
29             has 'filters' => [];
30             has 'reject_text' => 'Rejected.';
31             has 'skip_for_from' => coerce => \&addresses_to_hash;
32             has 'skip_for_rcpt' => coerce => \&addresses_to_hash;
33             has 'skip_for_sender' => coerce => \&addresses_to_hash;
34             has 'skip_if_relayclient';
35              
36             my @debug;
37              
38             sub debug {
39 1     1 1 3 my $self = shift;
40 1         6 push @debug, join ': ', @_;
41             }
42              
43             $SIG{__DIE__} //= sub {
44             __PACKAGE__->debug( died => "@_" ) unless $^S;
45             die @_;
46             };
47              
48             sub add_filters {
49 0     0 1   my $self = shift;
50 0           while ( defined( my $filter = shift ) ) {
51 0 0         unless ( blessed($filter) ) {
52 0 0 0       my $opt = shift if @_ && 'HASH' eq ref $_[0];
53 0 0         $filter = __PACKAGE__ . $filter if $filter =~ /^::/;
54 0           eval "use $filter";
55 0 0         confess $@ if length $@;
56 0           $filter = $filter->new(%$opt);
57             }
58 0           push @{ $self->{filters} }, $filter;
  0            
59             }
60 0           $self;
61             }
62              
63             sub filter {
64 0     0 1   my $self = shift;
65              
66 0           $_->run for @{ $self->filters };
  0            
67             }
68              
69             sub message {
70 0 0   0 1   state $message = MailX::Qmail::Queue::Message->receive
71             or die "Invalid message\n";
72             }
73              
74             sub reject {
75 0     0 1   my $self = shift;
76 0   0       my $reject_text = shift // $self->reject_text;
77 0 0 0       $reject_text = $reject_text->(@_)
78             if ref $reject_text && 'CODE' eq ref $reject_text;
79 0 0         $self->feedback_fh->print( $self->defer_only ? 'Z' : 'D', $reject_text );
80 0           $self->debug( action => 'reject' );
81 0           exit 88;
82             }
83              
84             sub run {
85 0     0 1   my $self = shift;
86              
87 0           my $package = ref $self;
88              
89 0 0 0       if ( exists $ENV{RELAYCLIENT} && $self->skip_if_relayclient ) {
90 0           $self->debug("$package skipped");
91 0           return;
92             }
93              
94 0 0         if ( my $skip_for_sender = $self->skip_for_sender ) {
95 0 0         if (
96             match_address(
97             $skip_for_sender, my $sender = $self->message->from
98             )
99             )
100             {
101 0           $self->debug( "$package skipped because of sender", $sender );
102 0           return;
103             }
104             }
105              
106 0 0 0       if ( ( my $skip_for_from = $self->skip_for_from )
107             && ( my $from = $self->message->header_from ) )
108             {
109 0 0         if ( match_address( $skip_for_from, $from = $from->address ) ) {
110 0           $self->debug( "$package skipped because of RFC5322.From", $from );
111 0           return;
112             }
113             }
114              
115 0 0         if ( my $skip_for_rcpt = $self->skip_for_rcpt ) {
116 0           for ( $self->message->to ) {
117 0 0         next unless match_address( $skip_for_rcpt, $_ );
118 0           $self->debug( "$package skipped because of rcpt", $_ );
119 0           return;
120             }
121             }
122              
123 0           $self->debug("$package started");
124 0           $self->filter;
125             }
126              
127             END {
128 1     1   960 __PACKAGE__->debug( 'exit code' => $? );
129 1         48 say STDERR "$FindBin::Script\[$$]: " . join '; ', @debug;
130             }
131              
132             __END__
133              
134             =head1 NAME
135              
136             Mail::Qmail::Filter - filter e-mails in qmail-queue context
137              
138             =head1 SYNOPSIS
139              
140             use Mail::Qmail::Filter;
141            
142             Mail::Qmail::Filter->new->add_filter(
143             '::LogEnvelope',
144             '::DMARC' => {
145             skip_if_relayclient => 1,
146             },
147             '::CheckDeliverability' => {
148             match => qr{/ezmlm-(?:checksub|reject)\b},
149             skip_if_relayclient => 1,
150             },
151             '::SpamAssassin' => {
152             skip_if_relayclient => 1,
153             reject_score => 5.2,
154             reject_text => 'I think your message is spam.',
155             },
156             '::Queue',
157             )->run;
158              
159             =head1 DESCRIPTION
160              
161             Mail::Qmail::Filter and its submodules are designed to help you filter
162             incoming e-mails when using L<qmail|http://netqmail.org/> as MTA.
163              
164             You should use it like so:
165              
166             =over 4
167              
168             =item 1.
169              
170             Write a frontend script to configure your filters,
171             like the one in the L</SYNOPSIS>.
172              
173             =item 2.
174              
175             In the run file for your C<qmail-smtpd> instance,
176             e.g. C</var/qmail/supervise/qmail-smtpd/run>,
177              
178             export QMAILQUEUE=path_to_your_frontend_script
179              
180             =back
181              
182             In each filter, you may do various things:
183              
184             =over 4
185              
186             =item *
187              
188             examine and change envelope data (RFC5321.MailFrom and recipients)
189              
190             =item *
191              
192             examine and modify the e-mail message (header and/or body)
193              
194             =item *
195              
196             L</reject> e-mails (or L<defer|/defer_only> them)
197              
198             =back
199              
200             =head1 FILTERS INCLUDED
201              
202             This distribution ships with the following predefined filters:
203              
204             =head2 Rejecting filters
205              
206             =over 4
207              
208             =item L<Mail::Qmail::Filter::CheckDeliverability>
209              
210             check deliverability according to .qmail files
211              
212             =item L<Mail::Qmail::Filter::DMARC>
213              
214             validate message against DMARC policy of the sender domain
215              
216             =item L<Mail::Qmail::Filter::RequireFrom>
217              
218             only allow certain RFC322.From addresses
219              
220             =item L<Mail::Qmail::Filter::SpamAssassin>
221              
222             spam-check message
223              
224             =item L<Mail::Qmail::Filter::ValidateFrom>
225              
226             validate RFC5322.From
227              
228             =item L<Mail::Qmail::Filter::ValidateSender>
229              
230             validate RFC5321.MailFrom
231              
232             =back
233              
234             =head2 Envelope modifying filters
235              
236             =over 4
237              
238             =item L<Mail::Qmail::Filter::RewriteSender>
239              
240             =back
241              
242             =head2 Header modifying filters
243              
244             =over 4
245              
246             =item L<Mail::Qmail::Filter::RewriteFrom>
247              
248             =back
249              
250             =head2 Logging-only filters
251              
252             =over 4
253              
254             =item L<Mail::Qmail::Filter::Dump>
255              
256             =item L<Mail::Qmail::Filter::LogEnvelope>
257              
258             =back
259              
260             =head2 Experimental filters
261              
262             =over 4
263              
264             =item L<Mail::Qmail::Filter::SkipQueue>
265              
266             =back
267              
268             =head1 COMMON PARAMETERS FOR ALL FILTERS
269              
270             =head2 skip_if_relayclient
271              
272             When set to a true calue, the L</run> method will skip the filter when
273             the environment variable C<RELAYCLIENT> exists.
274              
275             =head2 skip_for_sender
276              
277             Takes an e-mail address or a reference to a list of such.
278             The L</run> method will then skip the filter if the RFC5321.MailFrom address
279             of the L</message> is one of these.
280              
281             =head2 skip_for_from
282              
283             Takes an e-mail address or a reference to a list of such.
284             The L</run> method will then skip the filter if the RFC5322.From address
285             of the L</message> is one of these.
286              
287             =head2 skip_for_rcpt
288              
289             Takes an e-mail address or a reference to a list of such.
290             The L</run> method will then skip the filter if at least one of the recipients
291             in the envelope of the L</message> is one of these.
292              
293             =head2 defer_only
294              
295             When set to a true value, calls to the L</reject> method will
296             result in status code C<451>, that is, the message should only
297             be deferred on the sender side.
298              
299             =head1 METHODS
300              
301             =head2 add_filters
302              
303             Configure the filters you want to use.
304             Takes a list of filter packages to run in order.
305              
306             You may pass instances of filter objects here,
307             but usually it is more convenient to specify filters using their package name,
308             optionally followed by a hash of options.
309             C<add_filters> will then construct the filter object for you.
310             If your filter lives below the C<Mail::Qmail::Filter::> namespace,
311             you may abbreviate this prefix with C<::>.
312             Please see example in the L</SYNOPSIS> above.
313              
314             C<add_filters> may be called several times to add more and more filters,
315             but you can also just specify them all in one call.
316              
317             C<add_filters> will return the main L<Mail::Qmail::Filter> object,
318             so you may chain other methods, like L</run>.
319              
320             =head2 run
321              
322             checks if the filter should be skipped by evaluating the
323             L</OPTIONS COMMON TO ALL FILTERS>.
324             If not, runs it by calling its L</filter> method.
325              
326             =head2 filter
327              
328             Does the actual work:
329             Reads the message from C<qmail-smtpd>,
330             runs the filters which where L<added|/-E<gt>add_filters>
331             and if has not been L</reject>ed,
332             forwards the message to C<qmail-queue>.
333              
334             When L</WRITING YOUR OWN FILTERS>, overwrite this method
335             with what your filter is supposed to do.
336              
337             =head2 message
338              
339             returns the L<MailX::Qmail::Queue::Message> to be filtered
340              
341             =head2 reject
342              
343             rejects the message with status C<554> (default)
344             or with C<451> when L</defer_only> is set.
345             Stops the execution of the script; no further filters will be run,
346             and the message will I<not> be passed on to C<qmail-queue>.
347              
348             As first argument, expects the reply text the server should send to the client
349             or a L<sub|perlfunc/sub>routine which returns this reply text.
350             Additional arguments will be passed to this L<sub|perlfunc/sub>routine,
351             which is handy if you for example want to include an e-mail address which
352             caused the rejection.
353              
354             Please note that you should only use ASCII characters for the reply text and
355             that C<qmail-smtpd> usually limits its length to 254 characters.
356              
357             =head2 debug
358              
359             collects logging messages.
360             When the script finishes,
361             these will be automatically written to standard error, separated with C<; >s.
362             You should then find them in the log file of your C<qmail-smtpd>,
363             prefixed with the name of your frontend script.
364              
365             When passing several arguments, these will be L<joined|perlfunc/join> with
366             C<: >.
367              
368             =head1 WRITING YOUR OWN FILTERS
369              
370             For the L</COMMON OPTIONS FOR ALL FILTERS> to work properly,
371             your package has to:
372              
373             use Mo 'coerce';
374             extends 'Mail::Qmail::Filter';
375              
376             Apart from that, you only have to define a filter method
377             which does the actual work.
378              
379             For further insight, please have a look at the source code of the various
380             L</FILTERS INCLUDED> in this distribution.
381              
382             =head1 SEE ALSO
383              
384             L<MailX::Qmail::Queue::Message> and the L<FILTERS INCLUDED>.
385              
386             =head1 LICENSE AND COPYRIGHT
387              
388             Copyright 2019 Martin Sluka.
389              
390             This module is free software; you can redistribute it and/or modify it
391             under the terms of the the Artistic License (2.0). You may obtain a
392             copy of the full license at:
393              
394             L<http://www.perlfoundation.org/artistic_license_2_0>
395              
396             Any use, modification, and distribution of the Standard or Modified
397             Versions is governed by this Artistic License. By using, modifying or
398             distributing the Package, you accept this license. Do not use, modify,
399             or distribute the Package, if you do not accept this license.
400              
401             If your Modified Version has been derived from a Modified Version made
402             by someone other than you, you are nevertheless required to ensure that
403             your Modified Version complies with the requirements of this license.
404              
405             This license does not grant you the right to use any trademark, service
406             mark, tradename, or logo of the Copyright Holder.
407              
408             This license includes the non-exclusive, worldwide, free-of-charge
409             patent license to make, have made, use, offer to sell, sell, import and
410             otherwise transfer the Package with respect to any patent claims
411             licensable by the Copyright Holder that are necessarily infringed by the
412             Package. If you institute patent litigation (including a cross-claim or
413             counterclaim) against any party alleging that the Package constitutes
414             direct or contributory patent infringement, then this Artistic License
415             to you shall terminate on the date that such litigation is filed.
416              
417             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
418             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
419             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
420             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
421             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
422             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
423             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
424             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
425              
426             =cut