File Coverage

blib/lib/Mail/Milter/Authentication/Handler/Blocker.pm
Criterion Covered Total %
statement 71 79 89.8
branch 15 18 83.3
condition 3 3 100.0
subroutine 15 16 93.7
pod 1 8 12.5
total 105 124 84.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::Blocker;
2 1     1   74694 use strict;
  1         3  
  1         34  
3 1     1   5 use warnings;
  1         3  
  1         29  
4 1     1   6 use base 'Mail::Milter::Authentication::Handler';
  1         9  
  1         125  
5             our $VERSION = '2.20191120'; # VERSION
6             # ABSTRACT: Block mail based on simple rules
7              
8 1     1   8 use Sys::Syslog qw{:standard :macros};
  1         2  
  1         396  
9 1     1   9 use TOML;
  1         7  
  1         1005  
10              
11             sub register_metrics {
12             return {
13 4     4 1 23660 'blocker_total' => 'The number of emails blocked by blocker',
14             };
15             }
16              
17             sub _load_blocker_config_file {
18 17     17   42 my ( $self, $filename ) = @_;
19 17         34 my $blocker_config = {};
20 17 50       401 if ( -e $filename ) {
21 17         695 open ( my $inf, '<', $filename );
22 17         52 my $body = do { local $/; <$inf> };
  17         87  
  17         707  
23 17         184 close $inf;
24 17         93 my ( $data, $error ) = from_toml( $body );
25 17 50       63638 if ( $error ) {
26 0         0 $self->log_error( 'Invalid blocker toml file - ' . $error );
27             }
28             else {
29 17         90 $blocker_config = $data;
30             }
31             }
32             else {
33 0         0 open ( my $outf, '>', $filename );
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         144 return $blocker_config;
60             }
61              
62             sub _load_blocker_config {
63 57     57   117 my ( $self ) = @_;
64 57         168 my $config = $self->handler_config();
65 57 100       2290 return $self->{'blocker_config'} if exists $self->{'blocker_config'};
66              
67             my %blocker_config = map {
68 17         32 %{ $self->_load_blocker_config_file( $_ ) },
  17         55  
69 15         34 } ( @{$config->{ 'blocker_configs' } } );
  15         39  
70              
71 15         60 $self->{'blocker_config'} = \%blocker_config;
72 15         43 return \%blocker_config;
73             }
74              
75             sub _test_blocker {
76 57     57   189 my ( $self, $callback, $value ) = @_;
77              
78 57         147 my $blocker_config = $self->_load_blocker_config();
79 57         306 foreach my $key ( sort keys %$blocker_config ) {
80 289         839 my $item = $blocker_config->{$key};
81 289 100       730 next if $item->{'callback'} ne $callback;
82 69 100 100     208 next if $item->{'until'} && $item->{'until'} < time;
83 65         125 my $value_regex = $item->{'value'};
84 65 100       781 if ( $value =~ /$value_regex/ ) {
85 12 100       98 if ( rand(100) > $item->{'percent'} ) {
    100          
    50          
86 2         42 $self->dbgout( 'Blocker', 'sampled_out ' . $key, LOG_INFO );
87 2         278 $self->metric_count( 'blocker_total', { 'result' => 'sampled_out', 'id' => $key } );
88             }
89             elsif ( $item->{'with'} =~ /^5/ ) {
90 1         11 $self->dbgout( 'Blocker', 'reject ' . $key, LOG_INFO );
91 1         137 $self->metric_count( 'blocker_total', { 'result' => 'reject', 'id' => $key } );
92 1         34 $self->reject_mail( $item->{'with'} );
93             }
94             elsif ( $item->{'with'} =~ /^4/ ) {
95 9         66 $self->dbgout( 'Blocker', 'defer ' . $key, LOG_INFO );
96 9         1172 $self->metric_count( 'blocker_total', { 'result' => 'defer', 'id' => $key } );
97 9         194 $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 57         184 return;
106             }
107              
108             sub default_config {
109             return {
110 1     1 0 8072 'blocker_configs' => [ '/tmpfs/authmilter-blocker.toml' ],
111             };
112             }
113              
114             sub connect_callback {
115 15     15 0 232554 my ( $self, $hostname, $ip ) = @_;
116 15         66 $self->_test_blocker( 'connect', $ip->ip );
117 15         50 return;
118             }
119              
120             sub helo_callback {
121 9     9 0 8228 my ( $self, $helo_host ) = @_;
122 9         36 $self->_test_blocker( 'helo', $helo_host );
123 9         26 return;
124             }
125              
126             sub envfrom_callback {
127 8     8 0 7094 my ( $self, $env_from ) = @_;
128 8         33 $self->_test_blocker( 'envfrom', $env_from );
129 8         27 return;
130             }
131              
132             sub envrcpt_callback {
133 7     7 0 6213 my ( $self, $env_to ) = @_;
134 7         30 $self->_test_blocker( 'envrcpt', $env_to );
135 7         23 return;
136             }
137              
138             sub header_callback {
139 18     18 0 16257 my ( $self, $header, $value ) = @_;
140 18         80 $self->_test_blocker( 'header', "$header: $value" );
141 18         52 return;
142             }
143              
144             sub close_callback {
145 0     0 0   my ($self) = @_;
146 0           delete $self->{'blocker_config'};
147 0           return;
148             }
149              
150             1;
151              
152             __END__
153              
154             =pod
155              
156             =encoding UTF-8
157              
158             =head1 NAME
159              
160             Mail::Milter::Authentication::Handler::Blocker - Block mail based on simple rules
161              
162             =head1 VERSION
163              
164             version 2.20191120
165              
166             =head1 DESCRIPTION
167              
168             Defer/Reject mail based on simple rules.
169              
170             =head1 CONFIGURATION
171              
172             "Blocker" : { |
173             'blocker_configs' => [ '/tmpfs/authmilter-blocker.toml' ], | A list of blocker configs to test against.
174             } |
175              
176             =head1 AUTHOR
177              
178             Marc Bradshaw <marc@marcbradshaw.net>
179              
180             =head1 COPYRIGHT AND LICENSE
181              
182             This software is copyright (c) 2019 by Marc Bradshaw.
183              
184             This is free software; you can redistribute it and/or modify it under
185             the same terms as the Perl 5 programming language system itself.
186              
187             =cut