File Coverage

blib/lib/App/Milter/Limit.pm
Criterion Covered Total %
statement 79 123 64.2
branch 7 30 23.3
condition 4 14 28.5
subroutine 18 23 78.2
pod 3 4 75.0
total 111 194 57.2


line stmt bran cond sub pod time code
1             #
2             # This file is part of App-Milter-Limit
3             #
4             # This software is copyright (c) 2010 by Michael Schout.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9              
10             package App::Milter::Limit;
11             $App::Milter::Limit::VERSION = '0.54';
12             # ABSTRACT: Sendmail Milter that limits message rate by sender
13              
14 5     5   1835443 use strict;
  5         11  
  5         199  
15 5     5   23 use warnings;
  5         7  
  5         340  
16              
17 5     5   28 use base qw(Class::Accessor Class::Singleton);
  5         11  
  5         2973  
18              
19 5     5   14483 use Carp;
  5         15  
  5         362  
20 5     5   2695 use App::Milter::Limit::Config;
  5         19  
  5         27  
21 5     5   3033 use App::Milter::Limit::Log;
  5         31  
  5         461  
22 5     5   2995 use App::Milter::Limit::Util;
  5         19  
  5         203  
23 5     5   3101 use Sendmail::PMilter 0.98 ':all';
  5         58058  
  5         1573  
24 5     5   38 use Sys::Syslog ();
  5         11  
  5         6809  
