]>
diplodocus.org Git - minc/blob - minc
0e6135a23724cd241ab77b958502cb825bb9963f
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), checks for spam with razor
18 (http://razor.sourceforge.net/), and optionally filters mail into
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.
55 Dry run; do not actually incorporate the mail, but log and report to
56 stdout/stderr as normal.
60 Process SPAM only, then exit, leaving all non-spam messages in the
74 our $opt_s; # ;; # stupid cperl-mode
76 if (not getopts
('dhns')) {
83 print("Sorry bub, no help.\n");
89 if ($opt_s) { # ))){ # stupid cperl-mode
99 Where configuration files (.mincfilter) are found. Also,
100 $HOME/Maildir is used for the maildir if MAILDIR is not set.
104 Where mail is delivered.
110 use Env
qw(HOME MAILDIR);
113 die("HOME environment variable must be set.\n");
116 $MAILDIR = "$HOME/Maildir";
123 =item $HOME/.mincfilter
125 This file is Perl code (included via the 'require' directive) which is
126 expected to define the FILTERS hash.
128 =item `mhpath +`/logs/minc.log
130 Where minc logs what it does, unless in -n mode.
132 =item `mhpath +`/logs/dryrun.log
134 Where minc logs what it would do; used in -n mode.
139 require "$HOME/.mincfilter";
148 $logfile = $mh . '/logs/minc.log';
150 $logfile = $mh . '/logs/dryrun.log';
154 ###############################################################################
157 # debuglevels for the logger
158 use constant LOG_DEBUGLEVEL
=> 'info';
159 use constant SCREEN_DEBUGLEVEL
=> 'debug';
161 # characters used to wrap around the id field in the log so I can more
162 # easily parse the msg flow
163 use constant DEBUGCHAR
=> '%';
164 use constant INFOCHAR
=> '=';
165 use constant INCOMINGCHAR
=> '<';
166 use constant SAVECHAR
=> '>';
167 use constant WARNCHAR
=> 'W';
168 use constant ERRORCHAR
=> 'E';
170 my $logger = new Log
::Dispatch
;
171 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
176 # log alias, handles getting passed vars that are undef
180 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
182 foreach my $part (@_) {
183 if (defined($part)) {
187 # no newlines in the log message, thanks
189 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
190 $logger->log(level
=>$level, message
=>$msg);
192 if ($act eq SAVECHAR
) {
193 $logger->log(level
=>$level, message
=>"\n");
197 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
198 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
199 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
200 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
201 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
202 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
207 logincoming
('From: ', $headers{'return-path'});
208 logincoming
('To: ', $headers{'to'});
209 logincoming
('Subject: ', $headers{'subject'});
210 logincoming
('Message-Id: ', $headers{'message-id'});
214 ###############################################################################
218 printf('%s:%s', __FILE__
, __LINE__
);
247 my $mhfolder = shift;
252 $folder = $mh . '/' . $mhfolder;
255 if (not -d
$folder) {
256 foreach $component (split('/', $folder)) {
257 if (defined($component) and length($component) >= 1) {
258 $target = $target . '/' . $component;
259 if (-d
$target or mkdir($target)) {
263 "Failed to create +$mhfolder ($component)");
274 if (not opendir(DIR
, $dir)) {
275 err
(&EX_TEMPFAIL
, "Failed opendir($dir)");
278 # Initialize $! to 0 (success) because stupid stupid Perl provides
279 # no way to distinguish an error from an empty directory; that
280 # means setting $! to 0 and testing it afterwards is the only way
281 # to detect an error. Real Programmers don't handle errors,
285 ($_ ne '.' and $_ ne '..')
286 and $_ = "$MAILDIR/new/$_"
290 err
(&EX_TEMPFAIL
, "Failed readdir($dir)");
298 sub get_highest_msgnum
{
299 my $mhfolder = shift;
305 $dir = "$mh/$mhfolder";
306 if (not opendir(DIR
, $dir)) {
307 err
(&EX_TEMPFAIL
, "Failed opendir($dir)");
311 @list = readdir(DIR
);
314 err
(&EX_TEMPFAIL
, "Failed readdir($dir)");
320 foreach $msgnum (@list) {
322 if ($msgnum =~ /^[0-9]+$/) {
323 if ($msgnum > $highest) {
332 # We want to print the name of each list that has new mail only once,
333 # so use this hash to store the lists that have already been printed.
334 # Start the list out with SPAM already in it, since we don't care when
335 # new messages are added to it.
336 my %FOLDERS = ('SPAM'=>1);
340 my $mhfolder = shift;
346 # We must do this even in -n mode because later steps fail without
347 # it. This should be harmless.
350 $msgnum = get_highest_msgnum
($mhfolder);
351 for ($try = 0; ; $try++) {
353 $mhmsg = "$mh/$mhfolder/$msgnum";
355 if (not stat($mhmsg)) {
357 # Now we have a non-existent file, let's try to create
358 # it. We must create a zero-byte file first because a
359 # file my appear between our happy stat results and
360 # our later rename(2), which would clobber said file.
361 # So attempt to create a file with this name. If it
362 # succeeds, in just a bit here we'll knowingly clobber
363 # this file with the rename(2) call.
365 if (sysopen(MSG
, "$mhmsg",
366 O_WRONLY
| O_EXCL
| O_CREAT
, 0600)) {
376 # This algorithm is different from the maildir one; let's make
377 # 10 tries instead of 3.
379 errx
(&EX_TEMPFAIL
, "Attempted filename $mhmsg exists.");
382 # This algorithm is different; i don't think we need to sleep.
386 logsave
("+$mhfolder");
389 if (not rename($msg, $mhmsg)) {
390 err
(&EX_TEMPFAIL
, "Failed rename($msg, $mhmsg)");
393 # XXX: Lame! Instead, keep a hash of arrays. Keys are folder
394 # names, array elements are message numbers in that list.
395 # Then after all the messages have been sorted, run one mark
396 # command per folder, marking all messages for that folder in
398 if ($mhfolder ne 'SPAM') {
399 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
400 if (not (WIFEXITED
($status) and WEXITSTATUS
($status) == 0)) {
401 errx
(&EX_TEMPFAIL
, "Failed to mark message unseen");
406 if (not $FOLDERS{$mhfolder}) {
407 print("+$mhfolder\n");
408 $FOLDERS{$mhfolder} = 1;
415 my $current; # current header, used for unfolding lines
416 my $fieldname; # unmolested header name
426 # folded header continuation
428 if (not defined($current)) {
429 print("Malformed message, cannot parse headers.\n");
433 $headers{$current} .= $_;
435 ($fieldname) = split(/:/);
436 $current = lc($fieldname);
437 (undef, $headers{$current}) = split(/^$fieldname:\s*/);
446 ###############################################################################
458 $err =~ s
|/new/|/err/|;
459 $sig =~ s
|/new/|/sig/|;
466 ($message, $!) = split(/:/, $line);
468 logerr
("$err: $message: $!");
471 if (unlink($err) != 1) {
472 err
(&EX_TEMPFAIL
, "Failed unlink($err)");
476 # This is supposed to be a signature created with razor-check
477 # directly after delivery. Currently this isn't supported
478 # because it isn't clear to me how to get that signature back
479 # into razor-check. For now, just unlink any sig files we
480 # find and proceed with full razor-check mode.
483 if (unlink($sig) != 1) {
484 err
(&EX_TEMPFAIL
, "Failed unlink($sig)");
490 $status = system("razor-check < $msg");
491 if (not WIFEXITED
($status)) {
492 err
(&EX_TEMPFAIL
, "Failed to run razor-check < $msg");
493 } elsif (WEXITSTATUS
($status) == 0) {
512 foreach $msg (@msglist) {
513 printf('%sChecking for spam... %6d/%d',
514 "\r", ++$i, scalar(@msglist));
517 store_message
($msg, 'SPAM');
522 print("\nDone: ", scalar(@result), " survivors\n");
528 ###############################################################################
540 %headers = get_headers
($msg);
545 log_headers
(%headers);
547 foreach $header (keys(%FILTERS)) {
548 $contents = $headers{lc($header)};
550 if (defined($contents)) {
551 foreach $regex (keys(%{$FILTERS{$header}})) {
552 $subst = $FILTERS{$header}->{$regex};
554 $try = '$contents =~';
555 $try .= " s|$regex|$subst|is";
571 foreach $msg (@msglist) {
572 $mhfolder = find_mh_folder
($msg);
573 store_message
($msg, $mhfolder);
582 $Data::Dumper
::Indent
= 1;
583 print(Dumper
(\
%FILTERS));
587 @msglist = kill_spam
(getfiles
("$MAILDIR/new"));
590 filter_mail
(@msglist);
600 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
601 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
602 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
605 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
610 The first List-Id filter is a surprisingly effective one which creates
611 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
612 all Sourceforge lists to which i subscribe. Certainly there are lists
613 hosted at Sourceforge which do not work well with this, and this is
614 where it is important to remember that the first match is the one
615 uses. Simply put the more specific rules before this one.
617 The next List-Id example is simple. The swig example demonstrates
618 that the folder name does not have to use a portion of the matched
619 header; you can just hardcode the folder name.
621 The List-Post example is a nice one. Most ASF projects have their own
622 apache.org subdomain, with mailing lists hosted there. So, given a
623 list such as dev@httpd.apache.org, this filter will create the folder
624 name l/apache/httpd/dev.
628 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
629 stolen from Adam Lazur <adam@lazur.org>.
631 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.