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   1817 use 5.20.0;
  2         9  
3 2     2   18 use strict;
  2         7  
  2         46  
4 2     2   11 use warnings;
  2         9  
  2         92  
5 2     2   34 use Mail::Milter::Authentication::Pragmas;
  2         5  
  2         20  
6             # ABSTRACT: Block mail based on simple rules
7             our $VERSION = '3.20230911'; # VERSION
8 2     2   483 use base 'Mail::Milter::Authentication::Handler';
  2         5  
  2         176  
9 2     2   16 use TOML;
  2         6  
  2         2401  
10              
11             sub register_metrics {
12             return {
13 4     4 1 27 'blocker_total' => 'The number of emails blocked by blocker',
14             };
15             }
16              
17             sub _load_blocker_config_file {
18 17     17   52 my ( $self, $filename ) = @_;
19 17         35 my $blocker_config = {};
20 17 50       465 if ( -e $filename ) {
21 17         753 open ( my $inf, '<', $filename );
22 17         55 my $body = do { local $/; <$inf> };
  17         97  
  17         3595  
23 17         298 close $inf;
24 17         121 my ( $data, $error ) = from_toml( $body );
25 17 50       66419 if ( $error ) {
26 0         0 $self->log_error( 'Invalid blocker toml file - ' . $error );
27             }
28             else {
29 17         92 $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         150 return $blocker_config;
60             }
61              
62             sub _load_blocker_config {
63 57     57   102 my ( $self ) = @_;
64 57         167 my $config = $self->handler_config();
65 57 100       163 return $self->{'blocker_config'} if exists $self->{'blocker_config'};
66              
67             my %blocker_config = map {
68 17         36 %{ $self->_load_blocker_config_file( $_ ) }, ## no critic
  17         43  
69 15         35 } ( @{$config->{ 'blocker_configs' } } );
  15         44  
70              
71 15         54 $self->{'blocker_config'} = \%blocker_config;
72 15         38 return \%blocker_config;
73             }
74              
75             sub _test_blocker {
76 57     57   193 my ( $self, $callback, $value ) = @_;
77              
78 57         137 my $blocker_config = $self->_load_blocker_config();
79 57         323 foreach my $key ( sort keys %$blocker_config ) {
80 289         507 my $item = $blocker_config->{$key};
81 289 100       769 next if $item->{'callback'} ne $callback;
82 69 100 100     229 next if $item->{'until'} && $item->{'until'} < time;
83 65         114 my $value_regex = $item->{'value'};
84 65 100       877 if ( $value =~ /$value_regex/ ) {
85 12 100       89 if ( rand(100) > $item->{'percent'} ) {
    100          
    50          
86 2         26 $self->dbgout( 'Blocker', 'sampled_out ' . $key, LOG_INFO );
87 2         35 $self->metric_count( 'blocker_total', { 'result' => 'sampled_out', 'id' => $key } );
88             }
89             elsif ( $item->{'with'} =~ /^5/ ) {
90 1         8 $self->dbgout( 'Blocker', 'reject ' . $key, LOG_INFO );
91 1         7 $self->metric_count( 'blocker_total', { 'result' => 'reject', 'id' => $key } );
92 1         16 $self->reject_mail( $item->{'with'} );
93             }
94             elsif ( $item->{'with'} =~ /^4/ ) {
95 9         55 $self->dbgout( 'Blocker', 'defer ' . $key, LOG_INFO );
96 9         68 $self->metric_count( 'blocker_total', { 'result' => 'defer', 'id' => $key } );
97 9         49 $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 2252 'blocker_configs' => [ '/tmpfs/authmilter-blocker.toml' ],
109             };
110             }
111              
112             sub connect_callback {
113 15     15 0 46 my ( $self, $hostname, $ip ) = @_;
114 15         64 $self->_test_blocker( 'connect', $ip->ip );
115             }
116              
117             sub helo_callback {
118 9     9 0 35 my ( $self, $helo_host ) = @_;
119 9         24 $self->_test_blocker( 'helo', $helo_host );
120             }
121              
122             sub envfrom_callback {
123 8     8 0 26 my ( $self, $env_from ) = @_;
124 8         23 $self->_test_blocker( 'envfrom', $env_from );
125             }
126              
127             sub envrcpt_callback {
128 7     7 0 20 my ( $self, $env_to ) = @_;
129 7         19 $self->_test_blocker( 'envrcpt', $env_to );
130             }
131              
132             sub header_callback {
133 18     18 0 47 my ( $self, $header, $value ) = @_;
134 18         62 $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.20230911
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