]>
diplodocus.org Git - minc/blob - minc
fb898348eade5a7ff8a8960679122c9ffa5ca9de
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';
33 use Fcntl
qw(O_WRONLY O_EXCL O_CREAT);
37 use Log
::Dispatch
::File
;
38 use POSIX
qw(strftime WEXITSTATUS WIFEXITED);
46 Dump (using Data::Dumper) the FILTERS hash and exit. This is useful
47 for testing the syntax of .mincfilter.
51 Filter only, then exit. This is useful after running B<minc -s>.
59 Dry run; do not actually incorporate the mail, but log and report to
60 stdout/stderr as normal.
64 Process SPAM only, then exit, leaving all non-spam messages in the
80 our $opt_s; # ;; # stupid cperl-mode
82 if (not getopts
('dhns')) {
89 print("Sorry bub, no help.\n");
97 } elsif ($opt_s) { # ))){ # stupid cperl-mode
107 Where configuration files (.mincfilter) are found. Also,
108 $HOME/Maildir is used for the maildir if MAILDIR is not set.
112 Where mail is delivered.
118 use Env
qw(HOME MAILDIR);
121 die("HOME environment variable must be set.\n");
124 $MAILDIR = "$HOME/Maildir";
131 =item $HOME/.mincfilter
133 This file is Perl code (included via the 'require' directive) which is
134 expected to define the FILTERS hash.
136 =item `mhpath +`/logs/minc.log
138 Where minc logs what it does, unless in -n mode.
140 =item `mhpath +`/logs/dryrun.log
142 Where minc logs what it would do; used in -n mode.
147 require "$HOME/.mincfilter";
156 $logfile = $mh . '/logs/minc.log';
158 $logfile = $mh . '/logs/dryrun.log';
162 ###############################################################################
165 # debuglevels for the logger
166 use constant LOG_DEBUGLEVEL
=> 'info';
167 use constant SCREEN_DEBUGLEVEL
=> 'debug';
169 # characters used to wrap around the id field in the log so I can more
170 # easily parse the msg flow
171 use constant DEBUGCHAR
=> '%';
172 use constant INFOCHAR
=> '=';
173 use constant INCOMINGCHAR
=> '<';
174 use constant SAVECHAR
=> '>';
175 use constant WARNCHAR
=> 'W';
176 use constant ERRORCHAR
=> 'E';
178 my $logger = new Log
::Dispatch
;
179 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
184 # log alias, handles getting passed vars that are undef
188 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
190 foreach my $part (@_) {
191 if (defined($part)) {
195 # no newlines in the log message, thanks
197 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
198 $logger->log(level
=>$level, message
=>$msg);
200 if ($act eq SAVECHAR
) {
201 $logger->log(level
=>$level, message
=>"\n");
205 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
206 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
207 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
208 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
209 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
210 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
215 logincoming
('From: ', $headers{'return-path'});
216 logincoming
('To: ', $headers{'to'});
217 logincoming
('Subject: ', $headers{'subject'});
218 logincoming
('Message-Id: ', $headers{'message-id'});
222 ###############################################################################
226 printf('%s:%s', __FILE__
, __LINE__
);
255 my $mhfolder = shift;
260 $folder = $mh . '/' . $mhfolder;
263 if (not -d
$folder) {
264 foreach $component (split('/', $folder)) {
265 if (defined($component) and length($component) >= 1) {
266 $target = $target . '/' . $component;
267 if (-d
$target or mkdir($target)) {
271 "Failed to create +$mhfolder ($component)");
282 if (not opendir(DIR
, $dir)) {
283 err
(&EX_TEMPFAIL
, "Failed opendir($dir)");
286 # Initialize $! to 0 (success) because stupid stupid Perl provides
287 # no way to distinguish an error from an empty directory; that
288 # means setting $! to 0 and testing it afterwards is the only way
289 # to detect an error. Real Programmers don't handle errors,
293 ($_ ne '.' and $_ ne '..')
294 and $_ = "$MAILDIR/new/$_"
298 err
(&EX_TEMPFAIL
, "Failed readdir($dir)");
306 sub get_highest_msgnum
{
307 my $mhfolder = shift;
313 $dir = "$mh/$mhfolder";
314 if (not opendir(DIR
, $dir)) {
315 err
(&EX_TEMPFAIL
, "Failed opendir($dir)");
319 @list = readdir(DIR
);
322 err
(&EX_TEMPFAIL
, "Failed readdir($dir)");
328 foreach $msgnum (@list) {
330 if ($msgnum =~ /^[0-9]+$/) {
331 if ($msgnum > $highest) {
340 # We want to print the name of each list that has new mail only once,
341 # so use this hash to store the lists that have already been printed.
342 # Start the list out with SPAM already in it, since we don't care when
343 # new messages are added to it.
344 my %FOLDERS = ('SPAM'=>1);
348 my $mhfolder = shift;
354 # We must do this even in -n mode because later steps fail without
355 # it. This should be harmless.
358 $msgnum = get_highest_msgnum
($mhfolder);
359 for ($try = 0; ; $try++) {
361 $mhmsg = "$mh/$mhfolder/$msgnum";
363 if (not stat($mhmsg)) {
365 # Now we have a non-existent file, let's try to create
366 # it. We must create a zero-byte file first because a
367 # file my appear between our happy stat results and
368 # our later rename(2), which would clobber said file.
369 # So attempt to create a file with this name. If it
370 # succeeds, in just a bit here we'll knowingly clobber
371 # this file with the rename(2) call.
373 if (sysopen(MSG
, "$mhmsg",
374 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
384 # This algorithm is different from the maildir one; let's make
385 # 10 tries instead of 3.
387 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
390 # This algorithm is different; i don't think we need to sleep.
394 logsave
("+$mhfolder");
397 if (not rename($msg, $mhmsg)) {
398 err
(&EX_TEMPFAIL
, "Failed rename($msg, $mhmsg)");
401 # Mark each message as soon as we store it and bomb if that
402 # fails. While it is slow, it is not safe to store multiple
403 # messages and then have a failure before marking some (or
405 if ($mhfolder ne 'SPAM') {
406 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
407 if (not (WIFEXITED
($status) and WEXITSTATUS
($status) == 0)) {
408 errx
(&EX_TEMPFAIL
, "Failed to mark message unseen");
413 if (not $FOLDERS{$mhfolder}) {
414 print("+$mhfolder\n");
415 $FOLDERS{$mhfolder} = 1;
422 my $current; # current header, used for unfolding lines
423 my $fieldname; # unmolested header name
433 # folded header continuation
435 if (not defined($current)) {
436 print("Malformed message, cannot parse headers.\n");
440 $headers{$current} .= $_;
442 ($fieldname) = split(/:/);
443 $current = lc($fieldname);
444 (undef, $headers{$current}) = split(/^$fieldname:\s*/);
453 ###############################################################################
465 $err =~ s
|/new/|/err/|;
466 $sig =~ s
|/new/|/sig/|;
473 ($message, $!) = split(/:/, $line);
475 logerr
("$err: $message: $!");
478 if (unlink($err) != 1) {
479 err
(&EX_TEMPFAIL
, "Failed unlink($err)");
483 # This is supposed to be a signature created with razor-check
484 # directly after delivery. Currently this isn't supported
485 # because it isn't clear to me how to get that signature back
486 # into razor-check. For now, just unlink any sig files we
487 # find and proceed with full razor-check mode.
490 if (unlink($sig) != 1) {
491 err
(&EX_TEMPFAIL
, "Failed unlink($sig)");
497 $status = system("razor-check < $msg");
498 if (not WIFEXITED
($status)) {
499 err
(&EX_TEMPFAIL
, "Failed to run razor-check < $msg");
500 } elsif (WEXITSTATUS
($status) == 0) {
519 foreach $msg (@msglist) {
520 printf('%sChecking for spam... %6d/%d',
521 "\r", ++$i, scalar(@msglist));
524 store_message
($msg, 'SPAM');
529 print("\nDone: ", scalar(@result), " survivors\n");
535 ###############################################################################
547 %headers = get_headers
($msg);
552 log_headers
(%headers);
554 foreach $header (keys(%FILTERS)) {
555 $contents = $headers{lc($header)};
557 if (defined($contents)) {
558 foreach $regex (keys(%{$FILTERS{$header}})) {
559 $subst = $FILTERS{$header}->{$regex};
561 $try = '$contents =~';
562 $try .= " s|$regex|$subst|is";
578 foreach $msg (@msglist) {
579 $mhfolder = find_mh_folder
($msg);
580 store_message
($msg, $mhfolder);
589 $Data::Dumper
::Indent
= 1;
590 print(Dumper
(\
%FILTERS));
594 @msglist = getfiles
("$MAILDIR/new");
596 if (not $filteronly) {
597 @msglist = kill_spam
(@msglist);
601 filter_mail
(@msglist);
611 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
612 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
613 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
616 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
621 The first List-Id filter is a surprisingly effective one which creates
622 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
623 all Sourceforge lists to which i subscribe. Certainly there are lists
624 hosted at Sourceforge which do not work well with this, and this is
625 where it is important to remember that the first match is the one
626 uses. Simply put the more specific rules before this one.
628 The next List-Id example is simple. The swig example demonstrates
629 that the folder name does not have to use a portion of the matched
630 header; you can just hardcode the folder name.
632 The List-Post example is a nice one. Most ASF projects have their own
633 apache.org subdomain, with mailing lists hosted there. So, given a
634 list such as dev@httpd.apache.org, this filter will create the folder
635 name l/apache/httpd/dev.
639 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
640 stolen from Adam Lazur <adam@lazur.org>.
642 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.