]>
diplodocus.org Git - minc/blob - minc
d74a5f1a9ea57d1df5d0c91fcde4a218f7b65a64
7 B<minc> - Incorporate mail from a maildir into mh folders.
15 B<minc> is a program for incorporating mail from a maildir to a mh
16 folder hierarchy. It takes mail from a maildir folder (not a maildir
17 folder hierarchy), checks for spam with razor
18 (http://razor.sourceforge.net/), and optionally filters mail into
21 The filtering is quite sophisticated, as it is done using real Perl
22 substitution (s//) commands.
29 require 'sysexits.ph';
35 use Log
::Dispatch
::File
;
36 use POSIX
qw(strftime WEXITSTATUS WIFEXITED);
44 Dump (using Data::Dumper) the FILTERS hash and exit. This is useful
45 for testing the syntax of .mincfilter.
53 Dry run; do not actually incorporate the mail, but log and report to
54 stdout/stderr as normal.
58 Process SPAM only, then exit, leaving all non-spam messages in the
72 our $opt_s; # ;; # stupid cperl-mode
74 if (not getopts
('dhns')) {
81 print("Sorry bub, no help.\n");
87 if ($opt_s) { # ))){ # stupid cperl-mode
97 Where configuration files (.mincfilter) are found. Also,
98 $HOME/Maildir is used for the maildir if MAILDIR is not set.
102 Where mail is delivered.
108 use Env
qw(HOME MAILDIR);
111 die("HOME environment variable must be set.\n");
114 $MAILDIR = "$HOME/Maildir";
121 =item $HOME/.mincfilter
123 This file is Perl code (included via the 'require' directive) which is
124 expected to define the FILTERS hash.
126 =item `mhpath +`/logs/minc.log
128 Where minc logs what it does, unless in -n mode.
130 =item `mhpath +`/logs/dryrun.log
132 Where minc logs what it would do; used in -n mode.
137 require "$HOME/.mincfilter";
146 $logfile = $mh . '/logs/minc.log';
148 $logfile = $mh . '/logs/dryrun.log';
152 ###############################################################################
155 # debuglevels for the logger
156 use constant LOG_DEBUGLEVEL
=> 'info';
157 use constant SCREEN_DEBUGLEVEL
=> 'debug';
159 # characters used to wrap around the id field in the log so I can more
160 # easily parse the msg flow
161 use constant DEBUGCHAR
=> '%';
162 use constant INFOCHAR
=> '=';
163 use constant INCOMINGCHAR
=> '<';
164 use constant SAVECHAR
=> '>';
165 use constant WARNCHAR
=> 'W';
166 use constant ERRORCHAR
=> 'E';
168 my $logger = new Log
::Dispatch
;
169 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
174 # log alias, handles getting passed vars that are undef
178 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
180 foreach my $part (@_) {
181 if (defined($part)) {
185 # no newlines in the log message, thanks
187 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
188 $logger->log(level
=>$level, message
=>$msg);
190 if ($act eq SAVECHAR
) {
191 $logger->log(level
=>$level, message
=>"\n");
195 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
196 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
197 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
198 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
199 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
200 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
205 logincoming
('From: ', $headers{'return-path'});
206 logincoming
('To: ', $headers{'to'});
207 logincoming
('Subject: ', $headers{'subject'});
208 logincoming
('Message-Id: ', $headers{'message-id'});
212 ###############################################################################
216 printf('%s:%s', __FILE__
, __LINE__
);
245 my $mhfolder = shift;
250 $folder = $mh . '/' . $mhfolder;
253 if (not -d
$folder) {
254 foreach $component (split('/', $folder)) {
255 if (defined($component) and length($component) >= 1) {
256 $target = $target . '/' . $component;
257 if (-d
$target or mkdir($target)) {
261 "Failed to create +$mhfolder ($component)");
268 # We want to print the name of each list that has new mail only once,
269 # so use this hash to store the lists that have already been printed.
270 # Start the list out with SPAM already in it, since we don't care when
271 # new messages are added to it.
272 my %FOLDERS = ('SPAM'=>1);
276 my $mhfolder = shift;
281 # We must do this even in -n mode because later steps fail without
282 # it. This should be harmless.
285 # XXX: Grab the 'mhpath new' algorithm and implement it
286 # internally. Furthermore, use the maildir delivery algorithm
287 # (though slightly different: get filename, if exists increment
288 # and start over, create file, if fail bomb) to make sure we
289 # really do get a new message file.
290 $mhmsg = `mhpath +$mhfolder new`;
292 $msgnum = basename
($mhmsg);
294 logsave
("+$mhfolder");
297 if (not rename($msg, $mhmsg)) {
298 err
(&EX_TEMPFAIL
, "Failed rename($msg, $mhmsg)");
301 # XXX: Lame! Instead, keep a hash of arrays. Keys are folder
302 # names, array elements are message numbers in that list.
303 # Then after all the messages have been sorted, run one mark
304 # command per folder, marking all messages for that folder in
306 if ($mhfolder ne 'SPAM') {
307 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
308 if (not (WIFEXITED
($status) and WEXITSTATUS
($status) == 0)) {
309 errx
(&EX_TEMPFAIL
, "Failed to mark message unseen");
314 if (not $FOLDERS{$mhfolder}) {
315 print("+$mhfolder\n");
316 $FOLDERS{$mhfolder} = 1;
323 my $current; # current header, used for unfolding lines
324 my $fieldname; # unmolested header name
334 # folded header continuation
336 if (not defined($current)) {
337 print("Malformed message, cannot parse headers.\n");
341 $headers{$current} .= $_;
343 ($fieldname) = split(/:/);
344 $current = lc($fieldname);
345 (undef, $headers{$current}) = split(/^$fieldname:\s*/);
357 if (not opendir(DIR
, $dir)) {
358 err
(&EX_TEMPFAIL
, "Failed opendir($dir)");
361 # Initialize $! to 0 (success) because stupid stupid Perl provides
362 # no way to distinguish an error from an empty directory; that
363 # means setting $! to 0 and testing it afterwards is the only way
364 # to detect an error. Real Programmers don't handle errors,
368 ($_ ne '.' and $_ ne '..')
369 and $_ = "$MAILDIR/new/$_"
373 err
(&EX_TEMPFAIL
, "Failed readdir($dir)");
382 ###############################################################################
392 $err =~ s
|/new/|/err/|;
393 $sig =~ s
|/new/|/sig/|;
403 ($message, $!) = split(/:/, $line);
405 logerr
("$err: $message: $!");
408 if (unlink($err) != 1) {
409 err
(&EX_TEMPFAIL
, "Failed unlink($err)");
413 # This is supposed to be a signature created with razor-check
414 # directly after delivery. Currently this isn't supported
415 # because it isn't clear to me how to get that signature back
416 # into razor-check. For now, just unlink any sig files we
417 # find and proceed with full razor-check mode.
420 if (unlink($sig) != 1) {
421 err
(&EX_TEMPFAIL
, "Failed unlink($sig)");
427 $status = system("razor-check < $msg");
428 if (not WIFEXITED
($status)) {
429 err
(&EX_TEMPFAIL
, "Failed to run razor-check < $msg");
430 } elsif (WEXITSTATUS
($status) == 0) {
449 foreach $msg (@msglist) {
450 printf('%sChecking for spam... %6d/%d',
451 "\r", ++$i, scalar(@msglist));
454 store_message
($msg, 'SPAM');
459 print("\nDone: ", scalar(@result), " survivors\n");
465 ###############################################################################
477 %headers = get_headers
($msg);
482 log_headers
(%headers);
484 foreach $header (keys(%FILTERS)) {
485 $contents = $headers{lc($header)};
487 if (defined($contents)) {
488 foreach $regex (keys(%{$FILTERS{$header}})) {
489 $subst = $FILTERS{$header}->{$regex};
491 $try = '$contents =~';
492 $try .= " s|$regex|$subst|is";
508 foreach $msg (@msglist) {
509 $mhfolder = find_mh_folder
($msg);
510 store_message
($msg, $mhfolder);
519 $Data::Dumper
::Indent
= 1;
520 print(Dumper
(\
%FILTERS));
524 @msglist = kill_spam
(getfiles
("$MAILDIR/new"));
527 filter_mail
(@msglist);
537 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
538 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
539 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
542 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
547 The first List-Id filter is a surprisingly effective one which creates
548 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
549 all Sourceforge lists to which i subscribe. Certainly there are lists
550 hosted at Sourceforge which do not work well with this, and this is
551 where it is important to remember that the first match is the one
552 uses. Simply put the more specific rules before this one.
554 The next List-Id example is simple. The swig example demonstrates
555 that the folder name does not have to use a portion of the matched
556 header; you can just hardcode the folder name.
558 The List-Post example is a nice one. Most ASF projects have their own
559 apache.org subdomain, with mailing lists hosted there. So, given a
560 list such as dev@httpd.apache.org, this filter will create the folder
561 name l/apache/httpd/dev.
565 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
566 stolen from Adam Lazur <adam@lazur.org>.
568 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.