File Coverage

blib/lib/Mail/Qmail/Filter.pm
Criterion Covered Total %
statement 34 81 41.9
branch 1 30 3.3
condition 0 15 0.0
subroutine 13 20 65.0
pod 7 7 100.0
total 55 153 35.9


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