]>
diplodocus.org Git - minc/blob - minc
04a7d68409c06946b19fc2b1073382b10dd245c1
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.
163 =item `mhpath +`/.minc.context
165 minc uses this file for context (i.e. current folder) instead of
166 `mhpath +`/context. This allows you some amount of freedom to use mh
167 while minc is running. For example, you can changed folders without
168 causing a message to be stored in the wrong folder. Care must still
169 be taken, however, about the unseen sequence; if you change it
170 (whether via show, or mark, or anything else) while in the same folder
171 as minc, it is likely the new message minc stores will not end up
177 tie
(%FILTERS, 'Tie::IxHash');
178 require "$HOME/.mincfilter";
180 if (-f
"$HOME/.mincspam") {
181 require "$HOME/.mincspam";
195 $logfile = $mh . '/logs/minc.log';
197 $logfile = $mh . '/logs/dryrun.log';
200 $ENV{"MHCONTEXT"} = $mh . '/.minc.context';
203 ###############################################################################
206 # debuglevels for the logger
207 use constant LOG_DEBUGLEVEL
=> 'info';
208 use constant SCREEN_DEBUGLEVEL
=> 'debug';
210 # characters used to wrap around the id field in the log so I can more
211 # easily parse the msg flow
212 use constant DEBUGCHAR
=> '%';
213 use constant INFOCHAR
=> '=';
214 use constant INCOMINGCHAR
=> '<';
215 use constant SAVECHAR
=> '>';
216 use constant WARNCHAR
=> 'W';
217 use constant ERRORCHAR
=> 'E';
219 my $logger = new Log
::Dispatch
;
220 $logger->add(new Log
::Dispatch
::File
(name
=>'logfile',
225 # log alias, handles getting passed vars that are undef
229 my $timestamp = strftime
('%b %e %H:%M:%S', localtime);
231 foreach my $part (@_) {
232 if (defined($part)) {
236 # no newlines in the log message, thanks
238 $msg = sprintf("%s %s%s %s\n", $timestamp, $act, $act, $msg);
239 $logger->log(level
=>$level, message
=>$msg);
241 if ($act eq SAVECHAR
) {
242 $logger->log(level
=>$level, message
=>"\n");
246 sub logincoming
{ mylog
('info', INCOMINGCHAR
, @_); }
247 sub logsave
{ mylog
('notice', SAVECHAR
, @_); }
248 sub loginfo
{ mylog
('info', INFOCHAR
, @_); }
249 sub logdebug
{ mylog
('debug', DEBUGCHAR
, @_); }
250 sub logwarn
{ mylog
('warning', WARNCHAR
, @_); }
251 sub logerr
{ mylog
('error', ERRORCHAR
, @_); }
256 logincoming
('From: ', $headers{'return-path'});
257 logincoming
('To: ', $headers{'to'});
258 logincoming
('Subject: ', $headers{'subject'});
259 logincoming
('Message-Id: ', $headers{'message-id'});
263 ###############################################################################
267 printf('%s:%s', __FILE__
, __LINE__
);
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 unseen -add");
449 if (not WIFEXITED
($status)) {
450 err
(&EX_OSERR
, "Failed to run mark");
451 } elsif (WEXITSTATUS
($status) != 0) {
452 errx
(&EX_SOFTWARE
, "Failed to mark message unseen.");
457 if (not $FOLDERS{$mhfolder}) {
458 print("+$mhfolder\n");
459 $FOLDERS{$mhfolder} = 1;
466 my $current; # current header, used for unfolding lines
467 my $fieldname; # unmolested header name
477 # folded header continuation
479 if (not defined($current)) {
480 print("Malformed message, cannot parse headers.\n");
484 $headers{$current} .= $_;
486 ($fieldname) = split(/:/);
487 $current = lc($fieldname);
488 (undef, $headers{$current}) = split(/^\Q$fieldname\E:\s*/);
496 ###############################################################################
508 foreach $msg (@msglist) {
509 printf('%sChecking for spam... %6d/%d',
510 "\r", ++$i, scalar(@msglist));
513 store_message
($msg, 'SPAM');
518 print("\nDone: ", scalar(@result), " survivors\n");
524 ###############################################################################
536 %headers = get_headers
($msg);
541 log_headers
(%headers);
543 foreach $header (keys(%FILTERS)) {
544 $contents = $headers{lc($header)};
546 if (defined($contents)) {
547 foreach $regex (keys(%{$FILTERS{$header}})) {
548 $subst = $FILTERS{$header}->{$regex};
550 $try = '$contents =~';
551 $try .= " s|$regex|$subst|is";
567 foreach $msg (@msglist) {
568 $mhfolder = find_mh_folder
($msg);
569 store_message
($msg, $mhfolder);
578 $Data::Dumper
::Indent
= 1;
579 print(Dumper
(\
%FILTERS));
583 @msglist = getfiles
("$MAILDIR/new");
585 if (not $filteronly) {
586 @msglist = kill_spam
(@msglist);
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.
626 For an example is_spam function, see
627 L<http:E<047>E<047>pretzelnet.orgE<047>cvsE<047>dotfilesE<047>.mincspam>
631 Written by Eric Gillespie <epg@pretzelnet.org> with logging code
632 stolen from Adam Lazur <adam@lazur.org>.
634 Design by Eric Gillespie and Doug Porter <dsp@waterspout.com>.