1#!/usr/bin/perl 2##------------------------------------------------------------------------## 3## File: 4## $Id: mha-mhedit,v 1.2 2003/01/04 21:11:26 ehood Exp $ 5## Author: 6## Earl Hood earl@earlhood.com 7## Description: 8## Program to help replying to MIME messages in MH/nmh. 9## POD at __END__. 10##------------------------------------------------------------------------## 11## Copyright (C) 2002 Earl Hood, earl@earlhood.com 12## 13## This program is free software; you can redistribute it and/or modify 14## it under the terms of the GNU General Public License as published by 15## the Free Software Foundation; either version 2 of the License, or 16## (at your option) any later version. 17## 18## This program is distributed in the hope that it will be useful, 19## but WITHOUT ANY WARRANTY; without even the implied warranty of 20## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21## GNU General Public License for more details. 22## 23## You should have received a copy of the GNU General Public License 24## along with this program; if not, write to the Free Software 25## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 26## 02111-1307, USA 27##------------------------------------------------------------------------## 28 29package mha_mhedit; 30 31use Getopt::Long; 32use IPC::Open2; 33require 'shellwords.pl'; 34 35my $QuoteChars = q/>/; 36 37##-----------------------------------------------------------------------## 38 39MAIN: { 40 my $mhpath = `mhpath +`; chomp $mhpath; 41 my $drafts = `mhparam Draft-Folder`; chomp $drafts; 42 $drafts = join('/', $mhpath, $drafts); 43 my $mheditor = `mhparam Editor`; chomp $mheditor; 44 my @defargs = shellwords(`mhparam mha-mhedit`); 45 unshift(@ARGV, @defargs); 46 47 my %opt = ( ); 48 GetOptions(\%opt, 49 50 'editor=s', 51 'htmlconv=s', 52 'linelen=i', 53 'mharc=s', 54 'mhonarc=s', 55 'quotechars=s', 56 'quoteprefix=s', 57 'tmpdir=s', 58 59 'help', 60 'man' 61 ) || die qq/ERROR: Use -help for usage information\n/; 62 usage(1, 0) if ($opt{'help'}); 63 usage(2, 0) if ($opt{'man'}); 64 65 my $file = shift @ARGV; 66 my $orgmesg = $ENV{'editalt'}; 67 my $editor = $opt{'editor'} || $mheditor || $ENV{'EDITOR'} || 'vi'; 68 my $mharc = $opt{'mharc'} || join('/', $mhpath, 'mha-repl.mrc'); 69 my $tmpdir = $opt{'tmpdir'} || $ENV{'TMPDIR'} || $drafts; 70 my $mhonarc = $opt{'mhonarc'} || 'mhonarc'; 71 my $w3m = $opt{'htmlconv'} || 'w3m'; 72 my $maxlen = $opt{'linelen'} || 76; 73 my $qprefix = $opt{'quoteprefix'} || '> '; 74 my @w3margs = shellwords(`mhparam mha-mhedit-$w3m`); 75 my @edargs = shellwords(`mhparam mha-mhedit-$editor`); 76 my @mhaargs = shellwords(`mhparam mha-mhedit-mhonarc`); 77 78 $QuoteChars = $opt{'quotechars'} || '<'; 79 $QuoteChars = "\Q$QuoteChars\E"; 80 81 if (!@w3margs && $w3m eq 'w3m') { 82 @w3margs = qw(-dump -cols 76 -T text/html); 83 } 84 85 if (! -r $mharc) { 86 die qq/ERROR: "$mharc" does not exist or is not readable!/; 87 } 88 89 my $header = get_header_components($file); 90 my @mhacmd = ($mhonarc, 91 '-single', 92 '-rcfile', $mharc, 93 '-outdir', $tmpdir, 94 @mhaargs, 95 $orgmesg); 96 97 local(*DRAFT, *MHA, *W3M_IN, *W3M_OUT); 98 open(DRAFT, '>'.$file) || die qq/ERROR: Unable to write to "$file": $!/; 99 cmd_pipe_open(\*MHA, @mhacmd); 100 $/ = undef; 101 my $html = <MHA>; 102 close(MHA); 103 104 my $pid = open2(\*W3M_OUT, \*W3M_IN, $w3m, @w3margs); 105 print W3M_IN $html; 106 close(W3M_IN); 107 my $text = <W3M_OUT>; 108 close(W3M_OUT); 109 110 if ($maxlen > 0) { 111 $text =~ s/^(.*)$/break_line($1, $maxlen-length($qprefix))/gem; 112 } 113 $text =~ s/^/$qprefix/gm; 114 print DRAFT $header; 115 print DRAFT $text; 116 close(DRAFT); 117 118 exec($editor, @edargs, $file); 119} 120 121##-----------------------------------------------------------------------## 122 123sub get_header_components { 124 my $file = shift; 125 local(*DRAFT); 126 127 my $header = ''; 128 open(DRAFT, $file) || die qq/ERROR: Unable to open "$file": $!/; 129 while (<DRAFT>) { 130 $header .= $_; 131 last if /^--------/ || /^$/; 132 } 133 my $cache = ''; 134 my $n = 0; 135 while (defined($_ = <DRAFT>) && ($n < 5)) { 136 last if /^>/; 137 $cache .= $_; 138 } 139 close(DRAFT); 140 $header .= $cache if ($n < 5); 141 $header; 142} 143 144sub cmd_pipe_open { 145 my $handle = shift; 146 my @cmd = @_; 147 my $child_pid = open($handle, '-|'); 148 if ($child_pid) { # parent 149 return $handle; 150 } else { # child 151 #open(STDERR, '>&STDOUT'); 152 exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/; 153 } 154} 155 156sub cmd_pipe_write { 157 my $handle = shift; 158 my @cmd = @_; 159 my $child_pid = open($handle, '|-'); 160 if ($child_pid) { # parent 161 return $handle; 162 } else { # child 163 #open(STDERR, '>&STDOUT'); 164 exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/; 165 } 166} 167 168sub break_line { 169 my($str) = shift; 170 my($width) = shift; 171 my($q, $new) = ('', ''); 172 my($try, $trywidth, $len); 173 174 ## Translate tabs to spaces 175 1 while 176 $str =~ s/^([^\t]*)(\t+)/$1 . ' ' x (length($2) * 8 - length($1) % 8)/e; 177 178 ## Do nothing if str <= width 179 return $str if length($str) <= $width; 180 181 ## See if str begins with a quote char 182 if ($str =~ s/^([ ]?(?:[$QuoteChars][ ]?)+)//o) { 183 $q = $1; 184 $width -= length($q); 185 } 186 187 ## Create new string by breaking up str 188 while ($str ne "") { 189 # If $str less than width, break out 190 if (length($str) <= $width) { 191 $new .= $q . $str; 192 last; 193 } 194 195 # handle case where no-whitespace line larger than width 196 if (($str =~ /^(\S+)/) && (($len = length($1)) >= $width)) { 197 $new .= $q . $1; 198 substr($str, 0, $len) = ""; 199 next; 200 } 201 202 # Break string at whitespace 203 $try = ''; 204 $trywidth = $width; 205 $try = substr($str, 0, $trywidth); 206 if ($try =~ /(\S+)$/) { 207 $trywidth -= length($1); 208 $new .= $q . substr($str, 0, $trywidth); 209 } else { 210 $new .= $q . $try; 211 } 212 substr($str, 0, $trywidth) = ''; 213 214 } continue { 215 $new .= "\n" if $str; 216 } 217 $new; 218} 219 220sub usage { 221 require Pod::Usage; 222 my $verbose = shift || 0; 223 my $exit_code = shift; 224 225 if ($verbose == 0) { 226 Pod::Usage::pod2usage(-verbose => $verbose); 227 } else { 228 my $pager = $ENV{'PAGER'} || 'more'; 229 local(*PAGER); 230 my $fh = (-t STDOUT && open(PAGER, "|$pager")) ? \*PAGER : \*STDOUT; 231 Pod::Usage::pod2usage(-verbose => $verbose, 232 -output => $fh); 233 close(PAGER) if ($fh == \*PAGER); 234 } 235 defined($exit_code) && exit($exit_code); 236} 237 238__END__ 239 240=head1 NAME 241 242mha-mhedit - MH/nmh reply editor for multipart, non-text/plain messages 243 244=head1 SYNOPSIS 245 246 repl -editor mha-mhedit 247 248=head1 DESCRIPTION 249 250mha-mhedit nicely formats MIME messages for use with MH/nmh's repl(1) 251command. 252 253A big deficiency with MH/nmh's C<repl> is that it is not MIME aware, 254or more technically, repl filters are not MIME aware. Consequently, 255if replying to a multipart, non-plain text message, and your repl 256filter includes the body of the message being replied to, all the 257MIME formatting is included, which can be messing for binary data, 258like images, and for quoted-printable text. 259 260mha-mhedit is designed to be used as the C<repl> editor: 261 262 repl -editor mha-mhedit 263 264mha-mhedit is designed to functional transparently. mha-mhedit formats 265and quotes the reply-to message body for editing in the message draft. 266After the formatting is complete, mha-mhedit invokes your regular 267editor for final composition. 268 269mha-mhedit does its job by using MHonArc, 270E<lt>http://www.mhonarc.orgE<gt>, with a specially crafted MHonArc 271resource file, and by using a text-based HTML viewer, like w3m, 272E<lt>http://w3m.sourceforge.net/E<gt>. Of course, a tool can be 273developed that does not depend on these types of tools, but I did 274not feel like developing one and these work well for what I wanted. 275 276mha-mhedit can be invoke by default when using C<repl> by 277having the following in your C<.mh_profile>: 278 279 repl: -editor mha-mhedit 280 281However, since using mha-mhedit adds some extra initial overhead 282before your regular editor is invoked, you may choose to define a 283shell alias instead: 284 285 (t)csh: 286 alias mrepl repl -editor mha-mhedit 287 288 bash: 289 alias mrepl="repl -editor mha-mhedit" 290 291Therefore, if replying to a MIME messages, you enter C<mrepl> at your 292shell prompt and continue to use C<repl> for plain text messages. 293 294B<Note:> It is recommended to have something like the following 295in your C<.mh_profile>: 296 297 mha-mhedit-next: <your-editor> 298 299This way, at the C<What now?> prompt, if you enter C<edit>, it 300will invoke your regular editor again instead of calling 301mha-mhedit, which will cause all your edits to be lost. 302 303=head1 OPTIONS 304 305Options can be defined in your C<.mh_profile> like any other 306MH/nmh component. For example: 307 308 mha-mhedit: -editor vim 309 310The following options are available: 311 312=over 313 314=item -editor I<editor> 315 316Text editor to invoke at the end. If not specified, the 317value of the Editor profile component, the EDITOR environment variable, 318or C<vi> is used. 319 320Arguments to the editor can be defined by setting the C<mha-mhedit-I<editor>> 321profile component. For example, if your editor is set to C<vim>, the 322following can be added to your C<.mh_profile> to have vim start at the 323end of your reply: 324 325 mha-mhedit-vim: + 326 327=item -htmlconv I<command> 328 329Command that converts HTML to plain text. The command must be able 330to take HTML from stdin and dump plain text to stdout. If the command 331requires arguments. If not specified, then C<w3m> is used. 332 333You can define arguments to give the specified converter by defining the 334C<mha-mhedit-I<htmlconv>> profile component in your C<.mh_profile>. For 335example, the following represents the default arguments used if 336C<-htmlconv> is C<w3m>: 337 338 mha-mhedit-w3m: -dump -cols 76 -T text/html 339 340The following works well if C<-htmlconv> is set to C<lynx>: 341 342 mha-mhedit-lynx: -stdin -dump -force_html -nolist -width=76 343 344=item -linelen I<number> 345 346Maxium line length of formated reply text. If the orginal message 347contains lines longer then C<-linelen>, the lines will be wrapped. 348Default value is 76. 349 350=item -mharc I<pathname> 351 352Pathname to mhonarc resource file. If not specified, the file 353C<mha-repl.mrc> in your MH/nmh directory. 354 355B<Note:> If a resource file is not found, mha-mhedit aborts execution. 356If using a custom resource file, be careful of settings that could 357create extra files during mail-to-html conversion. 358 359=item -mhonarc I<pathname> 360 361Pathname to mhonarc program. If not specified, C<mhonarc> is used. 362 363You can define extra arguments to give C<mhonarc> by defining the 364C<mha-mhedit-mhonarc> profile component in your C<.mh_profile>. However, 365I'm unsure if you will ever need to. 366 367=item -quotechars I<chars> 368 369List of characters denoting the start of a quoted line in a message. 370This option affects is used during line wrapping when detecting for 371quoted lines. 372 373The default value is C<E<gt>>. 374 375=item -quoteprefix I<string> 376 377String to prepend to each line of text being responded to. The default 378value is "C<E<gt> >" (greater-than sign followed by a space). 379 380=item -tmpdir I<pathname> 381 382Pathname to directory to use for temporary files. If not 383specified, then the TMPDIR environment variable is used or 384the path location of Draft-Folder profile component. 385 386=item -help 387 388Display help information. 389 390=item -man 391 392Display entire manpage. 393 394=back 395 396=head1 INSTALLATION 397 398Copy the mha-mhedit program to somewhere in your search path. 399Copy mha-repl.mrc into your MH/nmh directory. 400 401=head1 NOTES 402 403=over 404 405=item * 406 407mha-mhedit is designed to work friendly with your normal replcomps 408and repl filter. mha-mhedit reads the head of the draft created 409by repl in order to preserve it after formatting the initial 410message body. mha-mhedit preserves the replcomps message header 411and up-to the next 5 lines afterwards that does not look like 412quoted reply text generated from the repl filter (mha-mhedit assumes 413that the a line starting with a '>' denotes the begining of 414quoted reply text). 415 416If I have lost you, here is the replcomps I have: 417 418 %(lit)%(formataddr %<{reply-to}%?{from}%?{sender}%?{return-path}%>)\ 419 %<(nonnull)%(void(width))%(putaddr To: )\n%>\ 420 %(lit)%(formataddr{to})%(formataddr{cc})%(formataddr(me))\ 421 %<(nonnull)%(void(width))%(putaddr cc: )\n%>\ 422 %<{fcc}Fcc: %{fcc}\n%>\ 423 %<{subject}Subject: Re: %(putstr(trim{subject}))\n%>\ 424 From: Earl Hood \<earl@earlhood.com\> 425 Reply-To: Earl Hood \<earl@earlhood.com\> 426 %<{date}In-reply-to: %<{message-id}%{message-id}%>\n%>\ 427 %<{message-id}References: %<{references}%(void{references})%(trim)%(putstr) %> 428 %(void{message-id})%(trim)%(putstr)\n%>\ 429 -------- 430 On %(lmonth{date}) %(mday{date}), %(year{date}) at \ 431 %02(putnumf(hour{date})):%02(putnumf(min{date})), \ 432 %<{from}%(friendly{from})%|you%> wrote: 433 434 435And the repl filter (repl.filter) I use for quoting the body of the 436reply-to message: 437 438 leftadjust,compwidth=14 439 body:component=> ,overflowtext=> ,noleftadjust 440 441The replcomps file includes a preamble in the message body providing the 442date and person who wrote the message I am replying to. 443 444My repl.filter causes the reply-to message body to be included in 445the composition draft quoted with "> " before each line. 446 447mha-mhedit automatically reads the initial draft header (everything 448up to the C<-------->). After that, mha-mhedit reads up-to the 449next 5 lines for potential inclusion. The body preamble in my 450replcomps takes up 2 lines (there is a trailing blank line that 451may not be easily noticed above). Therefore, mha-mhedit will 452preserve it. The third line will be the start of quoted text. 453mha-mhedit sees this, and stops reading the draft. 454 455Now, if I plan to make mha-mhedit my default repl editor, it will be 456more efficient if I remove the message body quoting since mha-mhedit 457will ignore and overwrite anyway. 458 459=item * 460 461Currently, no temporary files should be generated. However, this 462depends on the version of mhonarc you are using, the type 463of message being replied to, and any edits you may make to the 464mhonarc resource file. 465 466It may be possible that by-product files, like attachments, could 467be created. If so, manual deletion of the files in C<-tmpdir> will 468be required. 469 470=back 471 472=head1 FILES 473 474=over 475 476=item C<$HOME/.mh_profile> 477 478Profile definitions. 479 480=item C<E<lt>mh-dirE<gt>/mha-repl.mrc> 481 482MHonArc resource file. The resource file can be explicitly 483defined via the C<-mharc> command-line option. 484 485=back 486 487=head1 MH/nmh PROFILE COMPONENTS 488 489=over 490 491=item C<mha-mhedit> 492 493Arguments to mha-mhedit. 494 495=item C<mha-mhedit-I<editor>> 496 497Arguments to pass to editor invoked by mha-mhedit, where 498I<editor> is the name of the editor. 499 500=item C<mha-mhedit-I<htmlconv>> 501 502Arguments to pass to html-to-text converter, where 503I<htmlconv> is the name of the html-to-text converter program. 504 505=item C<mha-mhedit-mhonarc> 506 507Extra arguments to pass to mhonarc. 508 509=item C<Editor> 510 511Text editor. 512 513=back 514 515=head1 ENVIRONMENT 516 517=over 518 519=item C<EDITOR> 520 521Default editor. 522 523=item C<TMPDIR> 524 525Location of temporary files. 526 527=back 528 529=head1 DEPENDENCIES 530 531=over 532 533=item MHonArc 534 535MHonArc, E<lt>http://www.mhonarc.org/E<gt>, is used to translate 536the raw message into HTML. MHonArc is used since it conveniently 537combines multipart, non-plain text, messages into a singe HTML 538document. 539 540=item w3m 541 542w3m, E<lt>http://w3m.sourceforge.net/E<gt>, is used to translate HTML 543generated by mhonarc into format plain text. 544 545An alternate converter program can be used via the C<-htmlconv> option. 546It must be able to take HTML as standard input and dump formated text 547to standard out. 548 549=back 550 551=head1 VERSION 552 553C<$Id: mha-mhedit,v 1.2 2003/01/04 21:11:26 ehood Exp $> 554 555=head1 AUTHOR 556 557Earl Hood, earl@earlhood.com 558 559Copyright (C), 2002. 560This program comes with ABSOLUTELY NO WARRANTY and may be copied only 561under the terms of the GNU General Public License. 562 563=cut 564 565