25              
26             __PACKAGE__->mk_accessors(qw(driver milter));
27              
28              
29             sub _new_instance {
30 4     4   2642 my ($class, $driver) = @_;
31              
32 4 50       22 croak "usage: new(driver)" unless defined $driver;
33              
34 4         26 my $self = $class->SUPER::_new_instance();
35              
36 4         50 $self->init($driver);
37              
38 4         170 return $self;
39             }
40              
41             sub init {
42 4     4 0 12 my ($self, $driver) = @_;
43              
44 4         26 $self->_init_log;
45              
46 4         20 $self->_init_statedir;
47              
48 4         48 $self->milter(new Sendmail::PMilter);
49              
50 4         124 $self->_init_driver($driver);
51             }
52              
53             # initialize logging
54             sub _init_log {
55 4     4   10 my $self = shift;
56              
57 4         52 my $conf = $self->config->section('log');
58 4   50     104 $$conf{identity} ||= 'milter-limit';
59 4   50     24 $$conf{facility} ||= 'mail';
60              
61 4         28 Sys::Syslog::openlog($$conf{identity}, $$conf{options}, $$conf{facility});
62 4         102 info("syslog initialized");
63              
64             $SIG{__WARN__} = sub {
65 0     0   0 Sys::Syslog::syslog('warning', "warning: ".join('', @_));
66 4         1500 };
67              
68             $SIG{__DIE__} = sub {
69 0     0   0 Sys::Syslog::syslog('crit', "fatal: ".join('',@_));
70 0         0 die @_;
71 4         86 };
72             }
73              
74             # initialize the configured state dir.
75             # default: /var/run/milter-limit
76             sub _init_statedir {
77 4     4   10 my $self = shift;
78              
79 4         30 my $conf = $self->config->global;
80              
81 4         80 App::Milter::Limit::Util::make_path($$conf{state_dir});
82             }
83              
84             sub _init_driver {
85 4     4   14 my ($self, $driver) = @_;
86              
87 4         10 my $driver_class = "App::Milter::Limit::Plugin::$driver";
88              
89 4         476 eval "require $driver_class";
90 4 50       72 if ($@) {
91 0         0 die "failed to load $driver_class: $@\n";
92             }
93 4         32 debug("loaded driver $driver");
94              
95 4         888 $self->driver($driver_class->instance);
96             }
97              
98              
99             sub register {
100 2     2 1 8305 my $self = shift;
101              
102 2         115 my $milter = $self->milter;
103              
104 2         409 my $conf = $self->config->global;
105              
106 2 50       176 if ($$conf{connection}) {
107 2         97 $milter->setconn($$conf{connection});
108             }
109             else {
110             # figure out the connection from sendmail
111 0         0 my $path = $milter->auto_getconn($$conf{name});
112 0 0       0 $milter->setconn($path)
113             or croak "auto_setconn failed";
114              
115             # get the socket's file name without local: or unix:
116 0         0 $path = substr($path,index($path, ':')+1);
117              
118             # make sure the permissions are correct
119 0 0       0 chown $$conf{user}, $$conf{group}, $path
120             or die "chown($path): $!";
121             }
122              
123 2         3563 my %callbacks = (
124             envfrom => \&_envfrom_callback
125             );
126              
127 2         92 $milter->register($$conf{name}, \%callbacks, SMFI_CURR_ACTS);
128              
129 2         542 debug("registered as $$conf{name}");
130             }
131              
132             # drop user/group privs.
133             sub _drop_privileges {
134 2     2   42 my $self = shift;
135              
136 2         7 my $conf = $self->config->global;
137              
138 2 50       59 if (defined $$conf{group}) {
139 0         0 ($(,$)) = ($$conf{group}, $$conf{group});
140             }
141              
142 2 50       48 if (defined $$conf{user}) {
143 0         0 ($<,$>) = ($$conf{user}, $$conf{user});
144             }
145             }
146              
147              
148             sub main {
149 2     2 1 967 my $self = shift;
150              
151 2         20 $self->_drop_privileges;
152              
153 2         8 my $milter = $self->milter;
154              
155 2         24 my $conf = $self->config->global;
156              
157             my %dispatch_args = (
158             max_children => $$conf{max_children} || 5,
159 2   50     128 max_requests_per_child => $$conf{max_requests_per_child} || 100
      50        
160             );
161              
162 2         28 my $driver = $self->driver;
163              
164             # add child_init hook if necessary
165 2 50       138 if ($driver->can('child_init')) {
166 0         0 debug("child_init hook registered");
167 0     0   0 $dispatch_args{child_init} = sub { $driver->child_init };
  0         0  
168             }
169              
170             # add child_exit hook if necessary
171 2 50       47 if ($driver->can('child_exit')) {
172 0         0 debug("child_exit hook registered");
173 0     0   0 $dispatch_args{child_exit} = sub { $driver->child_exit };
  0         0  
174             }
175              
176 2         87 my $dispatcher = Sendmail::PMilter::prefork_dispatcher(%dispatch_args);
177              
178 2         284 $milter->set_dispatcher($dispatcher);
179              
180 2         98 info("starting");
181              
182 2         418 $milter->main;
183             }
184              
185             sub _envfrom_callback {
186 0     0   0 my ($ctx, $from) = @_;
187              
188 0         0 my $self = __PACKAGE__->instance();
189              
190 0         0 my $conf = $self->config->global;
191              
192 0 0       0 if (defined $$conf{limit_from}) {
193 0         0 my $val = $ctx->getsymval($$conf{limit_from});
194 0 0       0 if (defined $val) {
195 0         0 debug("overriding From value with $val");
196 0         0 $from = $val;
197             }
198             }
199              
200             # strip angle brackets
201 0         0 $from =~ s/(?:^\<)|(?:\>$)//g;
202              
203             # do not restrict NULL sender (bounces)
204 0 0       0 unless (length $from) {
205 0         0 return SMFIS_CONTINUE;
206             }
207              
208 0   0     0 my $reply = $$conf{reply} || 'reject';
209 0   0     0 my $message = $$conf{message} || 'Message limit exceeded';
210 0   0     0 my $ignore = $$conf{ignore} || '';
211              
212 0 0       0 if (index(','.$ignore.',', ','.$from.',') != -1) {
213 0         0 info("$from found in ignore list, continuing");
214 0         0 return SMFIS_CONTINUE;
215             }
216              
217 0         0 my $count = $self->driver->query($from);
218 0         0 debug("$from [$count/$$conf{limit}]");
219              
220 0 0       0 if ($count > $$conf{limit}) {
221 0 0       0 if ($reply eq 'defer') {
222 0         0 info("$from exceeded message limit, deferring");
223              
224 0         0 $ctx->setreply(450, '4.7.1', $message);
225              
226 0         0 return SMFIS_TEMPFAIL;
227             }
228             else {
229 0         0 info("$from exceeded message limit, rejecting");
230              
231 0         0 $ctx->setreply(550, '5.7.1', $message);
232              
233 0         0 return SMFIS_REJECT;
234             }
235             }
236             else {
237 0         0 return SMFIS_CONTINUE;
238             }
239             }
240              
241              
242             # shortcut to get the config.
243             sub config {
244 14     14 1 139 App::Milter::Limit::Config->instance;
245             }
246              
247             1;
248              
249             __END__