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