]>
diplodocus.org Git - minc/blob - minc
80c88f4c7c3fef312214cf308e5fd38be064cadf
7 B<minc> - incorporate mail from a maildir into mh folders
11 B<minc> [B<-m> I<MAX>] [B<-n>] [B<-p>]
19 B<minc> incorporates mail from a maildir to a mh folder hierarchy. It
20 takes mail from a maildir folder (not a maildir folder hierarchy),
21 optionally checks for spam with a user-defined spam-checking function,
22 and optionally filters mail into separate mh folders.
24 The filtering is quite sophisticated, as it is done using real Perl
25 matching (m//) commands.
34 use Fcntl
qw(O_WRONLY O_EXCL O_CREAT);
37 use Getopt
::Long
qw(:config gnu_getopt no_ignore_case);
38 use POSIX
qw(strftime WEXITSTATUS WIFEXITED);
43 # If a filter set's header is $MAGIC_TO_TOKEN, that set is compared
44 # against headers matching this regex (taken from procmail).
45 my $MAGIC_TO_REGEX = '^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To)';
46 my $MAGIC_TO_TOKEN = ' TO';
48 # List of SPAM message numbers, scanned at the end so the user can
49 # check for false positives.
58 Dump (using Data::Dumper) the FILTERS list and exit. This is useful
59 for testing the syntax of .mincfilter.
67 Stop processing after I<MAX> messages.
71 Dry run; do not actually incorporate the mail, but log and report to
72 stdout/stderr as normal.
76 Print the filename of each message before checking it for spam. This
77 can be handy if a particular message is giving the spam checker a
95 'p' => \
$printfilenames,
97 $help and pod2usage
(-exitstatus
=>0, -verbose
=>1);
98 scalar(@ARGV) == 0 or pod2usage
();
108 Where configuration files (.mincfilter) are found. Also,
109 $HOME/Maildir is used for the maildir if MAILDIR is not set.
113 Where mail is delivered.
119 use Env
qw(HOME MAILDIR);
122 die("HOME environment variable must be set.\n");
125 $MAILDIR = "$HOME/Maildir";
132 =item $HOME/.mincfilter
134 This file is Perl code (included via the 'require' directive) which is
135 expected to define the FILTERS list.
137 =item $HOME/.mincspam
139 If this file exists, B<minc> will include it with the expectation that
140 it will define a B<spam_check> function. This function takes a
141 message filename as an argument and returns 1 if the message is spam,
142 else 0. If this file does not exist, B<minc> will define a simple
143 function that always returns 0.
145 One of B<minc>'s global variables is available to the user-defined
146 B<spam_check> function: $run. This boolean should be honored;
147 B<spam_check> should only take real action (i.e. removing or creating
148 files, running external programs, etc.) if $run is non-zero.
150 This file may also declare two other functions: B<spam_start_hook> and
151 B<spam_stop_hook>. The former is passed no arguments and is expected
152 to return a list. This list is a "baton" that will also be passed to
153 B<spam_check> and B<spam_stop_hook>. It can hold anything
154 B<spam_check> will need to do its job, whether network sockets, pipes,
157 XXX: need more details about the spam-handling process; for now read
160 =item `mhpath +`/logs/minc.log
162 Where B<minc> logs what it does, unless in -n mode.
164 =item `mhpath +`/logs/dryrun.log
166 Where B<minc> logs what it would do; used in -n mode.
168 =item `mhpath +`/.minc.context
170 B<minc> uses this file for context (i.e. current folder) instead of
171 `mhpath +`/context. This allows you some amount of freedom to use mh
172 while B<minc> is running. For example, you can changed folders without
173 causing a message to be stored in the wrong folder. Care must still
174 be taken, however, about the unseen sequence; if you change it
175 (whether via show, or mark, or anything else) while in the same folder
176 as B<minc>, it is likely the new message B<minc> stores will not end up
184 require "$HOME/.mincfilter";
193 $logfile = $mh . '/logs/minc.log';
195 $logfile = $mh . '/logs/dryrun.log';
198 $ENV{"MHCONTEXT"} = $mh . '/.minc.context';
201 ###############################################################################
205 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
207 foreach my $part (@_) {
208 if (defined($part)) {
212 # no newlines in the log message, thanks
215 open(LOG
, ">>$logfile") or die("open(>>$logfile): $!");
216 print(LOG
"$timestamp $msg\n") or die("print(>>$logfile): $!");
217 close(LOG
) or die("close($logfile): $!");
221 my ($text, @contents) = @_;
225 $last = $contents[-1];
230 mylog
('<< ', $text, $last);
236 # For an explanation of the %headers structure, see the
237 # get_headers function below.
238 logheader
('From: ', @{$headers{'return-path'}});
239 logheader
('To: ', @{$headers{'to'}});
240 logheader
('Subject: ', @{$headers{'subject'}});
241 logheader
('Message-Id: ', @{$headers{'message-id'}});
245 ###############################################################################
249 my $mhfolder = shift;
254 $folder = $mh . '/' . $mhfolder;
257 if (not -d
$folder) {
258 foreach $component (split('/', $folder)) {
259 if (defined($component) and length($component) >= 1) {
260 $target = $target . '/' . $component;
261 if (-d
$target or mkdir($target)) {
264 die("Failed to create +$mhfolder ($component)");
275 if (not opendir(DIR
, $dir)) {
276 die("Failed opendir($dir)");
279 # Initialize $! to 0 (success) because stupid stupid Perl provides
280 # no way to distinguish an error from an empty directory; that
281 # means setting $! to 0 and testing it afterwards is the only way
282 # to detect an error. Real Programmers don't handle errors,
286 ($_ ne '.' and $_ ne '..')
287 and $_ = "$MAILDIR/new/$_"
291 die("Failed readdir($dir)");
294 if (scalar(@result) == 0) {
298 STDOUT-
>autoflush(1);
299 print(scalar(@result), " messages...");
306 sub get_highest_msgnum
{
307 my $mhfolder = shift;
313 $dir = "$mh/$mhfolder";
314 if (not opendir(DIR
, $dir)) {
315 die("Failed opendir($dir)");
319 @list = readdir(DIR
);
322 die("Failed readdir($dir)");
328 foreach $msgnum (@list) {
330 if ($msgnum =~ /^[0-9]+$/) {
331 if ($msgnum > $highest) {
342 my $mhfolder = shift;
348 # We must do this even in -n mode because later steps fail without
349 # it. This should be harmless.
352 # This loop is a modified version of the maildir delivery algorithm.
353 $msgnum = get_highest_msgnum
($mhfolder);
354 for ($try = 0; ; $try++) {
356 $mhmsg = "$mh/$mhfolder/$msgnum";
358 if (not stat($mhmsg)) {
360 # Now we have a non-existent file, let's try to create
361 # it. We must create a zero-byte file first because a
362 # file my appear between our happy stat results and
363 # our later rename(2), which would clobber said file.
364 # So attempt to create a file with this name. If it
365 # succeeds, in just a bit here we'll knowingly clobber
366 # this file with the rename(2) call.
368 # Another way to do this is not to use rename(2), but
369 # use link(2) + unlink(2) instead. That's how the
370 # standard maildir algorithm does it. Each method has
371 # a disadvantage: the program may crash between the
372 # link(2) and unlink(2) calls. With the standard
373 # maildir algorithm, that means the message will end
374 # up duplicated. The advantage of creating an empty
375 # file followed by rename(2) is that an extra empty
376 # file is left behind as opposed to a duplicate
377 # message. This is more easily detected by the user.
379 if (sysopen(MSG
, "$mhmsg",
380 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
390 # This algorithm is different from the maildir one; let's make
391 # 10 tries instead of 3.
393 die("Attempted filename $mhmsg exists.");
396 # This algorithm is different; i don't think we need to sleep.
400 if ($mhfolder ne 'SPAM') {
401 mylog
('+', $mhfolder);
405 if (not rename($msg, $mhmsg)) {
406 die("Failed rename($msg, $mhmsg)");
409 # Mark each message as soon as we store it and bomb if that
410 # fails. While it is slow, it is not safe to store multiple
411 # messages and then have a failure before marking some (or
413 if ($mhfolder eq 'SPAM') {
414 push(@SPAM, $msgnum);
416 $status = system('mark', "+$mhfolder", "$msgnum", '-sequence',
418 if (not WIFEXITED
($status)) {
419 die("Failed to run mark");
420 } elsif (WEXITSTATUS
($status) != 0) {
421 die("Failed to mark message unseen.");
429 # Parse a message file into a structure describing the headers. The
430 # structure is a hash of arrays. The hash keys are the names of the
431 # headers found in the message, made all lower-case. Each item in
432 # the hash is an array of header text. The array has one element
433 # per occurrence of the header. Most headers will only have a single
434 # element because they occur only once. The Received header is the
435 # most common header with multiple texts.
439 my $current; # current header, used for unfolding lines
440 my $fieldname; # unmolested header name
441 my $contents; # contents of header
451 # folded header continuation
453 if (not defined($current)) {
454 print(STDERR
"Malformed message, cannot parse headers.\n");
458 @{$headers{$current}}[-1] .= $_;
460 ($fieldname) = split(/:/);
461 $current = lc($fieldname);
462 (undef, $contents) = split(/^\Q$fieldname\E:\s*/);
464 if (defined($headers{$current})) {
465 # multiple occurence of same header
466 push(@{$headers{$current}}, $contents);
468 # first occurence of a header
469 $headers{$current} = [$contents];
479 ###############################################################################
494 %headers = get_headers
($msg);
499 log_headers
(%headers);
501 # Walk the list of filters. This structure is documented in
502 # pod at the end of the program.
503 foreach $filterref (@FILTERS) {
504 @filter = @$filterref;
505 $header = shift(@filter);
507 # Handle filters using the magic TO header specially.
508 if ($header eq $MAGIC_TO_TOKEN) {
509 foreach $header (keys(%headers)) {
510 if ($header =~ /$MAGIC_TO_REGEX/i) {
511 foreach $contents (@{$headers{lc($header)}}) {
512 foreach $pair (@filter) {
513 ($match, $expression) = @$pair;
514 if ($contents =~ /$match/) {
522 # Now that it's been processed specially, skip normal handling.
526 # Walk the list of message headers matching the filter's
528 foreach $contents (@{$headers{lc($header)}}) {
529 # Walk the filter's list of match/expression pairs.
530 foreach $pair (@filter) {
531 ($match, $expression) = @$pair;
532 if ($contents =~ /$match/i) {
533 if (eval "\$result = \"$expression\"") {
546 my $msgcount = scalar(@msglist);
547 my $msgcountlen = length($msgcount);
553 my %FOLDERS = ('SPAM'=>1);
555 if (-f
"$HOME/.mincspam") {
556 require "$HOME/.mincspam";
558 eval "sub spam_start_hook { return (); }";
559 eval "sub spam_stop_hook { }";
560 eval "sub spam_check { return 0; }";
563 @baton = spam_start_hook
();
565 foreach $msg (@msglist) {
566 if (spam_check
($msg, @baton)) {
568 $spam = scalar(@SPAM)+ 1;
570 $mhfolder = find_mh_folder
($msg);
574 store_message
($msg, $mhfolder);
577 if (not $FOLDERS{$mhfolder}) {
578 print(' ' x
$msgcountlen);
579 print(" \r+$mhfolder\n");
580 $FOLDERS{$mhfolder} = 1;
583 printf('%6d SPAM %6d saved %6d/%1d',
584 $spam, $saved, $spam + $saved, $msgcount);
586 defined($maxmsgs) and ($spam + $saved < $maxmsgs or last);
590 spam_stop_hook
(@baton);
599 $Data::Dumper
::Indent
= 1;
600 print(Dumper
(\
@FILTERS));
606 sort { $a->[0] <=> $b->[0] }
608 if (not ($st = stat($_))) {
613 getfiles
("$MAILDIR/new"));
615 filter_mail
(@msglist);
617 @SPAM and (exec('scan', '+SPAM', @SPAM) or die);
623 =head1 THE FILTERS STRUCTURE
625 The user's .mincfilter file must define the @FILTERS structure. This
626 structure is an array. Each element of @FILTERS is a filter. A
627 filter is itself an array. The first element of a filter is a string,
628 the name of the header this filter acts upon. The header name is not
629 case-sensitive. Each subsequent element of a filter is a pair (i.e. a
630 two-element array): first, a regular expression B<minc> uses to
631 determine whether this filter matches or not, and second, an
632 expression which B<minc> evaluates to get the folder name.
634 B<minc> decides where to store a message by iterating over the
635 @FILTERS array. It tests each regexp of each filter against all
636 headers matching that filter's specified header. As soon as a match
637 is found, B<minc> evaluates the second part of the pair. This part
638 may contain positional parameters from the matched regexp ($1, $2,
639 etc.). The result of this expression is used as the folder name.
641 Multiple occurrences of the same header are preserved and tested
642 individually against the filters. That means, for example, that
643 filters can search all the Received headers for a certain string.
645 It is important to note that all the arrays of the @FILTERS structure
646 are traversed I<in order>. This means the filters can be arranged so
647 that some have priority over others. XXX: get Doug to write an
648 example of having the same header matched more than once.
650 Lastly, B<minc> supports a magic ' TO' header. Filters using this
651 header are matched against a collection of headers related to (and
652 including) the To. Which headers to use is determined by a regular
653 expression borrowed from procmail.
660 ['<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>', 'l/$1/$2'],
661 ['<([[:graph:]]+?)(-list)?\.freedesktop\.org>', 'l/freedesktop/$1'],
662 ['<swig\.cs\.uchicago\.edu>', 'l/swig'],
666 ['<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>', 'l/apache/$2/$1'],
674 The first List-Id filter is a surprisingly effective one which creates
675 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
676 all Sourceforge lists to which i subscribe. Certainly there are lists
677 hosted at Sourceforge which do not work well with this, and this is
678 where it is important to remember that the first match is the one
679 uses. Simply put the more specific rules before this one.
681 The next List-Id example is simple. The swig example demonstrates
682 that the folder name does not have to use a portion of the matched
683 header; you can just hardcode the folder name.
685 The List-Post example is a nice one. Most ASF projects have their own
686 apache.org subdomain, with mailing lists hosted there. So, given a
687 list such as dev@httpd.apache.org, this filter will create the folder
688 name l/apache/httpd/dev.
690 For an example B<spam_check> function, see
691 L<http:E<047>E<047>pretzelnet.orgE<047>cvsE<047>dotfilesE<047>.mincspam>
695 Written by Eric Gillespie <epg@pretzelnet.org>. Design by Eric
696 Gillespie and Doug Porter <dsp@waterspout.com>.
698 This program is free software; you can redistribute it and/or modify
699 it under the same terms as Perl itself.
704 # cperl-indent-level: 4
705 # perl-indent-level: 4
706 # indent-tabs-mode: nil
709 # vi: set tabstop=4 expandtab: