File Coverage

blib/lib/Mail/Milter/Authentication/Handler/Blocker.pm
Criterion Covered Total %
statement 67 74 90.5
branch 15 18 83.3
condition 3 3 100.0
subroutine 16 17 94.1
pod 1 8 12.5
total 102 120 85.0


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::Blocker;
2 2     2   1843 use 5.20.0;
  2         14  
3 2     2   13 use strict;
  2         14  
  2         43  
4 2     2   38 use warnings;
  2         8  
  2         59  
5 2     2   13 use Mail::Milter::Authentication::Pragmas;
  2         5  
  2         21  
6             # ABSTRACT: Block mail based on simple rules
7             our $VERSION = '3.20230629'; # VERSION
8 2     2   599 use base 'Mail::Milter::Authentication::Handler';
  2         10  
  2         192  
9 2     2   16 use TOML;
  2         5  
  2         2468  
10              
11             sub register_metrics {
12             return {
13 4     4 1 21 'blocker_total' => 'The number of emails blocked by blocker',
14             };
15             }
16              
17             sub _load_blocker_config_file {
18 17     17   43 my ( $self, $filename ) = @_;
19 17         35 my $blocker_config = {};
20 17 50       472 if ( -e $filename ) {
21 17         774 open ( my $inf, '<', $filename );
22 17         59 my $body = do { local $/; <$inf> };
  17         90  
  17         2934  
23 17         249 close $inf;
24 17         125 my ( $data, $error ) = from_toml( $body );
25 17 50       64341 if ( $error ) {
26 0         0 $self->log_error( 'Invalid blocker toml file - ' . $error );
27             }
28             else {
29 17         86 $blocker_config = $data;
30             }
31             }
32             else {
33 0         0 open ( my $outf, '>', $filename ); ## no critic
34 0         0 print $outf qq(
35             # Authentication Milter Blocker quick config
36             #
37             # id for metrics and must be unique
38             # callbacks are connect,helo,envfrom,envrcpt,header
39             # value is applied as a regex
40             # percent is a percentage of matches to apply the block to
41             # with is the full SMTP reject string to send, 4xx or 5xx and MUST have an extended code 5.x.x or 4.x.x
42             # until (optional) is a unixtime after which the block will expire
43             #
44             # Example
45             #
46             # [flood]
47             # callback = "connect"
48             # value = "192\.168\.0\.1"
49             # with = "451 4.7.28 flood policy violation (HOTtest)"
50             # percent = 100
51             # until = 1573514783
52             #
53             # [rule2]
54             # callback = "connect"
55             # ...
56             );
57 0         0 close $outf;
58             }
59 17         143 return $blocker_config;
60             }
61              
62             sub _load_blocker_config {
63 57     57   113 my ( $self ) = @_;
64 57         180 my $config = $self->handler_config();
65 57 100       165 return $self->{'blocker_config'} if exists $self->{'blocker_config'};
66              
67             my %blocker_config = map {
68 17         52 %{ $self->_load_blocker_config_file( $_ ) }, ## no critic
  17         58  
69 15         29 } ( @{$config->{ 'blocker_configs' } } );
  15         42  
70              
71 15         67 $self->{'blocker_config'} = \%blocker_config;
72 15         40 return \%blocker_config;
73             }
74              
75             sub _test_blocker {
76 57     57   183 my ( $self, $callback, $value ) = @_;
77              
78 57         141 my $blocker_config = $self->_load_blocker_config();
79 57         315 foreach my $key ( sort keys %$blocker_config ) {
80 289         505 my $item = $blocker_config->{$key};
81 289 100       749 next if $item->{'callback'} ne $callback;
82 69 100 100     199 next if $item->{'until'} && $item->{'until'} < time;
83 65         126 my $value_regex = $item->{'value'};
84 65 100       786 if ( $value =~ /$value_regex/ ) {
85 12 100       121 if ( rand(100) > $item->{'percent'} ) {
    100          
    50          
86 2         46 $self->dbgout( 'Blocker', 'sampled_out ' . $key, LOG_INFO );
87 2         17 $self->metric_count( 'blocker_total', { 'result' => 'sampled_out', 'id' => $key } );
88             }
89             elsif ( $item->{'with'} =~ /^5/ ) {
90 1         9 $self->dbgout( 'Blocker', 'reject ' . $key, LOG_INFO );
91 1         19 $self->metric_count( 'blocker_total', { 'result' => 'reject', 'id' => $key } );
92 1         72 $self->reject_mail( $item->{'with'} );
93             }
94             elsif ( $item->{'with'} =~ /^4/ ) {
95 9         63 $self->dbgout( 'Blocker', 'defer ' . $key, LOG_INFO );
96 9         62 $self->metric_count( 'blocker_total', { 'result' => 'defer', 'id' => $key } );
97 9         66 $self->defer_mail( $item->{'with'} );
98             }
99             else {
100 0         0 $self->log_error( 'Invalid blocker entry with ' . $item->{'with'} );
101             }
102             }
103             }
104             }
105              
106             sub default_config {
107             return {
108 1     1 0 1694 'blocker_configs' => [ '/tmpfs/authmilter-blocker.toml' ],
109             };
110             }
111              
112             sub connect_callback {
113 15     15 0 42 my ( $self, $hostname, $ip ) = @_;
114 15         60 $self->_test_blocker( 'connect', $ip->ip );
115             }
116              
117             sub helo_callback {
118 9     9 0 27 my ( $self, $helo_host ) = @_;
119 9         31 $self->_test_blocker( 'helo', $helo_host );
120             }
121              
122             sub envfrom_callback {
123 8     8 0 22 my ( $self, $env_from ) = @_;
124 8         36 $self->_test_blocker( 'envfrom', $env_from );
125             }
126              
127             sub envrcpt_callback {
128 7     7 0 17 my ( $self, $env_to ) = @_;
129 7         20 $self->_test_blocker( 'envrcpt', $env_to );
130             }
131              
132             sub header_callback {
133 18     18 0 49 my ( $self, $header, $value ) = @_;
134 18         65 $self->_test_blocker( 'header', "$header: $value" );
135             }
136              
137             sub close_callback {
138 0     0 0   my ($self) = @_;
139 0           delete $self->{'blocker_config'};
140             }
141              
142             1;
143              
144             __END__
145              
146             =pod
147              
148             =encoding UTF-8
149              
150             =head1 NAME
151              
152             Mail::Milter::Authentication::Handler::Blocker - Block mail based on simple rules
153              
154             =head1 VERSION
155              
156             version 3.20230629
157              
158             =head1 DESCRIPTION
159              
160             Defer/Reject mail based on simple rules.
161              
162             =head1 CONFIGURATION
163              
164             "Blocker" : { |
165             'blocker_configs' => [ '/tmpfs/authmilter-blocker.toml' ], | A list of blocker configs to test against.
166             } |
167              
168             =head1 AUTHOR
169              
170             Marc Bradshaw <marc@marcbradshaw.net>
171              
172             =head1 COPYRIGHT AND LICENSE
173              
174             This software is copyright (c) 2020 by Marc Bradshaw.
175              
176             This is free software; you can redistribute it and/or modify it under
177             the same terms as the Perl 5 programming language system itself.
178              
179             =cut