]> diplodocus.org Git - minc/blob - minc
2da6922c7be3b5530cd4014a88c4bce2c166b481
[minc] / minc
1 #! /usr/bin/env perl
2
3 # $Id$
4
5 =head1 NAME
6
7 B<minc> - incorporate mail from a maildir into mh folders
8
9 =head1 SYNOPSIS
10
11 B<minc> [B<-m> I<MAX>] [B<-n>]
12
13 B<minc> B<-d>
14
15 B<minc> B<-r>
16
17 =head1 DESCRIPTION
18
19 B<minc> incorporates mail from a maildir to a mh folder hierarchy. It
20 takes mail from a maildir folder (not a maildir folder hierarchy),
21 running each message through regular expression based filter and hook
22 functions to determine in which folder to store it or whether it's
23 spam. Post-processing hooks may be applied to each message.
24
25 As it processes each message, B<minc> prints a line for each message
26 similar to B<inc(1)> and B<scan(1)>. This line includes the folder
27 and message number in which the message was stored, the last 'From'
28 header, and the last 'Subject' header. These fields are truncated to
29 fit in the user's terminal (see COLUMNS in B<ENVIRONMENT> below) in
30 the following proportions: folder (0.1), message number (0.0625), from
31 header (0.175). Any of these may be overridden with $SCAN_P_FOLDER,
32 $SCAN_P_MESSAGE, or $SCAN_P_FROM. The subject always fills out the
33 rest of the line.
34
35 =cut
36
37 use strict;
38 use warnings;
39
40 $SIG{'PIPE'} = 'IGNORE';
41
42 use Data::Dumper;
43 use Errno;
44 use Fcntl qw(O_WRONLY O_EXCL O_CREAT);
45 use FileHandle;
46 use File::Temp qw(tempfile);
47 use File::stat;
48 use Getopt::Long qw(:config gnu_getopt no_ignore_case);
49 use POSIX qw(strftime WEXITSTATUS WIFEXITED);
50 use Pod::Usage;
51
52 our $VERSION = 1;
53
54 # If a filter set's header is $MAGIC_TO_TOKEN, that set is compared
55 # against headers matching this regex (taken from procmail).
56 my $MAGIC_TO_REGEX = '^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To)';
57 my $MAGIC_TO_TOKEN = ' TO';
58
59 # List of SPAM message numbers, scanned at the end so the user can
60 # check for false positives.
61 my @SPAM;
62
63 =head1 OPTIONS
64
65 =over 4
66
67 =item B<-d>
68
69 Dump (using Data::Dumper) the FILTERS list and exit. This is useful
70 for testing the syntax of .minc.
71
72 =item B<-h>
73
74 Show help.
75
76 =item B<-m> I<MAX>
77
78 Stop processing after I<MAX> messages.
79
80 =item B<-n>
81
82 Dry run; do not actually incorporate the mail, but log and report to
83 stdout/stderr as normal.
84
85 =item B<-r>
86
87 Rebuild `mhpath +`/.folders from scratch, processing no mail.
88
89 =back
90
91 =cut
92
93 my $dumpfilters;
94 my $help;
95 my $maxmsgs;
96 my $norun;
97 my $rebuild_dot_folders;
98
99 GetOptions(
100 'd' => \$dumpfilters,
101 'h|help' => \$help,
102 'm=i' => \$maxmsgs,
103 'n' => \$norun,
104 'r' => \$rebuild_dot_folders,
105 ) or pod2usage();
106 $help and pod2usage(-exitstatus=>0, -verbose=>1);
107 @ARGV == 0 or pod2usage();
108
109 our $run = !$norun;
110
111 =head1 ENVIRONMENT
112
113 =over 4
114
115 =item COLUMNS
116
117 How many columns the user's terminal can hold, used to print scan
118 lines for each processed message. Defaults to 80.
119
120 =item HOME
121
122 Where the configuration file (.minc) is found. Also,
123 $HOME/Maildir is used for the maildir if MAILDIR is not set.
124
125 =item MAILDIR
126
127 Where mail is delivered.
128
129 =back
130
131 =cut
132
133 use Env qw(COLUMNS HOME MAILDIR);
134
135 $COLUMNS ||= 80;
136
137 if (not $HOME) {
138 die("HOME environment variable must be set.\n");
139 }
140 if (not $MAILDIR) {
141 $MAILDIR = "$HOME/Maildir";
142 }
143
144 =head1 FILES
145
146 =over 4
147
148 =item $HOME/.minc
149
150 This file is Perl code (included via the 'require' directive) which
151 may define the FILTERS list, @start_hooks, @filter_hooks,
152 @post_store_hooks, and @stop_hooks.
153
154 =item `mhpath +`/.folders
155
156 B<minc> adds all folders it filters into to this file, which is used
157 by lukem's B<new(1)> (XXX need a link).
158
159 =item `mhpath +`/logs/minc.log
160
161 Where B<minc> logs what it does, unless in -n mode.
162
163 =item `mhpath +`/.minc.context
164
165 B<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 B<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 B<minc>, it is likely the new message B<minc> stores will not end up
172 marked unseen.
173
174 =back
175
176 =cut
177
178 our @FILTERS;
179 our (@start_hooks, @stop_hooks, @filter_hooks, @post_store_hooks);
180 our $SCAN_P_FOLDER = 0.1;
181 our $SCAN_P_MESSAGE = 0.0625;
182 our $SCAN_P_FROM = 0.175;
183 our @folder_sort_list = (qr/^inbox$/);
184 our $folder_sorter = sub { sort_by_list(@_, @folder_sort_list) };
185
186 my $mh;
187
188 $mh = `mhpath +`;
189 chomp($mh);
190
191 my $logfile = $mh . '/logs/minc.log';
192
193 $ENV{"MHCONTEXT"} = $mh . '/.minc.context';
194
195 \f
196 ###############################################################################
197 # Logging
198
199 sub mylog {
200 $run or return;
201
202 my $timestamp = strftime('%b %e %H:%M:%S', localtime);
203 my $msg;
204 foreach my $part (@_) {
205 if (defined($part)) {
206 $msg .= $part;
207 }
208 }
209 # no newlines in the log message, thanks
210 $msg =~ s/\n/ /gm;
211
212 open(LOG, ">>$logfile") or die("open(>>$logfile): $!");
213 print(LOG "$timestamp $msg\n") or die("print(>>$logfile): $!");
214 close(LOG) or die("close($logfile): $!");
215 }
216
217 sub logheader {
218 my ($text, @contents) = @_;
219 my $last;
220
221 if (@contents) {
222 $last = $contents[-1];
223 } else {
224 $last = '';
225 }
226
227 mylog('<< ', $text, $last);
228 }
229
230 sub log_headers {
231 my %headers = @_;
232
233 # For an explanation of the %headers structure, see the
234 # get_headers function below.
235 logheader('From: ', @{$headers{'return-path'}});
236 logheader('To: ', @{$headers{'to'}});
237 logheader('Subject: ', @{$headers{'subject'}});
238 logheader('Message-Id: ', @{$headers{'message-id'}});
239 }
240
241 \f
242 ###############################################################################
243 # Utility procedures
244
245 sub sort_by_list {
246 my $a = shift;
247 my $b = shift;
248
249 for my $i (@_) {
250 my $am = $a =~ $i;
251 my $bm = $b =~ $i;
252 if ($am) {
253 if ($bm) {
254 last;
255 }
256 return -1;
257 } elsif ($bm) {
258 return 1;
259 }
260 }
261
262 return $a cmp $b;
263 }
264
265 sub mkfolder {
266 my $folder = shift;
267 my $target;
268 my $component;
269
270 $target = $mh;
271 foreach $component (split('/', $folder)) {
272 $target = join('/', $target, $component);
273 -d $target or mkdir($target) or die("mkdir($target): $!");
274 }
275 }
276
277 sub getfiles {
278 my $dir = shift;
279 my @result;
280
281 if (not opendir(DIR, $dir)) {
282 die("opendir($dir): $!");
283 }
284
285 # Initialize $! to 0 (success) because stupid stupid Perl provides
286 # no way to distinguish an error from an empty directory; that
287 # means setting $! to 0 and testing it afterwards is the only way
288 # to detect an error. Real Programmers don't handle errors,
289 # right? >sigh<
290 $! = 0;
291 @result = readdir(DIR);
292 if ($! != 0) {
293 die("readdir($dir): $!");
294 }
295
296 if (@result <= 2) {
297 return ();
298 }
299
300 STDOUT->autoflush(1);
301 print(@result - 2, " messages...");
302
303 closedir(DIR);
304
305 return @result;
306 }
307
308 my %msgnum_cache;
309 sub get_highest_msgnum {
310 my $mhfolder = shift;
311 my $dir;
312 my @list;
313 my $highest;
314 my $msgnum;
315
316 if (defined($msgnum_cache{$mhfolder})) {
317 return $msgnum_cache{$mhfolder}++;
318 }
319
320 $dir = "$mh/$mhfolder";
321 if (not opendir(DIR, $dir)) {
322 die("opendir($dir): $!");
323 }
324
325 # Insert rant from getfiles here.
326 $! = 0;
327 @list = readdir(DIR);
328
329 if ($! != 0) {
330 die("readdir($dir): $!");
331 }
332
333 closedir(DIR);
334
335 $highest = 0;
336 foreach $msgnum (@list) {
337 # Look for integers.
338 if ($msgnum =~ /^[0-9]+$/) {
339 if ($msgnum > $highest) {
340 $highest = $msgnum;
341 }
342 }
343 }
344
345 $msgnum_cache{$mhfolder} = $highest;
346 return $msgnum_cache{$mhfolder}++;
347 }
348
349 sub mark {
350 my $folder = shift;
351 my $msgnum = shift;
352 my $seq = shift;
353 my $fn = "$mh/$folder/.mh_sequences";
354 my $fh;
355 my @sequences;
356
357 if (not open($fh, $fn)) {
358 $!{ENOENT} or die("open($fn): $!");
359 } else {
360 @sequences = <$fh>;
361 chomp(@sequences);
362 }
363
364 my $marked = 0;
365 open($fh, '>', $fn) or die("open(>$fn): $!");
366 for $_ (@sequences) {
367 if (/^$seq: (.*)/) {
368 my @parts;
369 my @result;
370 my $done = 0;
371 for my $part (split(' ', $1)) {
372 if (not $done) {
373 my ($st, $en) = split('-', $part);
374 if ((defined($en) and ($msgnum >= $st and $msgnum <= $en))
375 or $msgnum == $st) {
376 # It's already there.
377 $done = 1;
378 }
379 if (defined($en)) {
380 if ($st - 1 == $msgnum) {
381 $part = "$msgnum-$en";
382 $done = 1;
383 } elsif ($en + 1 == $msgnum) {
384 $part = "$st-$msgnum";
385 $done = 1;
386 }
387 } else {
388 if ($part - 1 == $msgnum) {
389 $part = "$msgnum-$part";
390 $done = 1;
391 } elsif ($part + 1 == $msgnum) {
392 $part = "$part-$msgnum";
393 $done = 1;
394 }
395 }
396 }
397 push(@result, $part);
398 }
399 if (not $done) {
400 push(@result, $msgnum);
401 }
402 print($fh "$seq: ", join(' ', @result), "\n");
403 $marked = 1;
404 } else {
405 print($fh "$_\n");
406 }
407 }
408 $marked or print($fh "$seq: $msgnum\n");
409 close($fh) or die("close(>$fn): $!");
410 }
411
412 sub store_message {
413 my $msg = shift;
414 my $mhfolder = shift;
415 my $msgnum;
416 my $try;
417 my $mhmsg;
418 my $status;
419
420 # We must do this even in -n mode because later steps fail without
421 # it. This should be harmless.
422 mkfolder($mhfolder);
423
424 # This loop is a modified version of the maildir delivery algorithm.
425 $msgnum = get_highest_msgnum($mhfolder);
426 for ($try = 0; ; $try++) {
427 $msgnum++;
428 $mhmsg = "$mh/$mhfolder/$msgnum";
429
430 if (not stat($mhmsg)) {
431 if ($!{ENOENT}) {
432 # Now we have a non-existent file, let's try to create
433 # it. We must create a zero-byte file first because a
434 # file my appear between our happy stat results and
435 # our later rename(2), which would clobber said file.
436 # So attempt to create a file with this name. If it
437 # succeeds, in just a bit here we'll knowingly clobber
438 # this file with the rename(2) call.
439
440 # Another way to do this is not to use rename(2), but
441 # use link(2) + unlink(2) instead. That's how the
442 # standard maildir algorithm does it. Each method has
443 # a disadvantage: the program may crash between the
444 # link(2) and unlink(2) calls. With the standard
445 # maildir algorithm, that means the message will end
446 # up duplicated. The advantage of creating an empty
447 # file followed by rename(2) is that an extra empty
448 # file is left behind as opposed to a duplicate
449 # message. This is more easily detected by the user.
450 if ($run) {
451 if (sysopen(MSG, $mhmsg,
452 O_WRONLY | O_EXCL | O_CREAT, 0600)) {
453 close(MSG);
454 last;
455 }
456 } else {
457 last;
458 }
459 }
460 }
461
462 # This algorithm is different from the maildir one; let's make
463 # 10 tries instead of 3.
464 if ($try == 9) {
465 die("Attempted filename $mhmsg exists.");
466 }
467
468 # This algorithm is different; i don't think we need to sleep.
469 #sleep(2);
470 }
471
472 mylog('+', $mhfolder);
473
474 if ($run) {
475 if (not rename($msg, $mhmsg)) {
476 die("rename($msg, $mhmsg): $!");
477 }
478
479 # Mark each message as soon as we store it and bomb if that
480 # fails. While it is slow, it is not safe to store multiple
481 # messages and then have a failure before marking some (or
482 # all).
483 if ($mhfolder eq 'SPAM') {
484 push(@SPAM, $msgnum);
485 } else {
486 mark($mhfolder, $msgnum, 'unseen');
487 }
488 }
489
490 return $msgnum;
491 }
492
493 # Parse a message file into a structure describing the headers. The
494 # structure is a hash of arrays. The hash keys are the names of the
495 # headers found in the message, made all lower-case. Each item in
496 # the hash is an array of header text. The array has one element
497 # per occurrence of the header. Most headers will only have a single
498 # element because they occur only once. The Received header is the
499 # most common header with multiple texts.
500 sub get_headers {
501 my $msg = shift;
502 my %headers;
503 my $current; # current header, used for unfolding lines
504 my $fieldname; # unmolested header name
505 my $contents; # contents of header
506
507 open(MSG, $msg) or die("open(MSG, $msg): $!");
508 while (<MSG>) {
509 chomp;
510 if (length == 0) {
511 last;
512 }
513
514 if (/^\s/) {
515 # folded header continuation
516
517 if (not defined($current)) {
518 warn('Malformed message, cannot parse headers.');
519 next;
520 }
521
522 @{$headers{$current}}[-1] .= $_;
523 } else {
524 ($fieldname) = split(/:/);
525 $current = lc($fieldname);
526 (undef, $contents) = split(/^\Q$fieldname\E:\s*/);
527
528 if (defined($headers{$current})) {
529 # multiple occurence of same header
530 push(@{$headers{$current}}, $contents);
531 } else {
532 # first occurence of a header
533 $headers{$current} = [$contents];
534 }
535 }
536 }
537 close(MSG);
538
539 return %headers;
540 }
541
542 \f
543 ###############################################################################
544 # Filtering
545
546 sub find_mh_folder {
547 my $msg = shift;
548 my %headers = @_;
549 my $filterref;
550 my @filter;
551 my $header;
552 my $contents;
553 my $pair;
554 my $regexp;
555 my $expression;
556 my $result;
557
558 if (not %headers) {
559 return 'malformed';
560 }
561
562 # Walk the list of filters. This structure is documented in
563 # pod at the end of the program.
564 foreach $filterref (@FILTERS) {
565 @filter = @$filterref;
566 $header = shift(@filter);
567
568 # Handle filters using the magic TO header specially.
569 if ($header eq $MAGIC_TO_TOKEN) {
570 foreach $header (keys(%headers)) {
571 if ($header =~ /$MAGIC_TO_REGEX/i) {
572 foreach $contents (@{$headers{lc($header)}}) {
573 foreach $pair (@filter) {
574 ($regexp, $expression) = @$pair;
575 if ($contents =~ $regexp) {
576 if (eval "\$result = \"$expression\"") {
577 return $result;
578 }
579 }
580 }
581 }
582 }
583 }
584
585 # Now that it's been processed specially, skip normal handling.
586 next;
587 }
588
589 # Walk the list of message headers matching the filter's
590 # specified header.
591 foreach $contents (@{$headers{lc($header)}}) {
592 # Walk the filter's list of regexp/expression pairs.
593 foreach $pair (@filter) {
594 ($regexp, $expression) = @$pair;
595 if ($contents =~ $regexp) {
596 if (eval "\$result = \"$expression\"") {
597 return $result;
598 }
599 }
600 }
601 }
602 }
603
604 return 'inbox';
605 }
606
607 sub filter_mail {
608 @_ or return ();
609 my $msgcount = @_ - 2; # don't count . and ..
610 my $len = length($msgcount);
611 my @baton;
612 my $msg;
613 my $mhfolder;
614 my $spam = 0;
615 my $saved = 0;
616 my $msgnum;
617 my %folders;
618
619 if (-f "$HOME/.minc") {
620 require "$HOME/.minc";
621 }
622
623 # XXX lame names
624 my $nf = int($COLUMNS * $SCAN_P_FOLDER);
625 my $nm = int($COLUMNS * $SCAN_P_MESSAGE);
626 my $nF = int($COLUMNS * $SCAN_P_FROM);
627 my $ns = $COLUMNS - $nf - $nm - $nF - 3;
628
629 my %batons;
630 for my $hook (@start_hooks) {
631 my ($handle, @baton) = $hook->();
632 if (defined($handle)) {
633 $batons{$handle} = [@baton];
634 }
635 }
636
637 for $msg (@_) {
638 ($msg eq '.' or $msg eq '..') and next;
639
640 my %headers = get_headers($msg);
641 log_headers(%headers);
642
643 undef($mhfolder);
644 for my $hook (@filter_hooks) {
645 my $result = $hook->(\%batons, \%headers, $msg);
646 defined($result) and ($mhfolder = $result);
647 }
648
649 defined($mhfolder) or ($mhfolder = find_mh_folder($msg, %headers));
650
651 $msgnum = store_message($msg, $mhfolder);
652 $folders{$mhfolder}++;
653
654 if ($mhfolder eq 'SPAM') {
655 $spam++;
656 print("\r");
657 } else {
658 $saved++;
659 my $from = '';
660 my $subject = '';
661 # Sometimes these headers are missing...
662 eval { $from = [@{$headers{'from'}}]->[-1] };
663 eval { $subject = [@{$headers{'subject'}}]->[-1] };
664 for ($from, $subject) {
665 tr/\x00-\x1f\x80-\xff/ /;
666 }
667 printf("\r\%-${nf}s \%${nm}d \%-${nF}s \%s\n",
668 substr($mhfolder, 0, $nf), substr($msgnum, 0, $nm),
669 substr($from, 0, $nF),
670 substr($subject, 0, $ns));
671 }
672
673 for my $hook (@post_store_hooks) {
674 $hook->(\%batons, \%headers, $mhfolder, $msgnum);
675 }
676
677 printf(" \%${len}d SPAM \%${len}d saved \%${len}d/%1d",
678 $spam, $saved, $spam + $saved, $msgcount);
679
680 defined($maxmsgs) and ($spam + $saved < $maxmsgs or last);
681 }
682 print("\n");
683
684 for my $hook (@stop_hooks) {
685 $hook->(\%batons);
686 }
687
688 return %folders;
689 }
690
691 sub build_dot_folders {
692 my $folders = shift;
693 my $fh = shift;
694 my $fn;
695
696 if (defined($fh)) {
697 while (<$fh>) {
698 chomp;
699 $folders->{$_}++;
700 }
701 }
702
703 eval { ($fh, $fn) = tempfile("$mh/.folders.XXXXX") };
704 if ($@) {
705 warn("$@");
706 return;
707 }
708
709 for my $folder (sort { $folder_sorter->($a,$b) } keys(%$folders)) {
710 print($fh "$folder\n");
711 }
712
713 if (not close($fh)) {
714 warn("close($fn): $!");
715 unlink($fn) or warn("unlink($fn): $!");
716 return;
717 }
718
719 rename($fn, "$mh/.folders") or warn("rename($fn, $mh/.folders): $!");
720 }
721
722 sub create_dot_folders {
723 if (-f "$HOME/.minc") {
724 require "$HOME/.minc";
725 }
726
727 my %folders;
728 open(my $fh, '-|', 'folders', '-fast', '-recur')
729 or die("open(folders|): $!");
730 build_dot_folders(\%folders, $fh);
731 return 0;
732 }
733
734 sub update_dot_folders {
735 my $folders = shift;
736 my $fh;
737
738 if (not open($fh, "$mh/.folders") and not $!{ENOENT}) {
739 # For ENOENT, we go ahead and create it, else we error and
740 # don't clobber it.
741 warn("open($mh/.folders): $!");
742 return;
743 }
744
745 build_dot_folders($folders, $fh);
746 }
747
748 # XXX Could use some unification with getfiles...
749 sub maildir_spam {
750 my $dir = "$MAILDIR/spam/new";
751
752 if (not chdir($dir)) {
753 $!{ENOENT} or print(STDERR "skipping maildir spam: chdir($dir): $!\n");
754 return;
755 }
756
757 if (not opendir(DIR, '.')) {
758 print(STDERR "skipping maildir spam: opendir($dir): $!\n");
759 return;
760 }
761
762 $! = 0;
763 my @spams = readdir(DIR);
764 if ($! != 0) {
765 print(STDERR "skipping maildir spam: readdir($dir): $!\n");
766 return;
767 }
768
769 closedir(DIR);
770
771 for my $msg (@spams) {
772 ($msg eq '.' or $msg eq '..') and next;
773 store_message($msg, 'SPAM');
774 }
775 }
776
777 \f
778 MAIN: {
779 my $st;
780
781 if ($dumpfilters) {
782 require "$HOME/.minc";
783 $Data::Dumper::Indent = 1;
784 print(Dumper(\@FILTERS));
785 exit;
786 }
787
788 $rebuild_dot_folders and exit(create_dot_folders);
789
790 chdir("$MAILDIR/new") or die("chdir($MAILDIR/new): $!");
791 my %folders = filter_mail(map { $_->[1] }
792 sort { $a->[0] <=> $b->[0] }
793 map {
794 if (not ($st = stat($_))) {
795 die("stat($_): $!");
796 }
797 [$st->mtime, $_]
798 }
799 getfiles('.'));
800
801 $run and %folders and update_dot_folders(\%folders);
802
803 maildir_spam();
804
805 @SPAM and (exec('scan', '+SPAM', @SPAM) or die);
806 }
807
808 \f
809 __END__
810
811 =head1 THE FILTERS STRUCTURE
812
813 Each element of @FILTERS is a filter. A filter is itself an array.
814 The first element of a filter is a string, the name of the header this
815 filter acts upon. The header name is not case-sensitive. Each
816 subsequent element of a filter is a pair (i.e. a two-element array):
817 first, a regular expression B<minc> uses to determine whether this
818 filter matches or not, and second, an expression which B<minc>
819 evaluates to get the folder name.
820
821 B<minc> decides where to store a message by iterating over the
822 @FILTERS array. It tests each regexp of each filter against all
823 headers matching that filter's specified header. As soon as a match
824 is found, B<minc> evaluates the second part of the pair. This part
825 may contain positional parameters from the matched regexp ($1, $2,
826 etc.). The result of this expression is used as the folder name.
827
828 Multiple occurrences of the same header are preserved and tested
829 individually against the filters. That means, for example, that
830 filters can search all the Received headers for a certain string.
831
832 It is important to note that all the arrays of the @FILTERS structure
833 are traversed I<in order>. This means the filters can be arranged so
834 that some have priority over others. XXX: get Doug to write an
835 example of having the same header matched more than once.
836
837 Lastly, B<minc> supports a magic ' TO' header. Filters using this
838 header are matched against a collection of headers related to (and
839 including) the To. Which headers to use is determined by a regular
840 expression borrowed from procmail.
841
842 =head1 HOOKS
843
844 Filter hooks take a reference to a hash of batons, a reference to a
845 hash of headers, and the message filename as arguments. It returns
846 undef to decline filtering of this message (thus falling back to
847 subsequent filter hooks, and finally @FILTERS), or the name of the
848 folder to store this message into.
849
850 One of B<minc>'s global variables is available to the user-defined
851 hooks: $run. This boolean should be honored; hooks should only take
852 real action (i.e. removing or creating files, running external
853 programs, etc.) if $run is non-zero.
854
855 The baton hash is created simply from the start hooks; if the hook
856 returns at least one defined value, this value is used as the key and
857 all other return values are put into a list reference as the value.
858 This hash is then passed by reference to all filter, post-store, and
859 stop hooks.
860
861 Post store hooks take a reference to a hash of batons, a reference to
862 a hash of headers, the folder this message was stored in, and its new
863 message number.
864
865 XXX: need more details about the hook process; for now read the code.
866
867 =head1 EXAMPLES
868
869 @FILTERS = (
870
871 ['List-Id',
872 ['<([[:graph:]]+)-([[:graph:]]+)\.lists\.sourceforge\.net>', 'l/$1/$2'],
873 ['<([[:graph:]]+?)(-list)?\.freedesktop\.org>', 'l/freedesktop/$1'],
874 ['<swig\.cs\.uchicago\.edu>', 'l/swig'],
875 ],
876
877 ['List-Post',
878 ['<mailto:([[:graph:]]+)+@([[:graph:]]+)\.apache\.org>', 'l/apache/$2/$1'],
879 ],
880
881 ['To',
882 ['root', 'root'],
883 ],
884 );
885
886 The first List-Id filter is a surprisingly effective one which creates
887 appropriate folder names (such as l/htdig/updates and l/rox/devel) for
888 all Sourceforge lists to which i subscribe. Certainly there are lists
889 hosted at Sourceforge which do not work well with this, and this is
890 where it is important to remember that the first match is the one
891 uses. Simply put the more specific rules before this one.
892
893 The next List-Id example is simple. The swig example demonstrates
894 that the folder name does not have to use a portion of the matched
895 header; you can just hardcode the folder name.
896
897 The List-Post example is a nice one. Most ASF projects have their own
898 apache.org subdomain, with mailing lists hosted there. So, given a
899 list such as dev@httpd.apache.org, this filter will create the folder
900 name l/apache/httpd/dev.
901
902 XXX Need hook examples.
903
904 =head1 AUTHORS
905
906 Written by Eric Gillespie <epg@pretzelnet.org>. Design by Eric
907 Gillespie and Doug Porter <dsp@waterspout.com>.
908
909 This program is free software; you can redistribute it and/or modify
910 it under the same terms as Perl itself.
911
912 =cut
913
914 # Local variables:
915 # cperl-indent-level: 4
916 # perl-indent-level: 4
917 # indent-tabs-mode: nil
918 # End:
919
920 # vi: set tabstop=4 expandtab: