]>
diplodocus.org Git - minc/blob - minc
2cc604a5d99aa81d8450d6c28b0814576ad83d73
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);
47 Dump (using Data::Dumper) the FILTERS hash and exit. This is useful
48 for testing the syntax of .mincfilter.
52 Filter only, then exit. This is useful after running B<minc -s>.
60 Dry run; do not actually incorporate the mail, but log and report to
61 stdout/stderr as normal.
65 Process SPAM only, then exit, leaving all non-spam messages in the
81 our $opt_s; # ;; # stupid cperl-mode
83 if (not getopts
('dfhns')) {
90 print("Sorry bub, no help.\n");
98 } elsif ($opt_s) { # ))){ # stupid cperl-mode
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 hash.
137 =item $HOME/.mincspam
139 If this file exists, minc will include it with the expectation that it
140 will define a B<is_spam> function. This function takes a message
141 filename as an argument and returns 1 if the message is spam, else 0.
142 If this file does not exist, minc will define a simple function that
145 One of minc's global variables is available to the user-defined
146 is_spam function: $run. This boolean should be honored; is_spam
147 should only take real action (i.e. removing or creating files, running
148 external programs, etc.) if $run is non-zero.
150 XXX: need more details about the spam-handling process; for now read
153 =item `mhpath +`/logs/minc.log
155 Where minc logs what it does, unless in -n mode.
157 =item `mhpath +`/logs/dryrun.log
159 Where minc logs what it would do; used in -n mode.
166 tie
(%FILTERS, 'Tie::IxHash');
167 require "$HOME/.mincfilter";
169 if (-f
"$HOME/.mincspam") {
170 require "$HOME/.mincspam";
184 $logfile = $mh . '/logs/minc.log';
186 $logfile = $mh . '/logs/dryrun.log';
190 ###############################################################################
193 # debuglevels for the logger
194 use constant LOG_DEBUGLEVEL
=> 'info';
195 use constant SCREEN_DEBUGLEVEL
=> 'debug';
197 # characters used to wrap around the id field in the log so I can more
198 # easily parse the msg flow
199 use constant DEBUGCHAR
=> '%';
200 use constant INFOCHAR
=> '=';
201 use constant INCOMINGCHAR
=> '<';
202 use constant SAVECHAR
=> '>';
203 use constant WARNCHAR
=> 'W';
204 use constant ERRORCHAR
=> 'E';
206 my $logger = new Log
::Dispatch
;
207 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
212 # log alias, handles getting passed vars that are undef
216 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
218 foreach my $part (@_) {
219 if (defined($part)) {
223 # no newlines in the log message, thanks
225 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
226 $logger->log(level
=>$level, message
=>$msg);
228 if ($act eq SAVECHAR
) {
229 $logger->log(level
=>$level, message
=>"\n");
233 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
234 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
235 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
236 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
237 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
238 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
243 logincoming
('From: ', $headers{'return-path'});
244 logincoming
('To: ', $headers{'to'});
245 logincoming
('Subject: ', $headers{'subject'});
246 logincoming
('Message-Id: ', $headers{'message-id'});
250 ###############################################################################
254 printf('%s:%s', __FILE__
, __LINE__
);
283 my $mhfolder = shift;
288 $folder = $mh . '/' . $mhfolder;
291 if (not -d
$folder) {
292 foreach $component (split('/', $folder)) {
293 if (defined($component) and length($component) >= 1) {
294 $target = $target . '/' . $component;
295 if (-d
$target or mkdir($target)) {
299 "Failed to create +$mhfolder ($component)");
310 if (not opendir(DIR
, $dir)) {
311 err
(&EX_OSERR
, "Failed opendir($dir)");
314 # Initialize $! to 0 (success) because stupid stupid Perl provides
315 # no way to distinguish an error from an empty directory; that
316 # means setting $! to 0 and testing it afterwards is the only way
317 # to detect an error. Real Programmers don't handle errors,
321 ($_ ne '.' and $_ ne '..')
322 and $_ = "$MAILDIR/new/$_"
326 err
(&EX_OSERR
, "Failed readdir($dir)");
334 sub get_highest_msgnum
{
335 my $mhfolder = shift;
341 $dir = "$mh/$mhfolder";
342 if (not opendir(DIR
, $dir)) {
343 err
(&EX_OSERR
, "Failed opendir($dir)");
347 @list = readdir(DIR
);
350 err
(&EX_OSERR
, "Failed readdir($dir)");
356 foreach $msgnum (@list) {
358 if ($msgnum =~ /^[0-9]+$/) {
359 if ($msgnum > $highest) {
368 # We want to print the name of each list that has new mail only once,
369 # so use this hash to store the lists that have already been printed.
370 # Start the list out with SPAM already in it, since we don't care when
371 # new messages are added to it.
372 my %FOLDERS = ('SPAM'=>1);
376 my $mhfolder = shift;
382 # We must do this even in -n mode because later steps fail without
383 # it. This should be harmless.
386 # This loop is a modified version of the maildir delivery algorithm.
387 $msgnum = get_highest_msgnum
($mhfolder);
388 for ($try = 0; ; $try++) {
390 $mhmsg = "$mh/$mhfolder/$msgnum";
392 if (not stat($mhmsg)) {
394 # Now we have a non-existent file, let's try to create
395 # it. We must create a zero-byte file first because a
396 # file my appear between our happy stat results and
397 # our later rename(2), which would clobber said file.
398 # So attempt to create a file with this name. If it
399 # succeeds, in just a bit here we'll knowingly clobber
400 # this file with the rename(2) call.
402 if (sysopen(MSG
, "$mhmsg",
403 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
413 # This algorithm is different from the maildir one; let's make
414 # 10 tries instead of 3.
416 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
419 # This algorithm is different; i don't think we need to sleep.
423 logsave
("+$mhfolder");
426 if (not rename($msg, $mhmsg)) {
427 err
(&EX_OSERR
, "Failed rename($msg, $mhmsg)");
430 # Mark each message as soon as we store it and bomb if that
431 # fails. While it is slow, it is not safe to store multiple
432 # messages and then have a failure before marking some (or
434 if ($mhfolder ne 'SPAM') {
435 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
436 if (not WIFEXITED
($status)) {
437 err
(&EX_OSERR
, "Failed to run mark");
438 } elsif (WEXITSTATUS
($status) != 0) {
439 errx
(&EX_SOFTWARE
, "Failed to mark message unseen.");
444 if (not $FOLDERS{$mhfolder}) {
445 print("+$mhfolder\n");
446 $FOLDERS{$mhfolder} = 1;
453 my $current; # current header, used for unfolding lines
454 my $fieldname; # unmolested header name
464 # folded header continuation
466 if (not defined($current)) {
467 print("Malformed message, cannot parse headers.\n");
471 $headers{$current} .= $_;
473 ($fieldname) = split(/:/);
474 $current = lc($fieldname);
475 (undef, $headers{$current}) = split(/^\Q$fieldname\E:\s*/);
483 ###############################################################################
495 foreach $msg (@msglist) {
496 printf('%sChecking for spam... %6d/%d',
497 "\r", ++$i, scalar(@msglist));
500 store_message
($msg, 'SPAM');
505 print("\nDone: ", scalar(@result), " survivors\n");
511 ###############################################################################
523 %headers = get_headers
($msg);
528 log_headers
(%headers);
530 foreach $header (keys(%FILTERS)) {
531 $contents = $headers{lc($header)};
533 if (defined($contents)) {
534 foreach $regex (keys(%{$FILTERS{$header}})) {
535 $subst = $FILTERS{$header}->{$regex};
537 $try = '$contents =~';
538 $try .= " s|$regex|$subst|is";
554 foreach $msg (@msglist) {
555 $mhfolder = find_mh_folder
($msg);
556 store_message
($msg, $mhfolder);
565 $Data::Dumper
::Indent
= 1;
566 print(Dumper
(\
%FILTERS));
570 @msglist = getfiles
("$MAILDIR/new");
572 if (not $filteronly) {
573 @msglist = kill_spam
(@msglist);
577 filter_mail
(@msglist);
587 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
588 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
589 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
592 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
597 The first List-Id filter is a surprisingly effective one which creates
598 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
599 all Sourceforge lists to which i subscribe. Certainly there are lists
600 hosted at Sourceforge which do not work well with this, and this is
601 where it is important to remember that the first match is the one
602 uses. Simply put the more specific rules before this one.
604 The next List-Id example is simple. The swig example demonstrates
605 that the folder name does not have to use a portion of the matched
606 header; you can just hardcode the folder name.
608 The List-Post example is a nice one. Most ASF projects have their own
609 apache.org subdomain, with mailing lists hosted there. So, given a
610 list such as dev@httpd.apache.org, this filter will create the folder
611 name l/apache/httpd/dev.
613 For an example is_spam function, see
614 L<http:E<sol>E<sol>pretzelnet.orgE<sol>cvsE<sol>dotfilesE<sol>.mincspam>
618 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
619 stolen from Adam Lazur <adam@lazur.org>.
621 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.