]>
diplodocus.org Git - minc/blob - minc
2f852279330b3bf738c41ffa2d4cd13fa626ab09
7 B<minc> - Incorporate mail from a maildir into mh folders.
15 B<minc> incorporates mail from a maildir to a mh folder hierarchy. It
16 takes mail from a maildir folder (not a maildir folder hierarchy),
17 optionally checks for spam with a user-defined spam-checking function,
18 and optionally filters mail into separate mh folders.
20 The filtering is quite sophisticated, as it is done using real Perl
21 matching (m//) commands.
28 require 'sysexits.ph';
32 use Fcntl
qw(O_WRONLY O_EXCL O_CREAT);
38 use Log
::Dispatch
::File
;
39 use POSIX
qw(strftime WEXITSTATUS WIFEXITED);
41 # Autoflush STDOUT for the benefit of the status reporting in kill_spam().
44 # If a filter set's header is $MAGIC_TO_TOKEN, that set is compared
45 # against headers matching this regex (taken from procmail).
46 my $MAGIC_TO_REGEX = '^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To)';
47 my $MAGIC_TO_TOKEN = ' TO';
55 Dump (using Data::Dumper) the FILTERS list and exit. This is useful
56 for testing the syntax of .mincfilter.
60 Filter only, then exit. This is useful after running B<minc -s>.
68 Dry run; do not actually incorporate the mail, but log and report to
69 stdout/stderr as normal.
73 Print the filename of each message before checking it for spam. This
74 can be handy if a particular message is giving the spam checker a
79 Process SPAM only, then exit, leaving all non-spam messages in the
89 my $printfilenames = 0;
97 our $opt_s; # ;; # stupid cperl-mode
99 if (not getopts
('dfhnps')) {
106 print("Sorry bub, no help.\n");
118 } elsif ($opt_s) { # ))){ # stupid cperl-mode
128 Where configuration files (.mincfilter) are found. Also,
129 $HOME/Maildir is used for the maildir if MAILDIR is not set.
133 Where mail is delivered.
139 use Env
qw(HOME MAILDIR);
142 die("HOME environment variable must be set.\n");
145 $MAILDIR = "$HOME/Maildir";
152 =item $HOME/.mincfilter
154 This file is Perl code (included via the 'require' directive) which is
155 expected to define the FILTERS list.
157 =item $HOME/.mincspam
159 If this file exists, B<minc> will include it with the expectation that
160 it will define a B<spam_check> function. This function takes a
161 message filename as an argument and returns 1 if the message is spam,
162 else 0. If this file does not exist, B<minc> will define a simple
163 function that always returns 0.
165 One of B<minc>'s global variables is available to the user-defined
166 B<spam_check> function: $run. This boolean should be honored;
167 B<spam_check> should only take real action (i.e. removing or creating
168 files, running external programs, etc.) if $run is non-zero.
170 This file may also declare two other functions: B<spam_start_hook> and
171 B<spam_stop_hook>. The former is passed no arguments and is expected
172 to return a list. This list is a "baton" that will also be passed to
173 B<spam_check> and B<spam_stop_hook>. It can hold anything
174 B<spam_check> will need to do its job, whether network sockets, pipes,
177 XXX: need more details about the spam-handling process; for now read
180 =item `mhpath +`/logs/minc.log
182 Where B<minc> logs what it does, unless in -n mode.
184 =item `mhpath +`/logs/dryrun.log
186 Where B<minc> logs what it would do; used in -n mode.
188 =item `mhpath +`/.minc.context
190 B<minc> uses this file for context (i.e. current folder) instead of
191 `mhpath +`/context. This allows you some amount of freedom to use mh
192 while B<minc> is running. For example, you can changed folders without
193 causing a message to be stored in the wrong folder. Care must still
194 be taken, however, about the unseen sequence; if you change it
195 (whether via show, or mark, or anything else) while in the same folder
196 as B<minc>, it is likely the new message B<minc> stores will not end up
204 require "$HOME/.mincfilter";
206 if (-f
"$HOME/.mincspam") {
207 require "$HOME/.mincspam";
209 eval "sub spam_start_hook { return (); }";
210 eval "sub spam_stop_hook { }";
211 eval "sub spam_check { return 0; }";
221 $logfile = $mh . '/logs/minc.log';
223 $logfile = $mh . '/logs/dryrun.log';
226 $ENV{"MHCONTEXT"} = $mh . '/.minc.context';
229 ###############################################################################
232 # debuglevels for the logger
233 use constant LOG_DEBUGLEVEL
=> 'info';
234 use constant SCREEN_DEBUGLEVEL
=> 'debug';
236 # characters used to wrap around the id field in the log so I can more
237 # easily parse the msg flow
238 use constant DEBUGCHAR
=> '%';
239 use constant INFOCHAR
=> '=';
240 use constant INCOMINGCHAR
=> '<';
241 use constant SAVECHAR
=> '>';
242 use constant WARNCHAR
=> 'W';
243 use constant ERRORCHAR
=> 'E';
245 my $logger = new Log
::Dispatch
;
246 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
251 # log alias, handles getting passed vars that are undef
255 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
257 foreach my $part (@_) {
258 if (defined($part)) {
262 # no newlines in the log message, thanks
264 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
265 $logger->log(level
=>$level, message
=>$msg);
267 if ($act eq SAVECHAR
) {
268 $logger->log(level
=>$level, message
=>"\n");
272 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
273 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
274 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
275 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
276 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
279 my ($text, @contents) = @_;
283 $last = $contents[-1];
288 mylog
('info', INCOMINGCHAR
, $text, $last);
294 # For an explanation of the %headers structure, see the
295 # get_headers function below.
296 logincoming
('From: ', @{$headers{'return-path'}});
297 logincoming
('To: ', @{$headers{'to'}});
298 logincoming
('Subject: ', @{$headers{'subject'}});
299 logincoming
('Message-Id: ', @{$headers{'message-id'}});
303 ###############################################################################
307 printf(STDERR
'%s:%s', __FILE__
, __LINE__
);
321 print(STDERR
": $!\n");
336 my $mhfolder = shift;
341 $folder = $mh . '/' . $mhfolder;
344 if (not -d
$folder) {
345 foreach $component (split('/', $folder)) {
346 if (defined($component) and length($component) >= 1) {
347 $target = $target . '/' . $component;
348 if (-d
$target or mkdir($target)) {
352 "Failed to create +$mhfolder ($component)");
363 if (not opendir(DIR
, $dir)) {
364 err
(&EX_OSERR
, "Failed opendir($dir)");
367 # Initialize $! to 0 (success) because stupid stupid Perl provides
368 # no way to distinguish an error from an empty directory; that
369 # means setting $! to 0 and testing it afterwards is the only way
370 # to detect an error. Real Programmers don't handle errors,
374 ($_ ne '.' and $_ ne '..')
375 and $_ = "$MAILDIR/new/$_"
379 err
(&EX_OSERR
, "Failed readdir($dir)");
382 if (scalar(@result) == 0) {
391 sub get_highest_msgnum
{
392 my $mhfolder = shift;
398 $dir = "$mh/$mhfolder";
399 if (not opendir(DIR
, $dir)) {
400 err
(&EX_OSERR
, "Failed opendir($dir)");
404 @list = readdir(DIR
);
407 err
(&EX_OSERR
, "Failed readdir($dir)");
413 foreach $msgnum (@list) {
415 if ($msgnum =~ /^[0-9]+$/) {
416 if ($msgnum > $highest) {
425 # We want to print the name of each list that has new mail only once,
426 # so use this hash to store the lists that have already been printed.
427 # Start the list out with SPAM already in it, since we don't care when
428 # new messages are added to it.
429 my %FOLDERS = ('SPAM'=>1);
433 my $mhfolder = shift;
439 # We must do this even in -n mode because later steps fail without
440 # it. This should be harmless.
443 # This loop is a modified version of the maildir delivery algorithm.
444 $msgnum = get_highest_msgnum
($mhfolder);
445 for ($try = 0; ; $try++) {
447 $mhmsg = "$mh/$mhfolder/$msgnum";
449 if (not stat($mhmsg)) {
451 # Now we have a non-existent file, let's try to create
452 # it. We must create a zero-byte file first because a
453 # file my appear between our happy stat results and
454 # our later rename(2), which would clobber said file.
455 # So attempt to create a file with this name. If it
456 # succeeds, in just a bit here we'll knowingly clobber
457 # this file with the rename(2) call.
459 # Another way to do this is not to use rename(2), but
460 # use link(2) + unlink(2) instead. That's how the
461 # standard maildir algorithm does it. Each method has
462 # a disadvantage: the program may crash between the
463 # link(2) and unlink(2) calls. With the standard
464 # maildir algorithm, that means the message will end
465 # up duplicated. The advantage of creating an empty
466 # file followed by rename(2) is that an extra empty
467 # file is left behind as opposed to a duplicate
468 # message. This is more easily detected by the user.
470 if (sysopen(MSG
, "$mhmsg",
471 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
481 # This algorithm is different from the maildir one; let's make
482 # 10 tries instead of 3.
484 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
487 # This algorithm is different; i don't think we need to sleep.
491 if ($mhfolder ne 'SPAM') {
492 logsave
("+$mhfolder");
496 if (not rename($msg, $mhmsg)) {
497 err
(&EX_OSERR
, "Failed rename($msg, $mhmsg)");
500 # Mark each message as soon as we store it and bomb if that
501 # fails. While it is slow, it is not safe to store multiple
502 # messages and then have a failure before marking some (or
504 if ($mhfolder ne 'SPAM') {
505 $status = system('mark', "+$mhfolder", "$msgnum", '-sequence',
507 if (not WIFEXITED
($status)) {
508 err
(&EX_OSERR
, "Failed to run mark");
509 } elsif (WEXITSTATUS
($status) != 0) {
510 errx
(&EX_SOFTWARE
, "Failed to mark message unseen.");
515 if (not $FOLDERS{$mhfolder}) {
516 print("+$mhfolder\n");
517 $FOLDERS{$mhfolder} = 1;
521 # Parse a message file into a structure describing the headers. The
522 # structure is a hash of arrays. The hash keys are the names of the
523 # headers found in the message, made all lower-case. Each item in
524 # the hash is an array of header text. The array has one element
525 # per occurrence of the header. Most headers will only have a single
526 # element because they occur only once. The Received header is the
527 # most common header with multiple texts.
531 my $current; # current header, used for unfolding lines
532 my $fieldname; # unmolested header name
533 my $contents; # contents of header
543 # folded header continuation
545 if (not defined($current)) {
546 print(STDERR
"Malformed message, cannot parse headers.\n");
550 @{$headers{$current}}[-1] .= $_;
552 ($fieldname) = split(/:/);
553 $current = lc($fieldname);
554 (undef, $contents) = split(/^\Q$fieldname\E:\s*/);
556 if (defined($headers{$current})) {
557 # multiple occurence of same header
558 push(@{$headers{$current}}, $contents);
560 # first occurence of a header
561 $headers{$current} = [$contents];
570 ###############################################################################
571 # Spam handling, sorting, etc.
581 @baton = spam_start_hook
();
583 foreach $msg (@msglist) {
584 printf('%sChecking for spam... %6d/%d',
585 "\r", ++$i, scalar(@msglist));
586 if ($printfilenames) {
589 if (spam_check
($msg, @baton)) {
591 store_message
($msg, 'SPAM');
596 print("\nDone: ", scalar(@result), " survivors\n");
598 spam_stop_hook
(@baton);
600 return (map { $_->[1] }
601 sort { $a->[0] <=> $b->[0] }
603 if (not ($st = stat($_))) {
612 ###############################################################################
627 %headers = get_headers
($msg);
632 log_headers
(%headers);
634 # Walk the list of filters. This structure is documented in
635 # pod at the end of the program.
636 foreach $filterref (@FILTERS) {
637 @filter = @$filterref;
638 $header = shift(@filter);
640 # Handle filters using the magic TO header specially.
641 if ($header eq $MAGIC_TO_TOKEN) {
642 foreach $header (keys(%headers)) {
643 if ($header =~ /$MAGIC_TO_REGEX/i) {
644 foreach $contents (@{$headers{lc($header)}}) {
645 foreach $pair (@filter) {
646 ($match, $expression) = @$pair;
647 if ($contents =~ /$match/) {
655 # Now that it's been processed specially, skip normal handling.
659 # Walk the list of message headers matching the filter's
661 foreach $contents (@{$headers{lc($header)}}) {
662 # Walk the filter's list of match/expression pairs.
663 foreach $pair (@filter) {
664 ($match, $expression) = @$pair;
665 if ($contents =~ /$match/i) {
666 if (eval "\$result = \"$expression\"") {
682 foreach $msg (@msglist) {
683 $mhfolder = find_mh_folder
($msg);
684 store_message
($msg, $mhfolder);
693 $Data::Dumper
::Indent
= 1;
694 print(Dumper
(\
@FILTERS));
698 @msglist = getfiles
("$MAILDIR/new");
700 if (not $filteronly) {
701 @msglist = preprocess
(@msglist);
705 filter_mail
(@msglist);
712 =head1 THE FILTERS STRUCTURE
714 The user's .mincfilter file must define the @FILTERS structure. This
715 structure is an array. Each element of @FILTERS is a filter. A
716 filter is itself an array. The first element of a filter is a string,
717 the name of the header this filter acts upon. The header name is not
718 case-sensitive. Each subsequent element of a filter is a pair (i.e. a
719 two-element array): first, a regular expression B<minc> uses to
720 determine whether this filter matches or not, and second, an
721 expression which B<minc> evaluates to get the folder name.
723 B<minc> decides where to store a message by iterating over the
724 @FILTERS array. It tests each regexp of each filter against all
725 headers matching that filter's specified header. As soon as a match
726 is found, B<minc> evaluates the second part of the pair. This part
727 may contain positional parameters from the matched regexp ($1, $2,
728 etc.). The result of this expression is used as the folder name.
730 Multiple occurrences of the same header are preserved and tested
731 individually against the filters. That means, for example, that
732 filters can search all the Received headers for a certain string.
734 It is important to note that all the arrays of the @FILTERS structure
735 are traversed I<in order>. This means the filters can be arranged so
736 that some have priority over others. XXX: get Doug to write an
737 example of having the same header matched more than once.
739 Lastly, B<minc> supports a magic ' TO' header. Filters using this
740 header are matched against a collection of headers related to (and
741 including) the To. Which headers to use is determined by a regular
742 expression borrowed from procmail.
749 ['<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>', 'l/$1/$2'],
750 ['<([[:graph:]]+?)(-list)?\.freedesktop\.org>', 'l/freedesktop/$1'],
751 ['<swig\.cs\.uchicago\.edu>', 'l/swig'],
755 ['<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>', 'l/apache/$2/$1'],
763 The first List-Id filter is a surprisingly effective one which creates
764 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
765 all Sourceforge lists to which i subscribe. Certainly there are lists
766 hosted at Sourceforge which do not work well with this, and this is
767 where it is important to remember that the first match is the one
768 uses. Simply put the more specific rules before this one.
770 The next List-Id example is simple. The swig example demonstrates
771 that the folder name does not have to use a portion of the matched
772 header; you can just hardcode the folder name.
774 The List-Post example is a nice one. Most ASF projects have their own
775 apache.org subdomain, with mailing lists hosted there. So, given a
776 list such as dev@httpd.apache.org, this filter will create the folder
777 name l/apache/httpd/dev.
779 For an example B<spam_check> function, see
780 L<http:E<047>E<047>pretzelnet.orgE<047>cvsE<047>dotfilesE<047>.mincspam>
784 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
785 stolen from Adam Lazur <adam@lazur.org>.
787 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.
792 # cperl-indent-level: 4
793 # perl-indent-level: 4
794 # indent-tabs-mode: nil
797 # vi: set tabstop=4 expandtab: