]>
diplodocus.org Git - minc/blob - minc
1f3570fd68b446195652ccbb3edcdcee01fd6db0
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 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
('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 hash.
136 =item $HOME/.mincspam
138 If this file exists, 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, minc will define a simple function that
144 One of 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 minc logs what it does, unless in -n mode.
156 =item `mhpath +`/logs/dryrun.log
158 Where minc logs what it would do; used in -n mode.
165 require "$HOME/.mincfilter";
167 if (-f
"$HOME/.mincspam") {
168 require "$HOME/.mincspam";
182 $logfile = $mh . '/logs/minc.log';
184 $logfile = $mh . '/logs/dryrun.log';
188 ###############################################################################
191 # debuglevels for the logger
192 use constant LOG_DEBUGLEVEL
=> 'info';
193 use constant SCREEN_DEBUGLEVEL
=> 'debug';
195 # characters used to wrap around the id field in the log so I can more
196 # easily parse the msg flow
197 use constant DEBUGCHAR
=> '%';
198 use constant INFOCHAR
=> '=';
199 use constant INCOMINGCHAR
=> '<';
200 use constant SAVECHAR
=> '>';
201 use constant WARNCHAR
=> 'W';
202 use constant ERRORCHAR
=> 'E';
204 my $logger = new Log
::Dispatch
;
205 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
210 # log alias, handles getting passed vars that are undef
214 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
216 foreach my $part (@_) {
217 if (defined($part)) {
221 # no newlines in the log message, thanks
223 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
224 $logger->log(level
=>$level, message
=>$msg);
226 if ($act eq SAVECHAR
) {
227 $logger->log(level
=>$level, message
=>"\n");
231 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
232 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
233 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
234 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
235 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
236 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
241 logincoming
('From: ', $headers{'return-path'});
242 logincoming
('To: ', $headers{'to'});
243 logincoming
('Subject: ', $headers{'subject'});
244 logincoming
('Message-Id: ', $headers{'message-id'});
248 ###############################################################################
252 printf('%s:%s', __FILE__
, __LINE__
);
281 my $mhfolder = shift;
286 $folder = $mh . '/' . $mhfolder;
289 if (not -d
$folder) {
290 foreach $component (split('/', $folder)) {
291 if (defined($component) and length($component) >= 1) {
292 $target = $target . '/' . $component;
293 if (-d
$target or mkdir($target)) {
297 "Failed to create +$mhfolder ($component)");
308 if (not opendir(DIR
, $dir)) {
309 err
(&EX_OSERR
, "Failed opendir($dir)");
312 # Initialize $! to 0 (success) because stupid stupid Perl provides
313 # no way to distinguish an error from an empty directory; that
314 # means setting $! to 0 and testing it afterwards is the only way
315 # to detect an error. Real Programmers don't handle errors,
319 ($_ ne '.' and $_ ne '..')
320 and $_ = "$MAILDIR/new/$_"
324 err
(&EX_OSERR
, "Failed readdir($dir)");
332 sub get_highest_msgnum
{
333 my $mhfolder = shift;
339 $dir = "$mh/$mhfolder";
340 if (not opendir(DIR
, $dir)) {
341 err
(&EX_OSERR
, "Failed opendir($dir)");
345 @list = readdir(DIR
);
348 err
(&EX_OSERR
, "Failed readdir($dir)");
354 foreach $msgnum (@list) {
356 if ($msgnum =~ /^[0-9]+$/) {
357 if ($msgnum > $highest) {
366 # We want to print the name of each list that has new mail only once,
367 # so use this hash to store the lists that have already been printed.
368 # Start the list out with SPAM already in it, since we don't care when
369 # new messages are added to it.
370 my %FOLDERS = ('SPAM'=>1);
374 my $mhfolder = shift;
380 # We must do this even in -n mode because later steps fail without
381 # it. This should be harmless.
384 # This loop is a modified version of the maildir delivery algorithm.
385 $msgnum = get_highest_msgnum
($mhfolder);
386 for ($try = 0; ; $try++) {
388 $mhmsg = "$mh/$mhfolder/$msgnum";
390 if (not stat($mhmsg)) {
392 # Now we have a non-existent file, let's try to create
393 # it. We must create a zero-byte file first because a
394 # file my appear between our happy stat results and
395 # our later rename(2), which would clobber said file.
396 # So attempt to create a file with this name. If it
397 # succeeds, in just a bit here we'll knowingly clobber
398 # this file with the rename(2) call.
400 if (sysopen(MSG
, "$mhmsg",
401 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
411 # This algorithm is different from the maildir one; let's make
412 # 10 tries instead of 3.
414 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
417 # This algorithm is different; i don't think we need to sleep.
421 logsave
("+$mhfolder");
424 if (not rename($msg, $mhmsg)) {
425 err
(&EX_OSERR
, "Failed rename($msg, $mhmsg)");
428 # Mark each message as soon as we store it and bomb if that
429 # fails. While it is slow, it is not safe to store multiple
430 # messages and then have a failure before marking some (or
432 if ($mhfolder ne 'SPAM') {
433 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
434 if (not WIFEXITED
($status)) {
435 err
(&EX_OSERR
, "Failed to run mark");
436 } elsif (WEXITSTATUS
($status) != 0) {
437 errx
(&EX_SOFTWARE
, "Failed to mark message unseen.");
442 if (not $FOLDERS{$mhfolder}) {
443 print("+$mhfolder\n");
444 $FOLDERS{$mhfolder} = 1;
451 my $current; # current header, used for unfolding lines
452 my $fieldname; # unmolested header name
462 # folded header continuation
464 if (not defined($current)) {
465 print("Malformed message, cannot parse headers.\n");
469 $headers{$current} .= $_;
471 ($fieldname) = split(/:/);
472 $current = lc($fieldname);
473 (undef, $headers{$current}) = split(/^\Q$fieldname\E:\s*/);
481 ###############################################################################
493 foreach $msg (@msglist) {
494 printf('%sChecking for spam... %6d/%d',
495 "\r", ++$i, scalar(@msglist));
498 store_message
($msg, 'SPAM');
503 print("\nDone: ", scalar(@result), " survivors\n");
509 ###############################################################################
521 %headers = get_headers
($msg);
526 log_headers
(%headers);
528 foreach $header (keys(%FILTERS)) {
529 $contents = $headers{lc($header)};
531 if (defined($contents)) {
532 foreach $regex (keys(%{$FILTERS{$header}})) {
533 $subst = $FILTERS{$header}->{$regex};
535 $try = '$contents =~';
536 $try .= " s|$regex|$subst|is";
552 foreach $msg (@msglist) {
553 $mhfolder = find_mh_folder
($msg);
554 store_message
($msg, $mhfolder);
563 $Data::Dumper
::Indent
= 1;
564 print(Dumper
(\
%FILTERS));
568 @msglist = getfiles
("$MAILDIR/new");
570 if (not $filteronly) {
571 @msglist = kill_spam
(@msglist);
575 filter_mail
(@msglist);
585 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
586 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
587 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
590 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
595 The first List-Id filter is a surprisingly effective one which creates
596 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
597 all Sourceforge lists to which i subscribe. Certainly there are lists
598 hosted at Sourceforge which do not work well with this, and this is
599 where it is important to remember that the first match is the one
600 uses. Simply put the more specific rules before this one.
602 The next List-Id example is simple. The swig example demonstrates
603 that the folder name does not have to use a portion of the matched
604 header; you can just hardcode the folder name.
606 The List-Post example is a nice one. Most ASF projects have their own
607 apache.org subdomain, with mailing lists hosted there. So, given a
608 list such as dev@httpd.apache.org, this filter will create the folder
609 name l/apache/httpd/dev.
611 For an example is_spam function, see
612 L<http:E<sol>E<sol>pretzelnet.orgE<sol>cvsE<sol>dotfilesE<sol>.mincspam>
616 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
617 stolen from Adam Lazur <adam@lazur.org>.
619 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.