3 # replyfilter - A reply filter for nmh
5 # The idea behind this program is that it will act as a format filter
6 # for nmh. It will try to extract out all text/plain parts and format
7 # them if necessary using a filter program.
9 # To use this program, configure nmh in the following way (nmh 1.5 or later):
11 # - Put the path to this program in your .mh_profile under formatproc:
13 # formatproc: replyfilter
15 # - Create a mhl reply filter that consists of the following line:
17 # body:nocomponent,format,nowrap,formatarg="%(trim{content-type})%(putstr)",formatarg="%(trim{content-transfer-encoding})%(putstr)",formatarg=">"
19 # To decode this a bit:
21 # body - Output the "body" component
22 # nocomponent - Don't output a component prefix (normally here we use a
23 # component prefix of ">" as a quote character, but we're
24 # going to have replyfilter do that).
25 # nowrap - Don't wrap lines if they exceed the column width
26 # formatarg - Arguments to fmtproc. The first argument is the value of
27 # the Content-type header; the second is the value of the
28 # Content-Transfer-Encoding header. The last "formatarg"
29 # is used as your quoting prefix. Replace it with whatever
35 use MIME::QuotedPrint;
40 # The program we use to format "long" text
43 $filterprogram = 'par';
46 # Our output character set. This script assumes a UTF-8 locale, but if you
47 # want to run under a different locale the change it here.
50 $outcharset = 'utf-8';
53 # Maximum column width (used by the HTML converter and to decide if we need
54 # to invoke the filter program
60 # Out HTML converter program & arguments
63 @htmlconv = ('w3m', '-dump', '-cols', $maxcolwidth - 2, '-T', 'text/html',
67 die "Usage: $0 Content-type content-transfer-encoding quote-prefix\n"
71 my $ctype = Mail::Field->new('Content-Type', $ARGV[0]);
72 $content_type = $ctype->type;
73 $charset = $ctype->charset;
74 $boundary = $ctype->boundary;
76 $content_type = 'text/plain';
77 $charset = 'us-ascii';
80 $encoding = $ARGV[1] eq "" ? '7bit' : lc($ARGV[1]);
81 $quoteprefix = $ARGV[2];
84 # Set up our output to be in our character set
87 binmode(STDOUT, ":encoding($outcharset)");
90 # The simplest case: if we have a single type of text/plain, send it
91 # to our format subroutine.
94 if ($content_type eq 'text/plain') {
95 process_text(\*STDIN, $encoding, $charset);
100 # Alright, here's what we need to do.
102 # Find any text/plain parts and decode them. Decode them via base64 or
103 # quoted-printable, and feed them to our formatting filter when appropriate.
104 # Put markers in the output for other content types.
107 ($type) = (split('/', $content_type));
109 if ($type eq 'multipart') {
112 # For multipart messages we have to do a little extra.
113 # Eat the MIME prologue (everything up until the first boundary)
116 if (! defined $boundary || $boundary eq '') {
117 print "No boundary in Content-Type header!\n";
123 last if match_boundary($_, $boundary);
127 print "Unable to find boundary in message\n";
134 process_part(\*STDIN, $content_type, $encoding, $charset, $boundary);
138 # Eat the MIME epilog
146 # Handled encoded text. I think we can assume if the encoding is q-p
147 # or base64 to feed it into a formatting filter.
150 sub process_text (*$$;$)
152 my ($input, $encoding, $charset, $boundary) = @_;
153 my $text, $filterpid, $prefixpid, $finread, $finwrite;
154 my $foutread, $foutwrite, $decoder, $ret, $filterflag;
155 my @text = ( '' ), $maxline = 0;
158 # In the simple case, just spit out the text prefixed by the
162 if ($encoding eq '7bit' || $encoding eq '8bit') {
164 # Switch the character set to whatever is specified by
167 binmode($input, ":encoding($charset)");
169 $ret = match_boundary($_, $boundary);
171 binmode($input, ':encoding(us-ascii)');
174 print $quoteprefix, $_;
179 # If we've got some other encoding, the input text is almost
183 binmode($input, ':encoding(us-ascii)');
185 $decoder = find_decoder($encoding);
186 if (! defined $decoder) {
192 # Okay, assume that the encoding will make it so that we MIGHT need
193 # to filter it. Read it in; if it's too long, filter it.
196 my $chardecode = find_encoding($charset);
201 last if ($ret = match_boundary($_, $boundary));
203 @lines = split(/^/, $chardecode->decode(&$decoder($_)));
205 if (substr($text[$#text], -1, 1) eq "\n") {
206 push @text, shift @lines;
208 $text[$#text] .= shift @lines;
210 if (($len = length($text[$#text])) > $maxline) {
219 binmode($input, ':encoding(us-ascii)');
221 if (! defined $ret) {
225 if ($maxline <= $maxcolwidth) {
227 # These are short enough; just output it now as-is
229 foreach my $line (@text) {
230 print STDOUT $quoteprefix, $line;
236 # We fork a copy of ourselves to read the output from the filter
237 # program and prefix the quote character.
240 pipe($finread, $finwrite) || die "pipe() failed: $!\n";
241 pipe($foutread, $foutwrite) || die "pipe() (second) failed: $!\n";
243 binmode($finread, ":encoding($outcharset)");
244 binmode($finwrite, ":encoding($outcharset)");
245 binmode($foutread, ":encoding($outcharset)");
246 binmode($foutwrite, ":encoding($outcharset)");
248 if ($filterpid = fork) {
250 # Close the pipes in the parent that we're not using
255 } elsif (defined $filterpid) {
257 # Close our ununsed filehandles
264 # Dup() down the filehandles to standard input and output
267 open(STDIN, "<&", $finread) ||
268 die "dup(filterin) failed: $!\n";
269 open(STDOUT, ">&", $foutwrite) ||
270 die "dup(filterout) failed: $!\n";
283 exec $filterprogram ||
284 die "Unable to exec $filterprogram: $!\n";
286 die "Fork for $filterprogram failed: $!\n";
290 # Fork our output handler.
293 if ($prefixpid = fork) {
295 # We don't need these anymore
299 } elsif (defined $prefixpid) {
301 # Read from foutwrite, and output (with prefix) to stdout
306 while (<$foutread>) {
307 print STDOUT $quoteprefix, $_;
314 # Send our input to the filter program
317 print $finwrite @text;
320 waitpid $filterpid, 0;
321 warn "Filter process exited with ", ($? >> 8), "\n" if $?;
322 waitpid $prefixpid, 0;
323 warn "Pipe reader process exited with ", ($? >> 8), "\n" if $?;
329 # Filter HTML through a converter program
332 sub process_html (*$$;$)
334 my ($input, $encoding, $charset, $boundary) = @_;
335 my $filterpid, $prefixpid, $finread, $finwrite;
336 my $foutread, $foutwrite, $decoder, $ret;
338 if (! defined($decoder = find_decoder($encoding))) {
343 # We fork a copy of ourselves to read the output from the filter
344 # program and prefix the quote character.
347 pipe($finread, $finwrite) || die "pipe() failed: $!\n";
348 pipe($foutread, $foutwrite) || die "pipe() (second) failed: $!\n";
350 binmode($finread, ":encoding($outcharset)");
351 binmode($finread, ":encoding($outcharset)");
352 binmode($foutread, ":encoding($outcharset)");
353 binmode($foutwrite, ":encoding($outcharset)");
355 if ($filterpid = fork) {
357 # Close the pipes in the parent that we're not using
362 } elsif (defined $filterpid) {
364 # Close our ununsed filehandles
371 # Dup() down the filehandles to standard input and output
374 open(STDIN, "<&", $finread) ||
375 die "dup(filterin) failed: $!\n";
376 open(STDOUT, ">&", $foutwrite) ||
377 die "dup(filterout) failed: $!\n";
391 die "Unable to exec $filterprogram: $!\n";
393 die "Fork for $htmlconv[0] failed: $!\n";
397 # Fork our output handler.
400 if ($prefixpid = fork) {
402 # We don't need these anymore
406 } elsif (defined $prefixpid) {
408 # Read from foutwrite, and output (with prefix) to stdout
413 while (<$foutread>) {
414 print STDOUT $quoteprefix, $_;
421 # Send our input to the filter program
425 last if ($ret = match_boundary($_, $boundary));
426 print $finwrite (&$decoder($_));
429 if (! defined $ret) {
434 waitpid $filterpid, 0;
435 warn "HTML converter process exited with ", scalar($? >> 8), "\n" if $?;
436 waitpid $prefixpid, 0;
437 warn "Pipe reader process exited with ", $? >> 8, "\n" if $?;
443 # Decide what to do, based on what kind of content it is.
446 sub process_part (*$$$$;$)
448 my ($input, $content_type, $encoding, $charset, $boundary, $name) = @_;
449 my ($type, $subtype) = (split('/', $content_type, -1), '');
451 if ($type eq 'text') {
453 # If this is a text part, right now we only deal with
454 # plain and HTML parts.
456 if ($subtype eq 'plain') {
457 return process_text($input, $encoding, $charset,
459 } elsif ($subtype eq 'html') {
460 return process_html($input, $encoding, $charset,
463 print ">>> $content_type content\n";
464 return eat_part($input, $boundary);
466 } elsif ($type eq 'multipart') {
467 return process_multipart($input, $subtype, $boundary);
470 # Other types we're not sure what to do with right now
471 # Just put a marker in there
474 print ">>> $content_type attachment";
476 print ", name=$name";
480 return eat_part($input, $boundary);
485 # Process a multipart message.
487 # When called, we should be right after the beginning of the first
488 # boundary marker. So we should be pointed at header lines which describe
489 # the content of this part
492 sub process_multipart ($$$)
494 my ($input, $subtype, $boundary) = @_;
498 my $encoding, $type, $end, $name, $charset;
501 # Use the Mail::Header package to read in any headers
502 # corresponding to this part
505 my $head = Mail::Header->new($input, (MailFrom => 'IGNORE'));
508 # Extract out any Content-Type, Content-Transfer-Encoding, and
509 # Content-Disposition headers
512 my $ctype = Mail::Field->extract('Content-Type', $head);
513 my $cte = Mail::Field->extract('Content-Transfer-Encoding',
515 my $cdispo = Mail::Field->extract('Content-Disposition', $head);
517 if (defined $ctype) {
518 $type = $ctype->type;
519 $charset = $ctype->charset;
521 $type = 'text/plain';
522 $charset = 'us-ascii';
525 $encoding = defined $cte ? $cte->param('_') : '7bit';
526 $name = defined $cdispo ? $cdispo->param('filename') : undef;
529 # Special handling for multipart/alternative; pick
530 # the "first" one we can handle (which is usually
531 # text/plain) and silently eat the rest, but output a
532 # warning if we can't handle anything.
536 $end = eat_part($input, $boundary);
538 my $subboundary = $boundary;
539 my $maintype = (split('/', $type))[0];
541 if ($maintype eq 'multipart') {
542 $subboundary = $ctype->boundary;
544 # Go until we find our beginning of this
547 my $subend = eat_part($input, $subboundary);
549 if ($subend ne 'EOP') {
550 print ">>> WARNING: malformed ",
551 "nested multipart\n";
556 $end = process_part($input, $type, $encoding,
557 $charset, $subboundary, $name);
559 if ($subtype eq 'alternative' && ! defined $altout &&
560 $type eq 'text/plain') {
565 # Since we changed the semantics of $boundary
566 # above for nested multiparts, if we are
567 # handling a nested multipart then find the end
568 # of our current part
571 if ($maintype eq 'multipart') {
572 $end = eat_part($input, $boundary);
577 if ($end eq 'EOM' || $end eq 'EOF') {
578 if ($subtype eq 'alternative' && !defined $altout) {
579 print ">>>multipart/alternative: no suitable ",
588 # "Eat" a MIME part; consume content until we hit the boundary or EOF
593 my ($input, $boundary) = @_;
597 # If we weren't given a boundary just eat input until EOF
600 if (! defined $boundary) {
606 # Otherwise, consume data until we hit our boundary
610 if ($ret = match_boundary($_, $boundary)) {
619 # Return the decoder subroutine to use
626 if ($encoding eq '7bit' || $encoding eq '8bit') {
627 return \&null_decoder;
628 } elsif ($encoding eq 'base64') {
629 return \&decode_base64;
630 } elsif ($encoding eq 'quoted-printable') {
633 warn "Unknown encoding: $encoding\n";
646 # Match a line against the boundary string
649 sub match_boundary($$)
651 my ($_, $boundary) = @_;
653 return if ! defined $boundary;
655 if (substr($_, 0, 2) eq '--') {
657 if ($_ eq "--$boundary") {
659 } elsif ($_ eq "--$boundary--") {