root/bin/pl-janitor

Revision 326:6fe792e6c8f1, 9.6 KB (checked in by Anselm Lingnau <anselm@…>, 9 months ago)

Big get_config() cleanup to get rid of hard_coded defaults.
We're now getting parameter defaults from a central list instead of putting
them near the get_config() calls themselves, to avoid possible inconsistencies.
If for whatever reasons parameters end up with obviously invalid values, we
still try to do the Right Thing(TM).

  • Property exe set to *
Line 
1#!/usr/bin/perl -w
2#
3# pl-janitor --
4#
5#    Do periodic maintenance of Project Lancelot lists
6#
7# Copyright (C) 2006 Anselm Lingnau <anselm@anselms.net>.
8#
9# See the end of the in-file documentation for licensing details.
10
11=head1 NAME
12
13pl-janitor -- do periodic maintenance of Project Lancelot lists
14
15=head1 SYNOPSIS
16
17pl-janitor [-dv?] list@domain|DIRECTORY ...
18
19=head1 DESCRIPTION
20
21The B<pl-janitor> program is supposed to be run by B<cron> and does
22most of the work of automatic bounce processing. It checks whether a
23subscribing address has had excessive bounces and blocks it from receiving
24further messages if this is the case. It also takes care of sending out
25appropriate notifications to the address in question and finally unsubscribes
26it from the list if enough time has elapsed without a reaction.
27
28=head1 SEE ALSO
29
30pl-bouncedb(1), pl-incoming(1)
31
32=head1 AUTHOR
33
34Anselm Lingnau <anselm@anselms.net>
35
36=head1 COPYRIGHT AND LICENSE
37
38Copyright 2006 by Anselm Lingnau. This program is free software; you
39may redistribute it and/or modify it under the terms of the GNU
40General Public License as published by the Free Software Foundation;
41either version 2 of the License, or (at your option) any later version.
42
43This program is distributed in the hope that it will be useful, but
44WITHOUT ANY WARRANTY; without even the implied warranty of
45MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
46General Public License for more details.
47
48You should have received a copy of the GNU General Public License
49along with this program; if not, refer to
50<URL:http://www.gnu.org/copyleft/gpl.html>. You may also obtain it by
51writing to the Free Software Foundation, Inc., 59 Temple Place - Suite
52330, Boston, MA 02111-1307, USA.
53
54=cut
55
56use strict;
57
58use Config;
59use File::Basename;
60use Time::Piece;
61
62use vars qw($g_base_dir);
63BEGIN {
64    use FindBin qw($Bin);
65    $g_base_dir = dirname($Bin);
66}
67use lib "$g_base_dir/lib";
68use lib "$g_base_dir/lib/perl5/$Config{version}";
69use lib "$g_base_dir/lib/perl5/site_perl/$Config{version}";
70
71use Lancelot::GetOpt qw/GetOptions/;
72use Pod::Usage;
73
74use Lancelot::DB;
75use Lancelot::Log qw/log log_and_die/;
76use Lancelot::Send;
77use Lancelot::Template;
78
79use Lancelot::Module::process_bounce;
80use Lancelot::Module::post_digest;
81
82sub send_bounce_message {
83    my ($db, $listaddr, $template, $address, $keys) = @_;
84    my ($fromlocal, $fromdomain) = split /\@/, $listaddr, 2;
85    my ($rlocal, $rdomain) = split /\@/, $address, 2;
86    my ($delim) = $db->get_config("mail.delimiter");
87    log_and_die "crit", "mail.delimiter not set" unless $delim;
88    my (%keys) = (%$keys, "verp-recipient" => $address,
89                  "verp-from" => "$fromlocal${delim}bounce-notify-$rlocal=$rdomain\@$fromdomain",
90                  "reenable-address" => "$fromlocal${delim}bounce-reenable-$rlocal=$rdomain\@$fromdomain");
91    log "debug", "send bounce warning to $address";
92    Lancelot::Send::sendtemplate($template, $db, undef, \%keys);
93    # If this is the bounce-unsub message the log entry will be deleted at once
94    $db->log_bounce_warning($address);
95}
96
97register Lancelot::Template "bounce-message", "en", <<END;
98From: <<list.name>> Management <<<owner-address>>>
99To: <<verp-recipient>>
100Subject: Bounce notification for <<list.name>>
101
102I\'m sorry to have to tell you that several messages to the
103
104  <<verp-recipient>>
105
106address, which subscribes to the <<list.name>> list, have
107been bounced back to the list management software. The address in
108question has been disabled temporarily and will not be receiving
109further messages from the list for the time being.
110
111To re-enable the address for message reception, send a (possibly
112empty) message to
113
114  <<reenable-address>>
115
116within the next <<interval>> days. If the address is not re-enabled
117during this period, it will be unsubscribed permanently, and you
118will have to resubscribe to the list to receive messages again.
119
120If you have any queries, please feel free to contact <<list.ownername|the list owner>>
121at <<owner-address>>.
122
123The <<list.name>> Management Team
124
125END
126
127register Lancelot::Template "bounce-unsub-message", "en", <<END;
128From: <<list.name>> Management <<<owner-address>>>
129To: <<verp-recipient>>
130Subject: Unsubscription notification for <<list.name>>
131
132I\'m sorry to have to tell you that the
133
134  <<verp-recipient>>
135
136address, which used to subscribe to the <<list.name>> list, has
137been automatically unsubscribed from the list because it caused an excessive
138number of bounces (message transmission errors).
139
140If this has happened in error, please accept our apologies for the
141inconvenience. You may resubscribe immediately by sending an empty message
142to
143
144  <<sub-address>>
145
146Also feel free to contact <<list.ownername|the list owner>> at
147<<owner-address>> to vent your anger :^| In fact, we
148hope that you will do so, so we can figure out if anything went wrong at
149our end.
150
151The <<list.name>> Management Team
152
153END
154
155my (%opts) = ( notreally => 0, bounces => 1, digest => 1 );
156
157GetOptions(\%opts, -argvmin => 1, "no list address or directory specified",
158                   "notreally|n?", "bounces|b?", "digest|d?",
159                   "threshold:");
160
161# Replace directory name arguments by the names of Project Lancelot
162# lists immediately below them. This contains some special support for
163# Our-ISP, where lists are in directories called $HOME/domains/DOMAIN/LIST;
164# you need to use $HOME/domains/DOMAIN as an argument to pl-janitor in this
165# case.
166
167log "info", "starting run at " . scalar(localtime);
168
169my (@lists) = ();
170foreach my $arg (@ARGV) {
171    if (-d $arg) {
172        log "debug", "arg=$arg " . join ",", glob "$arg/*\@*";
173        foreach (grep { -f File::Spec->join($_, "list.db") } glob "$arg/*") {
174            my $list = basename $_;
175            $list .= '@' . basename dirname $_ if $list !~ /\@/;
176            push @lists, $list;
177        }
178    } else {
179        push @lists, $arg;
180    }
181}
182log "debug", "lists=@lists";
183
184# Go through the lists and do your thing
185
186foreach my $listaddr (@lists) {
187    my $db = new Lancelot::DB $listaddr, { nodefault => 1 };
188
189    if (!defined $db) {
190        log "err", "list $listaddr cannot be found";
191        warn "$0: list $listaddr cannot be found\n";
192        next;
193    }
194
195    process_bounces($listaddr, $db) if $opts{bounces};
196    process_digest($listaddr, $db) if $opts{digest};
197
198    # TODO: Weed out overdue confirmations
199}
200
201sub process_bounces {
202    my ($listaddr, $db) = @_;
203
204    log "debug", "processing bounces for $listaddr";
205
206    # Get bounce parameters for this list
207
208    my $bkt_count = $db->get_config("bounce.bucketcount");
209    my $bkt_size = $db->get_config("bounce.bucketsize");
210    my $b_threshold = $opts{'threshold'} || $db->get_config("bounce.threshold");
211    my $b_warnings = $db->get_config("bounce.warnings");
212    my $b_wperiod = $db->get_config("bounce.warnperiod");
213    log_and_die "crit", "bounce hander parameter(s) missing from configuration"
214        unless $bkt_count && $bkt_size && $b_threshold
215            && $b_warnings && $b_wperiod;
216
217    # Check whether any new addresses need to be marked BOUNCING
218
219    my $cutoff = time - $bkt_count * $bkt_size;
220    foreach my $address ($db->get_bounce_addresses("*", $cutoff)) {
221        my $addr_opts = $db->get_address_options($address);
222        my $t = Lancelot::Module::process_bounce::total_score($db, $address,
223                                                              $bkt_count,
224                                                              $bkt_size);
225        log "debug", "total score for $address is $t (vs. $b_threshold)";
226        if ($t > $b_threshold && $addr_opts->{status} eq "SUBSCRIBED") {
227            # Address is officially bouncing for the first time
228            log "info", "mark $address as BOUNCING";
229            unless ($opts{notreally}) {
230                $db->set_address_options($address, { status => "BOUNCING" });
231                send_bounce_message($db, $listaddr, "bounce-message", $address,
232                                    {interval => $b_warnings*$b_wperiod/86400});
233            }
234        }
235    }
236
237    # Check whether any BOUNCING addresses need unsubscribed or a new reminder
238
239    foreach my $address ($db->get_addresses("*", { bouncing => 1 })) {
240        log "debug", "check bouncing address $address";
241        my ($count, $last) = $db->last_bounce_warning($address);
242        log "debug","count=$count last=$last (".localtime($last)->datetime.")";
243        next if $last > time - $b_wperiod;
244        if ($count < $b_warnings) {
245            # Send another reminder
246            log "info", sprintf "send a bounce reminder to $address (%d left)",
247                                $b_warnings - $count;
248            send_bounce_message($db, $listaddr, "bounce-message", $address,
249                                {interval => ($b_warnings-$count)*$b_wperiod/86400})
250                unless $opts{notreally};
251        } else {
252            # That's it -- they will be unsubscribed
253            log "info", "unsubscribe $address due to excessive bounces";
254            unless ($opts{notreally}) {
255                send_bounce_message($db, $listaddr, "bounce-unsub-message",
256                                    $address, {});
257                $db->set_address_options($address, { status => "DELETE" });
258                $db->clear_bounce_warnings($address);
259                $db->clear_bounces($address);
260            }
261        }
262    }
263}
264
265sub process_digest {
266    my ($listaddr, $db) = @_;
267
268    # See whether we ought to send out a digest based on the time of the
269    # last digest and the maximum delay.
270
271    my $digest_period = $db->get_config("digest.maxperiod");
272    if ($digest_period =~ s/([hdw])$//) {
273        my $unit = $1;
274        my %factor = ( h => 3600, d => 24*3600, w => 7*24*3600 );
275        $digest_period *= $factor{$unit};
276    }
277    my $digest_last = $db->get_parameter("digest_last");
278    my $now = localtime;
279    log "debug", "time=" . $now->epoch . " digest_last=$digest_last digest_period=$digest_period";
280
281    if ($now->epoch - $digest_last > $digest_period) {
282        my $from = $db->get_parameter("digest_first" || 1);
283        my $to = $db->get_parameter("message") - 1;
284
285        if ($from <= $to) {
286            log "debug", "sending out overdue digest (messages $from-$to)";     
287            unless ($opts{notreally}) {
288                $db->set_parameter("digest_first", $to + 1);
289                Lancelot::Module::post_digest::send_digest($db, $from, $to);
290            }
291        } else {
292            log "debug" , "digest would be overdue but nothing was posted";
293        }
294    }
295}
Note: See TracBrowser for help on using the browser.