]>
diplodocus.org Git - minc/blob - minc
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), optionally checks for spam with a user-defined
18 spam-checking function, and optionally filters mail into separate mh
21 The filtering is quite sophisticated, as it is done using real Perl
22 matching (m//) 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 list 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
('dfhns')) {
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 list.
136 =item $HOME/.mincspam
138 If this file exists, B<minc> will include it with the expectation that it
139 will define a B<is_spam> function. This function takes a message
140 filename as an argument and returns 1 if the message is spam, else 0.
141 If this file does not exist, B<minc> will define a simple function that
144 One of B<minc>'s global variables is available to the user-defined
145 is_spam function: $run. This boolean should be honored; is_spam
146 should only take real action (i.e. removing or creating files, running
147 external programs, etc.) if $run is non-zero.
149 XXX: need more details about the spam-handling process; for now read
152 =item `mhpath +`/logs/minc.log
154 Where B<minc> logs what it does, unless in -n mode.
156 =item `mhpath +`/logs/dryrun.log
158 Where B<minc> logs what it would do; used in -n mode.
160 =item `mhpath +`/.minc.context
162 B<minc> uses this file for context (i.e. current folder) instead of
163 `mhpath +`/context. This allows you some amount of freedom to use mh
164 while B<minc> is running. For example, you can changed folders without
165 causing a message to be stored in the wrong folder. Care must still
166 be taken, however, about the unseen sequence; if you change it
167 (whether via show, or mark, or anything else) while in the same folder
168 as B<minc>, it is likely the new message B<minc> stores will not end up
176 require "$HOME/.mincfilter2";
178 if (-f
"$HOME/.mincspam") {
179 require "$HOME/.mincspam";
193 $logfile = $mh . '/logs/minc.log';
195 $logfile = $mh . '/logs/dryrun.log';
198 $ENV{"MHCONTEXT"} = $mh . '/.minc.context';
201 ###############################################################################
204 # debuglevels for the logger
205 use constant LOG_DEBUGLEVEL
=> 'info';
206 use constant SCREEN_DEBUGLEVEL
=> 'debug';
208 # characters used to wrap around the id field in the log so I can more
209 # easily parse the msg flow
210 use constant DEBUGCHAR
=> '%';
211 use constant INFOCHAR
=> '=';
212 use constant INCOMINGCHAR
=> '<';
213 use constant SAVECHAR
=> '>';
214 use constant WARNCHAR
=> 'W';
215 use constant ERRORCHAR
=> 'E';
217 my $logger = new Log
::Dispatch
;
218 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
223 # log alias, handles getting passed vars that are undef
227 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
229 foreach my $part (@_) {
230 if (defined($part)) {
234 # no newlines in the log message, thanks
236 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
237 $logger->log(level
=>$level, message
=>$msg);
239 if ($act eq SAVECHAR
) {
240 $logger->log(level
=>$level, message
=>"\n");
244 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
245 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
246 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
247 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
248 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
249 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
254 # For an explanation of the %headers structure, see the
255 # get_headers function below.
256 logincoming
('From: ', @{$headers{'return-path'}}[-1]);
257 logincoming
('To: ', @{$headers{'to'}}[-1]);
258 logincoming
('Subject: ', @{$headers{'subject'}}[-1]);
259 logincoming
('Message-Id: ', @{$headers{'message-id'}}[-1]);
263 ###############################################################################
267 printf(STDERR
'%s:%s', __FILE__
, __LINE__
);
281 print(STDERR
": $!\n");
296 my $mhfolder = shift;
301 $folder = $mh . '/' . $mhfolder;
304 if (not -d
$folder) {
305 foreach $component (split('/', $folder)) {
306 if (defined($component) and length($component) >= 1) {
307 $target = $target . '/' . $component;
308 if (-d
$target or mkdir($target)) {
312 "Failed to create +$mhfolder ($component)");
323 if (not opendir(DIR
, $dir)) {
324 err
(&EX_OSERR
, "Failed opendir($dir)");
327 # Initialize $! to 0 (success) because stupid stupid Perl provides
328 # no way to distinguish an error from an empty directory; that
329 # means setting $! to 0 and testing it afterwards is the only way
330 # to detect an error. Real Programmers don't handle errors,
334 ($_ ne '.' and $_ ne '..')
335 and $_ = "$MAILDIR/new/$_"
339 err
(&EX_OSERR
, "Failed readdir($dir)");
347 sub get_highest_msgnum
{
348 my $mhfolder = shift;
354 $dir = "$mh/$mhfolder";
355 if (not opendir(DIR
, $dir)) {
356 err
(&EX_OSERR
, "Failed opendir($dir)");
360 @list = readdir(DIR
);
363 err
(&EX_OSERR
, "Failed readdir($dir)");
369 foreach $msgnum (@list) {
371 if ($msgnum =~ /^[0-9]+$/) {
372 if ($msgnum > $highest) {
381 # We want to print the name of each list that has new mail only once,
382 # so use this hash to store the lists that have already been printed.
383 # Start the list out with SPAM already in it, since we don't care when
384 # new messages are added to it.
385 my %FOLDERS = ('SPAM'=>1);
389 my $mhfolder = shift;
395 # We must do this even in -n mode because later steps fail without
396 # it. This should be harmless.
399 # This loop is a modified version of the maildir delivery algorithm.
400 $msgnum = get_highest_msgnum
($mhfolder);
401 for ($try = 0; ; $try++) {
403 $mhmsg = "$mh/$mhfolder/$msgnum";
405 if (not stat($mhmsg)) {
407 # Now we have a non-existent file, let's try to create
408 # it. We must create a zero-byte file first because a
409 # file my appear between our happy stat results and
410 # our later rename(2), which would clobber said file.
411 # So attempt to create a file with this name. If it
412 # succeeds, in just a bit here we'll knowingly clobber
413 # this file with the rename(2) call.
415 if (sysopen(MSG
, "$mhmsg",
416 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
426 # This algorithm is different from the maildir one; let's make
427 # 10 tries instead of 3.
429 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
432 # This algorithm is different; i don't think we need to sleep.
436 logsave
("+$mhfolder");
439 if (not rename($msg, $mhmsg)) {
440 err
(&EX_OSERR
, "Failed rename($msg, $mhmsg)");
443 # Mark each message as soon as we store it and bomb if that
444 # fails. While it is slow, it is not safe to store multiple
445 # messages and then have a failure before marking some (or
447 if ($mhfolder ne 'SPAM') {
448 $status = system('mark', "+$mhfolder", "$msgnum", '-sequence',
450 if (not WIFEXITED
($status)) {
451 err
(&EX_OSERR
, "Failed to run mark");
452 } elsif (WEXITSTATUS
($status) != 0) {
453 errx
(&EX_SOFTWARE
, "Failed to mark message unseen.");
458 if (not $FOLDERS{$mhfolder}) {
459 print("+$mhfolder\n");
460 $FOLDERS{$mhfolder} = 1;
464 # Parse a message file into a structure describing the headers. The
465 # structure is a hash of arrays. The hash keys are the names of the
466 # headers found in the message, made all lower-case. Each item in
467 # the hash is an array of header text. The array has one element
468 # per occurrence of the header. Most headers will only have a single
469 # element because they occur only once. The Received header is the
470 # most common header with multiple texts.
474 my $current; # current header, used for unfolding lines
475 my $fieldname; # unmolested header name
476 my $contents; # contents of header
486 # folded header continuation
488 if (not defined($current)) {
489 print(STDERR
"Malformed message, cannot parse headers.\n");
493 @{$headers{$current}}[-1] .= $_;
495 ($fieldname) = split(/:/);
496 $current = lc($fieldname);
497 (undef, $contents) = split(/^\Q$fieldname\E:\s*/);
499 if (defined($headers{$current})) {
500 # multiple occurence of same header
501 push(@{$headers{$current}}, $contents);
503 # first occurence of a header
504 $headers{$current} = [$contents];
513 ###############################################################################
525 foreach $msg (@msglist) {
526 printf('%sChecking for spam... %6d/%d',
527 "\r", ++$i, scalar(@msglist));
530 store_message
($msg, 'SPAM');
535 print("\nDone: ", scalar(@result), " survivors\n");
541 ###############################################################################
556 %headers = get_headers
($msg);
561 log_headers
(%headers);
563 # Walk the list of filters. This structure is documented in
564 # pod at the end of the program.
565 foreach $filterref (@FILTERS) {
566 @filter = @$filterref;
567 $header = shift(@filter);
569 # Walk the list of message headers matching the filter's
571 foreach $contents (@{$headers{lc($header)}}) {
572 # Walk the filter's list of match/expression pairs.
573 foreach $pair (@filter) {
574 ($match, $expression) = @$pair;
575 if ($contents =~ /$match/) {
576 if (eval "\$result = \"$expression\"") {
592 foreach $msg (@msglist) {
593 $mhfolder = find_mh_folder
($msg);
594 store_message
($msg, $mhfolder);
603 $Data::Dumper
::Indent
= 1;
604 print(Dumper
(\
@FILTERS));
608 @msglist = getfiles
("$MAILDIR/new");
610 if (not $filteronly) {
611 @msglist = kill_spam
(@msglist);
615 filter_mail
(@msglist);
622 =head1 THE FILTERS STRUCTURE
624 The user's .mincfilter file must define the @FILTERS structure. This
625 structure is an array. Each element of @FILTERS is a filter. A
626 filter is itself an array. The first element of a filter is a string,
627 the name of the header this filter acts upon. The header name is not
628 case-sensitive. Each subsequent element of a filter is a pair (i.e. a
629 two-element array): first, a regular expression B<minc> uses to
630 determine whether this filter matches or not, and second, an
631 expression which B<minc> evaluates to get the folder name.
633 B<minc> decides where to store a message by iterating over the
634 @FILTERS array. It tests each regexp of each filter against all
635 headers matching that filter's specified header. As soon as a match
636 is found, B<minc> evaluates the second part of the pair. This part
637 may contain positional parameters from the matched regexp ($1, $2,
638 etc.). The result of this expression is used as the folder name.
640 Multiple occurrences of the same header are preserved and tested
641 individually against the filters. That means, for example, that
642 filters can search all the Received headers for a certain string.
644 It is important to note that all the arrays of the @FILTERS structure
645 are traversed I<in order>. This means the filters can be arranged so
646 that some have priority over others. XXX: get Doug to write an
647 example of having the same header matched more than once.
654 ['<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>', 'l/$1/$2'],
655 ['<([[:graph:]]+?)(-list)?\.freedesktop\.org>', 'l/freedesktop/$1'],
656 ['<swig\.cs\.uchicago\.edu>', 'l/swig'],
660 ['<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>', 'l/apache/$2/$1'],
668 The first List-Id filter is a surprisingly effective one which creates
669 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
670 all Sourceforge lists to which i subscribe. Certainly there are lists
671 hosted at Sourceforge which do not work well with this, and this is
672 where it is important to remember that the first match is the one
673 uses. Simply put the more specific rules before this one.
675 The next List-Id example is simple. The swig example demonstrates
676 that the folder name does not have to use a portion of the matched
677 header; you can just hardcode the folder name.
679 The List-Post example is a nice one. Most ASF projects have their own
680 apache.org subdomain, with mailing lists hosted there. So, given a
681 list such as dev@httpd.apache.org, this filter will create the folder
682 name l/apache/httpd/dev.
684 For an example is_spam function, see
685 L<http:E<047>E<047>pretzelnet.orgE<047>cvsE<047>dotfilesE<047>.mincspam>
689 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
690 stolen from Adam Lazur <adam@lazur.org>.
692 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.
697 # cperl-indent-level: 4
698 # perl-indent-level: 4
699 # indent-tabs-mode: nil
702 # vi: set tabstop=4 expandtab: