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