]>
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), checks for spam with razor (XXX crossref), and
18 optionally filters mail into separate mh folders.
20 The filtering is quite sophisticated, as it is done using real Perl
21 substitution (s//) commands.
28 require 'sysexits.ph';
34 use Log
::Dispatch
::File
;
35 use POSIX
qw(strftime WEXITSTATUS WIFEXITED);
37 # debuglevels for the logger
38 use constant LOG_DEBUGLEVEL
=> 'info';
39 use constant SCREEN_DEBUGLEVEL
=> 'debug';
41 # characters used to wrap around the id field in the log so I can more
42 # easily parse the msg flow
43 use constant DEBUGCHAR
=> '%';
44 use constant INFOCHAR
=> '=';
45 use constant INCOMINGCHAR
=> '<';
46 use constant SAVECHAR
=> '>';
47 use constant WARNCHAR
=> 'W';
48 use constant ERRORCHAR
=> 'E';
56 Dump (using Data::Dumper) the FILTERS hash and exit. This is useful
57 for testing the syntax of .mincfilter.
65 Dry run; do not actually incorporate the mail, but log and report to
66 stdout/stderr as normal.
70 Process SPAM only, then exit, leaving all non-spam messages in the
84 our $opt_s; # ;; # stupid cperl-mode
86 if (!getopts
('dhns')) {
93 print("Sorry bub, no help.\n");
99 if ($opt_s) { # ))){ # stupid cperl-mode
109 Where configuration files (.mincfilter) are found.
113 Where mail is delivered.
123 die("HOME environment variable must be set.\n");
126 $MAILDIR = "$HOME/Maildir";
133 =item $HOME/.mincfilter
135 This file is Perl code (included via the 'require' directive) which is
136 expected to define the FILTERS hash.
138 =item `mhpath +`/logs/minc.log
140 Where minc logs what it does, unless in -n mode.
142 =item `mhpath +`/logs/dryrun.log
144 Where minc logs what it would do; used in -n mode.
149 require "$HOME/.mincfilter";
158 $logfile = $mh . '/logs/minc.log';
160 $logfile = $mh . '/logs/dryrun.log';
164 ###############################################################################
167 my $logger = new Log
::Dispatch
;
168 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
173 # log alias, handles getting passed vars that are undef
177 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
179 foreach my $part (@_) {
180 if (defined($part)) {
184 # no newlines in the log message, thanks
186 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
187 $logger->log(level
=>$level, message
=>$msg);
189 if ($act eq SAVECHAR
) {
190 $logger->log(level
=>$level, message
=>"\n");
194 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
195 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
196 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
197 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
198 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
199 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
204 logincoming
('From: ', $headers{'return-path'});
205 logincoming
('To: ', $headers{'to'});
206 logincoming
('Subject: ', $headers{'subject'});
207 logincoming
('Message-Id: ', $headers{'message-id'});
211 ###############################################################################
215 printf('%s:%s', __FILE__
, __LINE__
);
244 my $mhfolder = shift;
249 $folder = $mh . '/' . $mhfolder;
253 foreach $component (split('/', $folder)) {
254 if (defined($component) and length($component) >= 1) {
255 $target = $target . '/' . $component;
256 if (-d
$target or mkdir($target)) {
260 "Failed to create +$mhfolder ($component)");
267 # We want to print the name of each list that has new mail only once,
268 # so use this hash to store the lists that have already been printed.
269 # Start the list out with SPAM already in it, since we don't care when
270 # new messages are added to it.
271 my %FOLDERS = ('SPAM'=>1);
275 my $mhfolder = shift;
280 # We must do this even in -n mode because later steps fail without
281 # it. This should be harmless.
284 $mhmsg = `mhpath +$mhfolder new`;
286 $msgnum = basename
($mhmsg);
288 logsave
("+$mhfolder");
291 if (not rename($msg, $mhmsg)) {
292 err
(&EX_TEMPFAIL
, "Failed rename($msg, $mhmsg)");
295 if ($mhfolder ne 'SPAM') {
296 $status = system("mark +$mhfolder $msgnum -sequence unseen -add");
297 if (not (WIFEXITED
($status) and WEXITSTATUS
($status) == 0)) {
298 errx
(&EX_TEMPFAIL
, "Failed to mark message unseen");
303 if (not $FOLDERS{$mhfolder}) {
304 print("+$mhfolder\n");
305 $FOLDERS{$mhfolder} = 1;
312 my $current; # current header, used for unfolding lines
313 my $fieldname; # unmolested header name
323 # folded header continuation
325 if (!defined($current)) {
326 print("Malformed message, cannot parse headers.\n");
330 $headers{$current} .= $_;
332 ($fieldname) = split(/:/);
333 $current = lc($fieldname);
334 (undef, $headers{$current}) = split(/^$fieldname:\s*/);
343 ###############################################################################
353 $err =~ s
|/new/|/err/|;
354 $sig =~ s
|/new/|/sig/|;
364 ($message, $!) = split(/:/, $line);
366 logerr
("$err: $message: $!");
369 if (unlink($err) != 1) {
370 err
(&EX_TEMPFAIL
, "Failed unlink($err)");
374 # This is supposed to be a signature created with razor-check
375 # directly after delivery. Currently this isn't supported
376 # because it isn't clear to me how to get that signature back
377 # into razor-check. For now, just unlink any sig files we
378 # find and proceed with full razor-check mode.
381 if (unlink($sig) != 1) {
382 err
(&EX_TEMPFAIL
, "Failed unlink($sig)");
388 $status = system("razor-check < $msg");
389 if (not WIFEXITED
($status)) {
390 err
(&EX_TEMPFAIL
, "Failed to run razor-check < $msg");
391 } elsif (WEXITSTATUS
($status) == 0) {
410 foreach $msg (@msglist) {
411 printf('%sChecking for spam... %6d/%d',
412 "\r", ++$i, scalar(@msglist));
415 store_message
($msg, 'SPAM');
420 print("\nDone: ", scalar(@result), " survivors\n");
426 ###############################################################################
438 %headers = get_headers
($msg);
443 log_headers
(%headers);
445 foreach $header (keys(%FILTERS)) {
446 $contents = $headers{lc($header)};
448 if (defined($contents)) {
449 foreach $regex (keys(%{$FILTERS{$header}})) {
450 $subst = $FILTERS{$header}->{$regex};
452 $try = '$contents =~';
453 $try .= " s|$regex|$subst|is";
469 foreach $msg (@msglist) {
470 $mhfolder = find_mh_folder
($msg);
471 store_message
($msg, $mhfolder);
480 $Data::Dumper
::Indent
= 1;
481 print(Dumper
(\
%FILTERS));
485 @msglist = kill_spam
(glob("$MAILDIR/new/*"));
488 filter_mail
(@msglist);
498 {'.*<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>.*'=>'l/$1/$2',
499 '.*<([[:graph:]]+?)(-list)?\.freedesktop\.org>.*'=>'l/freedesktop/$1',
500 '.*<swig\.cs\.uchicago\.edu>.*'=>'l/swig'},
503 {'<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>'=>'l/apache/$2/$1'},
508 The first List-Id filter is a surprisingly effective one which creates
509 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
510 all Sourceforge lists to which i subscribe. Certainly there are lists
511 hosted at Sourceforge which do not work well with this, and this is
512 where it is important to remember that the first match is the one
513 uses. Simply put the more specific rules before this one.
515 The next List-Id example is simple. The swig example demonstrates
516 that the folder name does not have to use a portion of the matched
517 header; you can just hardcode the folder name.
519 The List-Post example is a nice one. Most ASF projects have their own
520 apache.org subdomain, with mailing lists hosted there. So, given a
521 list such as dev@httpd.apache.org, this filter will create the folder
522 name l/apache/httpd/dev.
526 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
527 stolen from Adam Lazur <adam@lazur.org>.
529 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.