]> diplodocus.org Git - nmh/commitdiff
Added -convertargs switch to repl(1), to pass arguments to programs
authorDavid Levine <levinedl@acm.org>
Mon, 15 Dec 2014 04:11:00 +0000 (22:11 -0600)
committerDavid Levine <levinedl@acm.org>
Mon, 15 Dec 2014 04:11:00 +0000 (22:11 -0600)
specified in the user's profile or mhn.defaults to convert message
content.

14 files changed:
Makefile.am
docs/contrib/replaliases [new file with mode: 0644]
docs/pending-release-notes
etc/mhl.replywithoutbody [new file with mode: 0644]
etc/mhn.defaults.sh
h/mime.h
man/mh-mime.man
man/mhbuild.man
man/repl.man
test/common.sh.in
test/repl/test-convert [new file with mode: 0755]
test/repl/test-repl
uip/mhbuildsbr.c
uip/repl.c

index 1cc0875..b70e125 100644 (file)
@@ -88,7 +88,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \
        test/post/test-post-group test/post/test-mts test/post/test-messageid \
        test/post/test-sendfiles test/prompter/test-prompter \
        test/rcv/test-rcvdist test/rcv/test-rcvpack test/rcv/test-rcvstore \
        test/post/test-post-group test/post/test-mts test/post/test-messageid \
        test/post/test-sendfiles test/prompter/test-prompter \
        test/rcv/test-rcvdist test/rcv/test-rcvpack test/rcv/test-rcvstore \
-       test/rcv/test-rcvtty test/refile/test-refile \
+       test/rcv/test-rcvtty test/refile/test-refile test/repl/test-convert \
        test/repl/test-if-str test/repl/test-trailing-newline \
        test/repl/test-multicomp test/repl/test-repl \
        test/scan/test-scan test/scan/test-scan-multibyte \
        test/repl/test-if-str test/repl/test-trailing-newline \
        test/repl/test-multicomp test/repl/test-repl \
        test/scan/test-scan test/scan/test-scan-multibyte \
@@ -109,7 +109,7 @@ DISTCHECK_CONFIGURE_FLAGS = DISABLE_SETGID_MAIL=1
 ## automake 1.12.6 on FreeBSD 9 needs the sbr/dtimep.c.
 ##
 CLEANFILES = config/version.c sbr/sigmsg.h sbr/dtimep.c etc/mts.conf \
 ## automake 1.12.6 on FreeBSD 9 needs the sbr/dtimep.c.
 ##
 CLEANFILES = config/version.c sbr/sigmsg.h sbr/dtimep.c etc/mts.conf \
-            etc/gen-ctype-checked sbr/ctype-checked.h sbr/ctype-checked.c \
+            etc/gen-ctype-checked sbr/ctype-checked.[hc] \
             etc/mhn.defaults man/man.sed man/mh-chart.man $(man_MANS) \
             *.plist
 clean-local:
             etc/mhn.defaults man/man.sed man/mh-chart.man $(man_MANS) \
             *.plist
 clean-local:
@@ -188,15 +188,16 @@ noinst_HEADERS = h/addrsbr.h h/aliasbr.h h/crawl_folders.h h/dropsbr.h \
 ## Extra files we need to install in various places
 ##
 dist_nmhetc_DATA = etc/MailAliases etc/components etc/digestcomps \
 ## Extra files we need to install in various places
 ##
 dist_nmhetc_DATA = etc/MailAliases etc/components etc/digestcomps \
-                   etc/distcomps etc/forwcomps etc/mhl.body etc/mhl.digest \
-                   etc/mhl.format etc/mhl.forward etc/mhl.headers \
-                   etc/mhl.reply etc/mhshow.marker etc/rcvdistcomps \
-                   etc/rcvdistcomps.outbox etc/replcomps etc/replgroupcomps \
-                   etc/scan.MMDDYY \
-                   etc/scan.YYYYMMDD etc/scan.curses etc/scan.default \
-                   etc/scan.highlighted \
-                   etc/scan.mailx etc/scan.nomime etc/scan.size etc/scan.time \
-                   etc/scan.timely etc/scan.unseen
+                  etc/distcomps etc/forwcomps etc/mhl.body etc/mhl.digest \
+                  etc/mhl.format etc/mhl.forward etc/mhl.headers \
+                  etc/mhl.reply etc/mhl.replywithoutbody \
+                  etc/mhshow.marker etc/rcvdistcomps etc/rcvdistcomps.outbox \
+                  etc/replcomps etc/replgroupcomps \
+                  etc/scan.MMDDYY \
+                  etc/scan.YYYYMMDD etc/scan.curses etc/scan.default \
+                  etc/scan.highlighted \
+                  etc/scan.mailx etc/scan.nomime etc/scan.size etc/scan.time \
+                  etc/scan.timely etc/scan.unseen
 
 ##
 ## The same as above, but we don't include these in the distribution
 
 ##
 ## The same as above, but we don't include these in the distribution
@@ -221,6 +222,7 @@ dist_doc_DATA = COPYRIGHT INSTALL NEWS README VERSION \
 contribdir = $(docdir)/contrib
 dist_contrib_SCRIPTS = docs/contrib/replyfilter docs/contrib/build_nmh \
                       docs/contrib/ml docs/contrib/vpick
 contribdir = $(docdir)/contrib
 dist_contrib_SCRIPTS = docs/contrib/replyfilter docs/contrib/build_nmh \
                       docs/contrib/ml docs/contrib/vpick
+dist_contrib_DATA = docs/contrib/replaliases
 
 ##
 ## Our man pages
 
 ##
 ## Our man pages
@@ -328,7 +330,8 @@ uip_mark_LDADD = $(LDADD) $(POSTLINK)
 
 uip_mhbuild_SOURCES = uip/mhbuild.c uip/mhbuildsbr.c uip/mhcachesbr.c \
                      uip/mhlistsbr.c uip/mhoutsbr.c uip/mhmisc.c  \
 
 uip_mhbuild_SOURCES = uip/mhbuild.c uip/mhbuildsbr.c uip/mhcachesbr.c \
                      uip/mhlistsbr.c uip/mhoutsbr.c uip/mhmisc.c  \
-                     uip/mhfree.c uip/mhparse.c uip/md5.c
+                     uip/mhfree.c uip/mhparse.c uip/md5.c \
+                     uip/mhstoresbr.c uip/mhshowsbr.c
 uip_mhbuild_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_mhfixmsg_SOURCES = uip/mhfixmsg.c uip/mhparse.c uip/mhcachesbr.c \
 uip_mhbuild_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_mhfixmsg_SOURCES = uip/mhfixmsg.c uip/mhparse.c uip/mhcachesbr.c \
diff --git a/docs/contrib/replaliases b/docs/contrib/replaliases
new file mode 100644 (file)
index 0000000..68e9443
--- /dev/null
@@ -0,0 +1,23 @@
+#### replaliases
+####
+#### convenience functions for various repl(1) commands
+
+#### If using par (see mhn.defaults), it really helps to have its
+#### PARINIT environment variable set.  If you really want it to be
+#### null, either comment this out or set it to, e.g., ' '.
+if [ -z "$PARINIT" ]; then
+    PARINIT='rTbgqR B=.,?_A_a Q=_s>|'
+    export PARINIT
+fi
+
+#### Reply, including text/html (converted to text/plain) and text/plain parts.
+#### To ease editing at the What now? prompt, add a line like this to your
+#### .mh_profile:
+####     mhbuild-next: $EDITOR
+#### assuming that your EDTIOR environment variable is set; if not, replace
+#### $EDITOR above with the name of your editor.  Without that profile entry,
+#### enter "e[dit] $EDITOR" at the What now? prompt.
+rt() {
+    repl -filter mhl.replywithoutbody -editor mhbuild \
+         -convertargs text/html '' -convertargs text/plain '' "$@"
+}
index fe12e69..6077711 100644 (file)
@@ -28,6 +28,9 @@ NEW FEATURES
 - The -changecur and -nochangecur switches have been added to mhfixmsg(1).
 - mhfixmsg now removes an extraneous trailing semicolon from header
   parameter lists.
 - The -changecur and -nochangecur switches have been added to mhfixmsg(1).
 - mhfixmsg now removes an extraneous trailing semicolon from header
   parameter lists.
+- Added -convertargs switch to repl(1), to pass arguments to programs
+  specified in the user's profile or mhn.defaults to convert message
+  content.
 
 -----------------
 OBSOLETE FEATURES
 
 -----------------
 OBSOLETE FEATURES
diff --git a/etc/mhl.replywithoutbody b/etc/mhl.replywithoutbody
new file mode 100644 (file)
index 0000000..6abeab3
--- /dev/null
@@ -0,0 +1,5 @@
+; mhl.replywithoutbody
+;
+; message filter for `repl' (repl -format) that excludes the message body
+;
+from:nocomponent,formatfield="%(decode(friendly{text})) writes:"
index 2f7ddaf..2e43c87 100755 (executable)
@@ -25,6 +25,23 @@ fi
 TMP=/tmp/nmh_temp.$$
 trap "rm -f $TMP" 0 1 2 3 13 15
 
 TMP=/tmp/nmh_temp.$$
 trap "rm -f $TMP" 0 1 2 3 13 15
 
+if [ ! -z `$SEARCHPROG "$SEARCHPATH" par` ]; then
+    #### par fails on input lines that are too wide, so used fold(1)
+    #### to wrap them first.  The widths here correspond to those
+    #### for the text browsers below.
+    textfmt=' | fold -s -w 64 | par 64'
+    replfmt=" | fold -s -w 62 | sed 's/^\(.\)/> \1/; s/^$/>/;' | par 64"
+elif [ ! -z `$SEARCHPROG "$SEARCHPATH" fmt` ]; then
+    textfmt=' | fmt'
+    replfmt=" | fmt | sed 's/^\(.\)/> \1/; s/^$/>/;'"
+else
+    textfmt=
+    replfmt=
+fi
+[ ! -z `$SEARCHPROG "$SEARCHPATH" iconv` ]  &&
+    charsetconv=' | iconv -f ${charset:-us-ascii} -t utf-8'"${textfmt}"  ||
+    charsetconv=
+
 cat >>"$TMP" <<'EOF'
 mhstore-store-text: %m%P.txt
 mhstore-store-text/html: %m%P.html
 cat >>"$TMP" <<'EOF'
 mhstore-store-text: %m%P.txt
 mhstore-store-text/html: %m%P.html
@@ -111,9 +128,10 @@ fi
 #### mhbuild-disposition-<type>[/<subtype>] entries are used by the
 #### WhatNow attach for deciding whether the Content-Disposition
 #### should be 'attachment' or 'inline'.  Only those values are
 #### mhbuild-disposition-<type>[/<subtype>] entries are used by the
 #### WhatNow attach for deciding whether the Content-Disposition
 #### should be 'attachment' or 'inline'.  Only those values are
-#### supported.
+#### supported.  mhbuild-convert-text/html is defined below.
 ####
 cat <<EOF >>${TMP}
 ####
 cat <<EOF >>${TMP}
+mhbuild-convert-text: charset=%{charset}; iconv -f \${charset:-us-ascii} -t utf-8 %F${replfmt}
 mhbuild-disposition-text/calendar: inline
 mhbuild-disposition-message/rfc822: inline
 EOF
 mhbuild-disposition-text/calendar: inline
 mhbuild-disposition-message/rfc822: inline
 EOF
@@ -261,12 +279,17 @@ EOF
 # but only once I've added a new %-escape that makes more permanent temp files,
 # so netscape -remote can be used (without -remote you get a complaint dialog
 # that another netscape is already running and certain things can't be done).
 # but only once I've added a new %-escape that makes more permanent temp files,
 # so netscape -remote can be used (without -remote you get a complaint dialog
 # that another netscape is already running and certain things can't be done).
+#
+# The widths here correspond to the replfmt width, with par, above.
 PGM=`$SEARCHPROG "$SEARCHPATH" w3m`
 if [ ! -z "$PGM" ]; then
     echo 'mhshow-show-text/html: charset=%{charset}; '"\
 %l$PGM"' -dump ${charset:+-I "$charset"} -T text/html %F' >> $TMP
     echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-dump ${charset:+-I "$charset"} -O utf-8 -T text/html %F' >> $TMP
 PGM=`$SEARCHPROG "$SEARCHPATH" w3m`
 if [ ! -z "$PGM" ]; then
     echo 'mhshow-show-text/html: charset=%{charset}; '"\
 %l$PGM"' -dump ${charset:+-I "$charset"} -T text/html %F' >> $TMP
     echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-dump ${charset:+-I "$charset"} -O utf-8 -T text/html %F' >> $TMP
+    echo 'mhbuild-convert-text/html: charset=%{charset}; '"\
+$PGM "'-dump ${charset:+-I "$charset"} -O utf-8 -T text/html -cols 62 %F '"\
+${replfmt}" >> $TMP
 else
     PGM=`$SEARCHPROG "$SEARCHPATH" lynx`
     if [ ! -z "$PGM" ]; then
 else
     PGM=`$SEARCHPROG "$SEARCHPATH" lynx`
     if [ ! -z "$PGM" ]; then
@@ -276,6 +299,9 @@ else
         echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-child -dump -force_html ${charset:+--assume_charset "$charset"} %F | '"\
 expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
         echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-child -dump -force_html ${charset:+--assume_charset "$charset"} %F | '"\
 expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
+        echo 'mhbuild-convert-text/html: charset=%{charset}; '"\
+$PGM "'-child -dump -force_html ${charset:+--assume_charset "$charset"} '"\
+-width=62 %F${replfmt}" >> $TMP
     else
         PGM=`$SEARCHPROG "$SEARCHPATH" elinks`
         if [ ! -z "$PGM" ]; then
     else
         PGM=`$SEARCHPROG "$SEARCHPATH" elinks`
         if [ ! -z "$PGM" ]; then
@@ -283,6 +309,10 @@ expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
 -eval 'set document.browse.margin_width = 0' %F" >> $TMP
             echo "mhfixmsg-format-text/html: $PGM -dump -force-html \
 -no-numbering -eval 'set document.browse.margin_width = 0' %F" >> $TMP
 -eval 'set document.browse.margin_width = 0' %F" >> $TMP
             echo "mhfixmsg-format-text/html: $PGM -dump -force-html \
 -no-numbering -eval 'set document.browse.margin_width = 0' %F" >> $TMP
+            echo "mhbuild-convert-text/html: $PGM -dump -force-html \
+-no-numbering -eval 'set document.browse.margin_width = 0' %F${replfmt}" >> $TMP
+        else
+            echo 'mhbuild-convert-text/html: cat %F' >> $TMP
         fi
     fi
 fi
         fi
     fi
 fi
index 9fcaf5d..9cac94e 100644 (file)
--- a/h/mime.h
+++ b/h/mime.h
 #define        DESCR_FIELD     "Content-Description"
 #define        DISPO_FIELD     "Content-Disposition"
 #define        MD5_FIELD       "Content-MD5"
 #define        DESCR_FIELD     "Content-Description"
 #define        DISPO_FIELD     "Content-Disposition"
 #define        MD5_FIELD       "Content-MD5"
-#define PSEUDOHEADER_PREFIX            "Nmh-"
-#define        ATTACH_FIELD                    PSEUDOHEADER_PREFIX "Attach"
-#define        ATTACH_FIELD_ALT                "Attach"
+#define        PSEUDOHEADER_PREFIX        "Nmh-"
+#define        ATTACH_FIELD               PSEUDOHEADER_PREFIX "Attach"
+#define        ATTACH_FIELD_ALT           "Attach"
+#define        MHBUILD_FILE_PSEUDOHEADER  PSEUDOHEADER_PREFIX "mhbuild-file-"
+#define        MHBUILD_ARGS_PSEUDOHEADER  PSEUDOHEADER_PREFIX "mhbuild-args-"
 
 #define        isatom(c)   (isascii((unsigned char) c) \
                      && !isspace ((unsigned char) c) \
 
 #define        isatom(c)   (isascii((unsigned char) c) \
                      && !isspace ((unsigned char) c) \
index 51f0607..7a4bc81 100644 (file)
@@ -1,4 +1,4 @@
-.TH MH\-MIME %manext7% "March 16, 2014" "%nmhversion%"
+.TH MH\-MIME %manext7% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
@@ -141,8 +141,11 @@ command at the \*(lqWhat now?\*(rq prompt to process them.
 When replying to messages using
 .IR repl (1)
 the traditional MH method of including the original text in the reply does
 When replying to messages using
 .IR repl (1)
 the traditional MH method of including the original text in the reply does
-not interoperate with MIME messages.  As of this writing there is no
-native solution for addressing this issue, but the contrib directory
+not interoperate with MIME messages.  The
+.B \-convertargs
+switch to
+.IR repl (1)
+provides one solution.  Another solution:  the contrib directory
 .RI ( %docdir%/contrib )
 contains a Perl program called
 .B replyfilter
 .RI ( %docdir%/contrib )
 contains a Perl program called
 .B replyfilter
index 4b261f3..becf642 100644 (file)
@@ -1,4 +1,4 @@
-.TH MHBUILD %manext1% "March 13, 2014" "%nmhversion%"
+.TH MHBUILD %manext1% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
@@ -19,7 +19,7 @@ mhbuild \- translate MIME composition draft
 .RB [ \-verbose " | " \-noverbose ]
 .RB [ \-disposition " | " \-nodisposition ]
 .RB [ \-check " | " \-nocheck ]
 .RB [ \-verbose " | " \-noverbose ]
 .RB [ \-disposition " | " \-nodisposition ]
 .RB [ \-check " | " \-nocheck ]
-.RB [ \-headerencoding 
+.RB [ \-headerencoding
 .IR encoding\-algorithm
 .RB " | " \-autoheaderencoding ]
 .RB [ \-maxunencoded
 .IR encoding\-algorithm
 .RB " | " \-autoheaderencoding ]
 .RB [ \-maxunencoded
@@ -119,6 +119,158 @@ to supply the disposition value.  The only supported values are
 .I attachment
 and
 .IR inline.
 .I attachment
 and
 .IR inline.
+.SS "Convert Interface"
+.nr item 1 1
+The \*(lqconvert\*(rq interface is a powerful mechanism that supports
+replying to MIME messages.  These placeholders are used in the following
+description:
+.IP TYPE
+content type/subtype
+.IP CONVERTER
+external program, and any fixed arguments, to convert content, such as
+from a request to a reply
+.IP ARGSTRING
+arguments to pass from
+.B repl
+to
+.I CONVERTER
+.IP FILE
+full path of message being replied to
+.PP
+.RE
+The convert support is based on pseudoheaders of the form
+.PP
+.RS 5
+  Nmh-mhbuild-file-TYPE: FILE
+  Nmh-mhbuild-args-TYPE: ARGSTRING
+.RE
+.PP
+in the draft.  For each such pseudoheader, mhbuild looks in the
+profile and mhn.defaults for this corresponding TYPE entry to find the
+converter that supports it:
+.PP
+.RS 5
+.RI mhbuild-convert- TYPE :
+.I CONVERTER
+.RE
+.PP
+It's a fatal error if no such entry is found for TYPE.  An empty
+entry, e.g.,
+.PP
+.RS 5
+mhbuild-convert-text/html:
+.RE
+.PP
+excludes parts of that TYPE from the draft.  The mhn.defaults file
+contains default
+.B mhbuild-convert-text/html
+and
+.BR mhbuild-convert-text/plain
+entries.  Profile entries can be used to override corresponding
+mhn.defaults entries, as usual.
+.PP
+For each
+.I TYPE
+part in
+.IR FILE ,
+.B mhbuild
+runs
+.I CONVERTER ARGSTRING
+on the content of the part.
+.PP
+Each part in
+.I FILE
+that has no corresponding TYPE entry in the profile or mhn.defaults is
+excluded from the draft; the user can include them using mhbuild
+directives as usual.
+.PP
+.B repl
+inserts Nmh-mhbuild-text/html: and Nmh-mhbuild-text/plain:
+pseudoheaders in every draft.  The user can prevent insertion of
+content parts of either of those types by putting corresponding empty
+entries in their profile.
+.PP
+Only the highest precedence alternative with a supported
+.I TYPE
+of a multipart/alternative part is used.
+.PP
+mhn.defaults.sh selects the text/html-to-text/plain converter at
+install time.  It includes
+.BR iconv "(1),"
+and
+.BR par (1)
+or
+.BR fmt "(1),"
+in the pipeline only if found.
+.PP
+Some content types require the addition of parameters to the
+Content-Type header, such as
+.I method=REPLY
+for text/calendar.  mhbuild looks for a Content-Type header, followed
+by a blank line, at the beginning of the converter output.  If one is
+found, it is used for the corresponding part in the reply draft.
+.PP
+The \*(lqconvert\*(rq interface doesn't support different
+.IR ARGSTRING s
+or different converters for different parts of the same
+.IR  TYPE .
+That would require associating parts by part number with the
+.IR ARGSTRING s
+or converters.  Instead, that can be done (currently, without using
+the convert support), with
+.B mhbuild
+directives as described below, e.g.,
+.PP
+.RS 5
+#text/html; charset=utf-8 *8bit | mhstore -noverbose -part 42.7 -outfile - | w3m -dump -cols 64 -T text/html -O utf-8
+.RE
+.PP
+The only way to mix
+.B convert
+pseudoheaders and mhbuild directives is to insert the directives before
+.B mhbuild
+is run, which is typically done by entering
+.I mime
+at the \*(lqWhat now?\*(rq prompt, or with an
+.B \-editor mhbuild
+switch.
+.PP
+These (optional) setup steps can make the convert support
+easier to use:
+.IP \n[item]. 3
+If the
+.BR par (1)
+program is installed on your system, it will be set by default
+(in mhn.defaults) to filter the converter output.  It helps to
+set the
+.B $PARINIT
+environment variable, as described in its man page.
+.IP \n+[item]. 3
+Add this line to your profile:
+.PP
+.RS 5
+.nf
+mhbuild-next: $EDITOR
+.fi
+.RE
+.PP
+.RS 3
+assuming that your EDTIOR environment variable is set; if not, replace
+$EDITOR with the name of your editor.  Without that profile entry, a
+response of \*(lqe[dit]\*(rq at the What now? prompt will require
+specification of your editor if an
+.B \-editor mhbuild
+switch is used.
+.RE
+.IP \n+[item]. 3
+If using
+.BR repl ,
+source the Bourne-shell compatible functions in
+%docdir%/contrib/replaliases.  That script also sets the
+.B $PARINIT
+environment variable if it was not set.
+.RE
+.PP
 .SS "Translating the Composition File"
 .B mhbuild
 is essentially a filter to aid in the composition of MIME
 .SS "Translating the Composition File"
 .B mhbuild
 is essentially a filter to aid in the composition of MIME
@@ -180,7 +332,7 @@ errors:
 .nf
 #off
 #include <stdio.h>
 .nf
 #off
 #include <stdio.h>
-printf("Hello, World!);
+printf("Hello, World!");
 #pop
 .fi
 .RE
 #pop
 .fi
 .RE
@@ -522,7 +674,7 @@ The
 switch will indicate which algorithm to use when encoding any message headers
 that contain 8\-bit characters.  The valid arguments are
 .I base64
 switch will indicate which algorithm to use when encoding any message headers
 that contain 8\-bit characters.  The valid arguments are
 .I base64
-for based\-64 encoding and 
+for based\-64 encoding and
 .I quoted
 for quoted\-printable encoding.  The
 .B \-autoheaderencoding
 .I quoted
 for quoted\-printable encoding.  The
 .B \-autoheaderencoding
@@ -610,7 +762,7 @@ to translate the composition file into MIME format.
 .PP
 Normally it is an error to invoke
 .B mhbuild
 .PP
 Normally it is an error to invoke
 .B mhbuild
-on file that already in MIME format.  The 
+on file that already in MIME format.  The
 .B \-auto
 switch will cause
 .B mhbuild
 .B \-auto
 switch will cause
 .B mhbuild
@@ -770,7 +922,10 @@ is checked.
 .SH "SEE ALSO"
 .IR mhlist (1),
 .IR mhshow (1),
 .SH "SEE ALSO"
 .IR mhlist (1),
 .IR mhshow (1),
-.IR mhstore (1)
+.IR mhstore (1),
+.IR fmt (1),
+.IR iconv (1),
+.IR par (1)
 .PP
 .I "Proposed Standard for Message Encapsulation"
 (RFC 934),
 .PP
 .I "Proposed Standard for Message Encapsulation"
 (RFC 934),
index e775222..da8f015 100644 (file)
@@ -1,4 +1,4 @@
-.TH REPL %manext1% "December 13, 2014" "%nmhversion%"
+.TH REPL %manext1% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
@@ -36,6 +36,8 @@ all/to/cc/me]
 .RB [ \-editor
 .IR editor ]
 .RB [ \-noedit ]
 .RB [ \-editor
 .IR editor ]
 .RB [ \-noedit ]
+.RB [ \-convertargs
+.IR "type argstring" ]
 .RB [ \-whatnowproc
 .IR program ]
 .RB [ \-nowhatnowproc ]
 .RB [ \-whatnowproc
 .IR program ]
 .RB [ \-nowhatnowproc ]
@@ -234,6 +236,25 @@ and
 .B \-noatfile
 options.
 .PP
 .B \-noatfile
 options.
 .PP
+The
+.B \-convertargs
+switch directs
+.B repl
+to pass the arguments for
+.I type
+to
+.BR mhbuild .
+Both arguments are required;
+.I type
+must be non-empty while
+.I argstring
+can be empty, e.g., '' in a shell command line.  The
+.B \-convertargs
+switch can be used multiple times.
+.RI %docdir%/contrib/replaliases
+shows the use of
+.BR "repl \-convertargs" .
+.PP
 Although
 .B repl
 uses a forms file to direct it how to construct
 Although
 .B repl
 uses a forms file to direct it how to construct
@@ -516,6 +537,8 @@ is checked.
 .IR send (1),
 .IR whatnow (1),
 .IR mh\-format (5)
 .IR send (1),
 .IR whatnow (1),
 .IR mh\-format (5)
+.PP
+.I %docdir%/contrib/replaliases
 .SH DEFAULTS
 .nf
 .RB ` +folder "' defaults to the current folder"
 .SH DEFAULTS
 .nf
 .RB ` +folder "' defaults to the current folder"
index 004c8b1..417a4f9 100644 (file)
@@ -294,12 +294,12 @@ showproc: ${MH_LIBEXEC_DIR}/mhl
 EOF
 
   for f in MailAliases components digestcomps distcomps forwcomps mhl.body \
 EOF
 
   for f in MailAliases components digestcomps distcomps forwcomps mhl.body \
-           mhl.digest mhl.format mhl.forward mhl.headers mhl.reply \
+           mhl.digest mhl.format mhl.forward mhl.headers mhl.reply* \
            mhn.defaults rcvdistcomps replcomps replgroupcomps scan.MMDDYY \
            scan.YYYYMMDD scan.curses scan.default scan.highlighted scan.mailx \
           scan.nomime scan.size scan.time scan.timely scan.unseen
   do
            mhn.defaults rcvdistcomps replcomps replgroupcomps scan.MMDDYY \
            scan.YYYYMMDD scan.curses scan.default scan.highlighted scan.mailx \
           scan.nomime scan.size scan.time scan.timely scan.unseen
   do
-    cp "${MH_INST_DIR}${nmhetcdir}/${f}" "${MH_TEST_DIR}/Mail" || exit 1
+    cp "${MH_INST_DIR}${nmhetcdir}/"${f} "${MH_TEST_DIR}/Mail" || exit 1
   done
 
   folder -create +inbox > /dev/null
   done
 
   folder -create +inbox > /dev/null
diff --git a/test/repl/test-convert b/test/repl/test-convert
new file mode 100755 (executable)
index 0000000..a651957
--- /dev/null
@@ -0,0 +1,123 @@
+#!/bin/sh
+######################################################
+#
+# Test repl -convertarg
+#
+######################################################
+
+set -e
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname $0`/../..
+    MH_OBJ_DIR=`cd $srcdir && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/post/test-post-common.sh"
+
+expected="$MH_TEST_DIR/test-convert$$.expected"
+actual=`mhpath +`/draft
+
+printf 'Local-Mailbox: recipient@example.com' >>"$MH"
+
+
+# check -convertarg with multiple parts and no additional text in draft
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test
+Comments: In-reply-to sender@example.com
+   message dated "Thu, 11 Dec 2014 08:19:02 -0600."
+MIME-Version: 1.0
+Content-Type: text/plain; charset="US-ASCII"
+
+> This is part 1.
+
+> This is part 2.
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test
+Date: Thu, 11 Dec 2014 08:19:02 -0600
+Content-Type: multipart/mixed; boundary="_001_"
+MIME-Version: 1.0
+
+--_001_
+Content-Type: text/plain
+
+This is part 1.
+
+--_001_
+Content-Type: text/plain
+
+This is part 2.
+
+--_001_
+Content-Type: text/enriched
+
+This should not appear in the reply
+because the content type isn't matched.
+
+--_001_--
+EOF
+
+repl -noformat -convertarg text/plain '' -nowhatnowproc last
+mhbuild "$actual"
+check "$actual" "$expected"
+
+
+#### Make sure that this works with 8-bit encoding.
+require_locale en_US.utf-8 en_US.utf8
+LC_ALL=en_US.UTF-8; export LC_ALL
+
+
+# check -convertarg with multiple parts and additional text in draft
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test
+Comments: In-reply-to sender@example.com
+   message dated "Thu, 11 Dec 2014 08:19:02 -0600."
+MIME-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 8bit
+
+sender@example.com writes:
+
+> This is part 1.
+
+> This is part 2.
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test
+Date: Thu, 11 Dec 2014 08:19:02 -0600
+Content-Type: multipart/mixed; boundary="_001_"
+MIME-Version: 1.0
+
+--_001_
+Content-Type: text/plain
+
+This is part 1.
+
+--_001_
+Content-Type: text/plain
+
+This is part 2.
+
+--_001_--
+EOF
+
+repl -filter mhl.replywithoutbody -convertarg text/plain '' -nowhatnowproc last
+mhbuild "$actual"
+check "$actual" "$expected"
+
+
+exit $failed
index e4856d0..20bb96d 100755 (executable)
@@ -35,6 +35,7 @@ Usage: repl: [+folder] [msg] [switches]
   -nodraftfolder
   -editor editor
   -noedit
   -nodraftfolder
   -editor editor
   -noedit
+  -convertargs type argstring
   -fcc folder
   -filter filterfile
   -form formfile
   -fcc folder
   -filter filterfile
   -form formfile
index e046886..d6fe376 100644 (file)
@@ -48,6 +48,13 @@ struct attach_list {
     struct attach_list *next;
 };
 
     struct attach_list *next;
 };
 
+typedef struct convert_list {
+    char *type;
+    char *filename;
+    char *argstring;
+    struct convert_list *next;
+} convert_list;
+
 /*
  * Maximum size of URL token in message/external-body
  */
 /*
  * Maximum size of URL token in message/external-body
  */
@@ -62,6 +69,8 @@ void content_error (char *, CT, char *, ...);
 int find_cache (CT, int, int *, char *, char *, int);
 
 /* mhfree.c */
 int find_cache (CT, int, int *, char *, char *, int);
 
 /* mhfree.c */
+extern CT *cts;
+void freects_done (int) NORETURN;
 void free_ctinfo (CT);
 void free_encoding (CT, int);
 
 void free_ctinfo (CT);
 void free_encoding (CT, int);
 
@@ -70,6 +79,12 @@ void free_encoding (CT, int);
  */
 static int init_decoded_content (CT, const char *);
 static void setup_attach_content(CT, char *);
  */
 static int init_decoded_content (CT, const char *);
 static void setup_attach_content(CT, char *);
+static void set_disposition (CT);
+static void set_charset (CT, int);
+static void expand_pseudoheaders (CT, struct multipart *, const char *,
+                                  const convert_list *);
+static void expand_pseudoheader (CT, struct multipart *, const char *,
+                                 const char *, const char *);
 static char *fgetstr (char *, int, FILE *);
 static int user_content (FILE *, char *, CT *, const char *infilename);
 static void set_id (CT, int);
 static char *fgetstr (char *, int, FILE *);
 static int user_content (FILE *, char *, CT *, const char *infilename);
 static void set_id (CT, int);
@@ -77,6 +92,7 @@ static int compose_content (CT, int);
 static int scan_content (CT, size_t);
 static int build_headers (CT, int);
 static char *calculate_digest (CT, int);
 static int scan_content (CT, size_t);
 static int build_headers (CT, int);
 static char *calculate_digest (CT, int);
+static int extract_headers (CT, char *, FILE **);
 
 
 static unsigned char directives_stack[32];
 
 
 static unsigned char directives_stack[32];
@@ -133,6 +149,7 @@ build_mime (char *infile, int autobuild, int dist, int directives,
     HF hp;
     m_getfld_state_t gstate = 0;
     struct attach_list *attach_head = NULL, *attach_tail = NULL, *at_entry;
     HF hp;
     m_getfld_state_t gstate = 0;
     struct attach_list *attach_head = NULL, *attach_tail = NULL, *at_entry;
+    convert_list *convert_head = NULL, *convert_tail = NULL, *convert;
 
     directive_init(directives);
 
 
     directive_init(directives);
 
@@ -239,6 +256,86 @@ build_mime (char *infile, int autobuild, int dist, int directives,
                } else {
                    attach_head = attach_tail = entry;
                }
                } else {
                    attach_head = attach_tail = entry;
                }
+           } else if (strncasecmp(MHBUILD_FILE_PSEUDOHEADER, np,
+                                   strlen (MHBUILD_FILE_PSEUDOHEADER)) == 0) {
+                /* E.g.,
+                 * Nmh-mhbuild-file-text/calendar: /home/user/Mail/inbox/9
+                 */
+                char *type = np + strlen (MHBUILD_FILE_PSEUDOHEADER);
+                char *filename = vp;
+
+                /* vp should begin with a space because m_getfld()
+                   includes the space after the colon in buf. */
+                while (isspace((unsigned char) *filename)) { ++filename; }
+                /* Trim trailing newline and any other whitespace. */
+                rtrim (filename);
+
+                for (convert = convert_head; convert; convert = convert->next) {
+                    if (strcasecmp (convert->type, type) == 0) { break; }
+                }
+                if (convert) {
+                    if (convert->filename  &&
+                        strcasecmp (convert->filename, filename)) {
+                        adios (NULL, "Multiple %s headers with different files"
+                               " not allowed", type);
+                    } else {
+                        convert->filename = getcpy (filename);
+                    }
+                } else {
+                    convert = mh_xcalloc (sizeof *convert, 1);
+                    convert->filename = getcpy (filename);
+                    convert->type = getcpy (type);
+
+                    if (convert_tail) {
+                        convert_tail->next = convert;
+                    } else {
+                        convert_head = convert;
+                    }
+                    convert_tail = convert;
+                }
+
+                free (vp);
+                free (np);
+            } else if (strncasecmp(MHBUILD_ARGS_PSEUDOHEADER, np,
+                                   strlen (MHBUILD_ARGS_PSEUDOHEADER)) == 0) {
+                /* E.g.,
+                 * Nmh-mhbuild-args-text/calendar: -reply accept
+                 */
+                char *type = np + strlen (MHBUILD_ARGS_PSEUDOHEADER);
+                char *argstring = vp;
+
+                /* vp should begin with a space because m_getfld()
+                   includes the space after the colon in buf. */
+                while (isspace((unsigned char) *argstring)) { ++argstring; }
+                /* Trim trailing newline and any other whitespace. */
+                rtrim (argstring);
+
+                for (convert = convert_head; convert; convert = convert->next) {
+                    if (strcasecmp (convert->type, type) == 0) { break; }
+                }
+                if (convert) {
+                    if (convert->argstring  &&
+                        strcasecmp (convert->argstring, argstring)) {
+                        adios (NULL, "Multiple %s headers with different "
+                               "argstrings not allowed", type);
+                    } else {
+                        convert->argstring = getcpy (argstring);
+                    }
+                } else {
+                    convert = mh_xcalloc (sizeof *convert, 1);
+                    convert->type = getcpy (type);
+                    convert->argstring = getcpy (argstring);
+
+                    if (convert_tail) {
+                        convert_tail->next = convert;
+                    } else {
+                        convert_head = convert;
+                    }
+                    convert_tail = convert;
+                }
+
+                free (vp);
+                free (np);
            } else {
                add_header (ct, np, vp);
            }
            } else {
                add_header (ct, np, vp);
            }
@@ -361,6 +458,40 @@ finish_field:
        free(at_prev);
     }
 
        free(at_prev);
     }
 
+    /*
+     * Handle the mhbuild pseudoheaders, which deal with specific
+     * content types.
+     */
+    if (convert_head) {
+        CT *ctp;
+        convert_list *next;
+
+        done = freects_done;
+
+        /* In case there are multiple calls that land here, prevent leak. */
+        for (ctp = cts; ctp && *ctp; ++ctp) { free_content (*ctp); }
+        free (cts);
+
+        /* Extract the type part (as a CT) from filename. */
+        if (! (cts = (CT *) mh_xcalloc ((size_t) 2, sizeof *cts))) {
+            adios (NULL, "out of memory");
+        } else if (! (cts[0] = parse_mime (convert_head->filename))) {
+            adios (NULL, "failed to parse %s", convert_head->filename);
+        }
+
+        expand_pseudoheaders (cts[0], m, infile, convert_head);
+
+        /* Free the convert list. */
+        for (convert = convert_head; convert; convert = next) {
+            next = convert->next;
+            free (convert->type);
+            free (convert->filename);
+            free (convert->argstring);
+            free (convert);
+        }
+        convert_head = NULL;
+    }
+
     /*
      * To allow for empty message bodies, if we've found NO content at all
      * yet cook up an empty text/plain part.
     /*
      * To allow for empty message bodies, if we've found NO content at all
      * yet cook up an empty text/plain part.
@@ -1471,24 +1602,7 @@ scan_content (CT ct, size_t maxunencoded)
      * If the content is text and didn't specify a character set,
      * we need to figure out which one was used.
      */
      * If the content is text and didn't specify a character set,
      * we need to figure out which one was used.
      */
-
-    if (ct->c_type == CT_TEXT) {
-       t = (struct text *) ct->c_ctparams;
-       if (t->tx_charset == CHARSET_UNSPECIFIED) {
-           CI ci = &ct->c_ctinfo;
-           char *eightbitcharset = write_charset_8bit();
-
-           if (contains8bit && strcasecmp(eightbitcharset, "US-ASCII") == 0) {
-               adios(NULL, "Text content contains 8 bit characters, but "
-                     "character set is US-ASCII");
-           }
-
-           add_param(&ci->ci_first_pm, &ci->ci_last_pm, "charset",
-                       contains8bit ? eightbitcharset : "us-ascii", 0);
-
-           t->tx_charset = CHARSET_SPECIFIED;
-       }
-    }
+    set_charset (ct, contains8bit);
 
     /*
      * Decide which transfer encoding to use.
 
     /*
      * Decide which transfer encoding to use.
@@ -1866,7 +1980,6 @@ setup_attach_content(CT ct, char *filename)
     char *type, *simplename = r1bindex(filename, '/');
     struct str2init *s2i;
     PM pm;
     char *type, *simplename = r1bindex(filename, '/');
     struct str2init *s2i;
     PM pm;
-    char *cp;
 
     if (! (type = mime_type(filename))) {
        adios(NULL, "Unable to determine MIME type of \"%s\"", filename);
 
     if (! (type = mime_type(filename))) {
        adios(NULL, "Unable to determine MIME type of \"%s\"", filename);
@@ -1939,17 +2052,26 @@ setup_attach_content(CT ct, char *filename)
     ct->c_descr = add("\n", ct->c_descr);
     ct->c_cefile.ce_file = getcpy(filename);
 
     ct->c_descr = add("\n", ct->c_descr);
     ct->c_cefile.ce_file = getcpy(filename);
 
-    /*
-     * Look for mhbuild-disposition-<type>/<subtype> entry
-     * that specifies Content-Disposition type.  Only
-     * 'attachment' and 'inline' are allowed.  Default to
-     * 'attachment'.
-     */
+    set_disposition (ct);
+
+    add_param(&ct->c_dispo_first, &ct->c_dispo_last, "filename", simplename, 0);
+}
 
 
-    cp = context_find_by_type ("disposition", ct->c_ctinfo.ci_type,
-                               ct->c_ctinfo.ci_subtype);
-    if (cp != NULL) {
-        if (strcasecmp (cp, "attachment")  &&  strcasecmp (cp, "inline")) {
+/*
+ * If disposition type hasn't already been set in ct:
+ * Look for mhbuild-disposition-<type>/<subtype> entry
+ * that specifies Content-Disposition type.  Only
+ * 'attachment' and 'inline' are allowed.  Default to
+ * 'attachment'.
+ */
+void
+set_disposition (CT ct) {
+    if (ct->c_dispo_type == NULL) {
+        char *cp = context_find_by_type ("disposition", ct->c_ctinfo.ci_type,
+                                         ct->c_ctinfo.ci_subtype);
+
+        if (cp  &&  strcasecmp (cp, "attachment")  &&
+            strcasecmp (cp, "inline")) {
             admonish (NULL, "configuration problem: %s-disposition-%s%s%s "
                       "specifies '%s' but only 'attachment' and 'inline' are "
                       "allowed", invo_name,
             admonish (NULL, "configuration problem: %s-disposition-%s%s%s "
                       "specifies '%s' but only 'attachment' and 'inline' are "
                       "allowed", invo_name,
@@ -1958,13 +2080,384 @@ setup_attach_content(CT ct, char *filename)
                       ct->c_ctinfo.ci_subtype ? ct->c_ctinfo.ci_subtype : "",
                       cp);
         }
                       ct->c_ctinfo.ci_subtype ? ct->c_ctinfo.ci_subtype : "",
                       cp);
         }
+
+        ct->c_dispo_type = cp  ?  getcpy (cp)  :  getcpy ("attachment");
+    }
+}
+
+/*
+ * Set text content charset if it was unspecified.  contains8bit
+ * selctions:
+ * 0: content does not contain 8-bit characters
+ * 1: content contains 8-bit characters
+ * -1: ignore content and use user's locale to determine charset
+ */
+void
+set_charset (CT ct, int contains8bit) {
+    if (ct->c_type == CT_TEXT) {
+        struct text *t;
+
+        if (ct->c_ctparams == NULL) {
+            if ((t = ct->c_ctparams =
+                 (struct text *) mh_xcalloc (1, sizeof (struct text))) ==
+                NULL) {
+                adios (NULL, "out of memory");
+            }
+            t->tx_charset = CHARSET_UNSPECIFIED;
+        } else {
+            t = (struct text *) ct->c_ctparams;
+        }
+
+        if (t->tx_charset == CHARSET_UNSPECIFIED) {
+            CI ci = &ct->c_ctinfo;
+            char *eightbitcharset = write_charset_8bit();
+            char *charset = contains8bit ? eightbitcharset : "us-ascii";
+
+            if (contains8bit == 1  &&
+                strcasecmp (eightbitcharset, "US-ASCII") == 0) {
+                adios (NULL, "Text content contains 8 bit characters, but "
+                       "character set is US-ASCII");
+            }
+
+            add_param (&ci->ci_first_pm, &ci->ci_last_pm, "charset", charset,
+                       0);
+
+            t->tx_charset = CHARSET_SPECIFIED;
+        }
     }
     }
+}
+
+
+/*
+ * Look at all of the replied-to message parts and expand any that
+ * are matched by a pseudoheader.  Except don't descend into
+ * message parts.
+ */
+void
+expand_pseudoheaders (CT ct, struct multipart *m, const char *infile,
+                      const convert_list *convert_head) {
+    switch (ct->c_type) {
+    case CT_MULTIPART: {
+        struct multipart *mp = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        if (ct->c_subtype == MULTI_ALTERNATE) {
+            int matched = 0;
+
+            /* The parts are in descending priority order (defined by
+               RFC 2046 Sec. 5.1.4) because they were reversed by
+               parse_mime ().  So, stop looking for matches with
+               immediate subparts after the first match of an
+               alternative. */
+            for (part = mp->mp_parts; ! matched && part; part = part->mp_next) {
+                char *type_subtype =
+                    concat (part->mp_part->c_ctinfo.ci_type, "/",
+                            part->mp_part->c_ctinfo.ci_subtype, NULL);
+
+                if (part->mp_part->c_type == CT_MULTIPART) {
+                    expand_pseudoheaders (part->mp_part, mp, infile,
+                                          convert_head);
+                } else {
+                    const convert_list *c;
+
+                    for (c = convert_head; c; c = c->next) {
+                        if (! strcasecmp (type_subtype, c->type)) {
+                            expand_pseudoheader (part->mp_part, m, infile,
+                                                 c->type, c->argstring);
+                            matched = 1;
+                            break;
+                        }
+                    }
+                }
+                free (type_subtype);
+            }
+        } else {
+            for (part = mp->mp_parts; part; part = part->mp_next) {
+                expand_pseudoheaders (part->mp_part, m, infile, convert_head);
+            }
+        }
+        break;
+    }
+
+    default: {
+        char *type_subtype =
+            concat (ct->c_ctinfo.ci_type, "/", ct->c_ctinfo.ci_subtype,
+                    NULL);
+        const convert_list *c;
+
+        for (c = convert_head; c; c = c->next) {
+            if (! strcasecmp (type_subtype, c->type)) {
+                expand_pseudoheader (ct, m, infile, c->type, c->argstring);
+                break;
+            }
+        }
+        free (type_subtype);
+        break;
+    }
+    }
+}
 
 
-    if (cp) {
-       ct->c_dispo_type = getcpy(cp);
+
+/*
+ * Expand a single pseudoheader.  It's for the specified type.
+ */
+void
+expand_pseudoheader (CT ct, struct multipart *m, const char *infile,
+                     const char *type, const char *argstring) {
+    /* text_plain_ct is used to concatenate all of the text/plain
+       replies into one part, instead of having each one in a separate
+       part. */
+    CT text_plain_ct = NULL;
+    char *reply_file;
+    FILE *reply_fp = NULL;
+    char *convert, *type_p, *subtype_p;
+    char *convert_command;
+    char *charset = NULL;
+    char *cp;
+    struct str2init *s2i;
+    CT reply_ct;
+    struct part *part;
+
+    type_p = getcpy (type);
+    if ((subtype_p = strchr (type_p, '/'))) {
+        *subtype_p++ = '\0';
+        convert = context_find_by_type ("convert", type_p, subtype_p);
     } else {
     } else {
-       ct->c_dispo_type = getcpy("attachment");
+        free (type_p);
+        type_p = concat ("mhbuild-convert-", type, NULL);
+        convert = context_find (type_p);
     }
     }
+    free (type_p);
 
 
-    add_param(&ct->c_dispo_first, &ct->c_dispo_last, "filename", simplename, 0);
+    if (! (convert)) {
+        /* No mhbuild-convert- entry in mhn.defaults or profile
+           for type. */
+        return;
+    }
+    /* reply_file is used to pass the output of the convert. */
+    reply_file = getcpy (m_mktemp2 (NULL, invo_name, NULL, NULL));
+    convert_command =
+        concat (convert, " ", argstring ? argstring : "", " >", reply_file,
+                NULL);
+
+    /* Convert here . . . */
+    ct->c_storeproc = getcpy (convert_command);
+    ct->c_umask = ~m_gmprot ();
+    if (show_content_aux (ct, 0, convert_command, NULL, NULL) == NOTOK) {
+        admonish (NULL, "store of %s content failed", type);
+    }
+    free (convert_command);
+
+    /* Fill out the the new ct, reply_ct. */
+    reply_ct = (CT) mh_xcalloc (1, sizeof *reply_ct);
+    init_decoded_content (reply_ct, infile);
+
+    if (extract_headers (reply_ct, reply_file, &reply_fp) == NOTOK) {
+        free (reply_file);
+        admonish (NULL,
+                  "failed to extract headers from convert output in %s",
+                  reply_file);
+        return;
+    }
+
+    /* This sets reply_ct->c_ctparams, and reply_ct->c_termproc if the
+       charset can't be handled natively. */
+    for (s2i = str2cts; s2i->si_key; s2i++) {
+        if (strcasecmp(reply_ct->c_ctinfo.ci_type, s2i->si_key) == 0) {
+            break;
+        }
+    }
+
+    if ((reply_ct->c_ctinitfnx = s2i->si_init)) {
+        (*reply_ct->c_ctinitfnx)(reply_ct);
+    }
+
+    if ((cp =
+         get_param (reply_ct->c_ctinfo.ci_first_pm, "charset", '?', 1))) {
+        /* The reply Content-Type had the charset. */
+        charset = cp;
+    } else {
+        set_charset (reply_ct, -1);
+        charset = get_param (reply_ct->c_ctinfo.ci_first_pm, "charset", '?', 1);
+        if (reply_ct->c_reqencoding == CE_UNKNOWN  &&
+            strcasecmp (charset, "US-ASCII")) {
+            /* Assume that 8bit is sufficient (for text). */
+            reply_ct->c_reqencoding = CE_8BIT;
+        }
+    }
+
+    /* Concatenate text/plain parts. */
+    if (reply_ct->c_type == CT_TEXT  &&
+        reply_ct->c_subtype == TEXT_PLAIN) {
+
+        if (m->mp_parts  &&  ! text_plain_ct) {
+            text_plain_ct = m->mp_parts->mp_part;
+            /* Make sure that the charset is set in the text/plain
+               part. */
+            set_charset (text_plain_ct, -1);
+            if (text_plain_ct->c_reqencoding == CE_UNKNOWN  &&
+                strcasecmp (charset, "US-ASCII")) {
+                /* Assume that 8bit is sufficient (for text). */
+                text_plain_ct->c_reqencoding = CE_8BIT;
+            }
+        }
+
+        if (text_plain_ct) {
+            /* Only concatenate if the charsets are identical. */
+            char *text_plain_ct_charset =
+                get_param (text_plain_ct->c_ctinfo.ci_first_pm, "charset",
+                           '?', 1);
+
+            if (strcasecmp (text_plain_ct_charset, charset) == 0) {
+                /* Append this text/plain reply to the first one.
+                   If there's a problem anywhere along the way,
+                   instead attach it is a separate part. */
+                int text_plain_reply =
+                    open (text_plain_ct->c_cefile.ce_file,
+                          O_WRONLY | O_APPEND);
+                int addl_reply = open (reply_file, O_RDONLY);
+
+                if (text_plain_reply != NOTOK  &&  addl_reply != NOTOK) {
+                    /* Insert blank line before each addl part. */
+                    if (write (text_plain_reply, "\n", 1) == 1) {
+                        /* Copy the text from the new reply and
+                           then free its Content struct. */
+                        cpydata (addl_reply, text_plain_reply,
+                                 text_plain_ct->c_cefile.ce_file,
+                                 reply_file);
+                        if (close (text_plain_reply) == OK  &&
+                            close (addl_reply) == OK) {
+                            if (reply_fp) { fclose (reply_fp); }
+                            free (reply_file);
+                            free_content (reply_ct);
+                            return;
+                        }
+                    }
+                }
+            }
+        } else {
+            text_plain_ct = reply_ct;
+        }
+    }
+
+    reply_ct->c_cefile.ce_file = reply_file;
+    reply_ct->c_cefile.ce_fp = reply_fp;
+    reply_ct->c_cefile.ce_unlink = 1;
+
+    /* Attach the new part to the parent mulitpart/mixed, "m". */
+    part = (struct part *) mh_xcalloc (1, sizeof *part);
+    part->mp_part = reply_ct;
+    if (m->mp_parts) {
+        struct part *p;
+
+        for (p = m->mp_parts; p && p->mp_next; p = p->mp_next) { continue; }
+        p->mp_next = part;
+    } else {
+        m->mp_parts = part;
+    }
+}
+
+
+/* Extract any Content-Type header from beginning of convert output. */
+int
+extract_headers (CT ct, char *reply_file, FILE **reply_fp) {
+    char *buffer = NULL, *cp, *end_of_header;
+    int found_header = 0;
+    struct stat statbuf;
+
+    /* Read the convert reply from the file to memory. */
+    if (stat (reply_file, &statbuf) == NOTOK) {
+        admonish (reply_file, "failed to stat");
+        goto failed_to_extract_ct;
+    }
+
+    buffer = mh_xmalloc (statbuf.st_size + 1);
+
+    if ((*reply_fp = fopen (reply_file, "r+")) == NULL  ||
+        fread (buffer, 1, (size_t) statbuf.st_size, *reply_fp) <
+            (size_t) statbuf.st_size) {
+        admonish (reply_file, "failed to read");
+        goto failed_to_extract_ct;
+    }
+    buffer[statbuf.st_size] = '\0';
+
+    /* Look for a header in the convert reply. */
+    if ((end_of_header = strstr (buffer, "\r\n\r\n"))) {
+        end_of_header += 2;
+        found_header = 1;
+    } else if ((end_of_header = strstr (buffer, "\n\n"))) {
+        ++end_of_header;
+        found_header = 1;
+    }
+
+    if (found_header) {
+        CT tmp_ct;
+        char *tmp_file;
+        FILE *tmp_f;
+        size_t n;
+
+        /* Truncate buffer to just the C-T. */
+        *end_of_header = '\0';
+        n = strlen (buffer);
+
+        if (get_ctinfo (buffer + 14, ct, 0) != OK) {
+            admonish (NULL, "unable to get content info for reply");
+            goto failed_to_extract_ct;
+        }
+
+        /* Hack.  Use parse_mime() to detect the type/subtype of the
+           reply, which we'll use below. */
+        tmp_file = getcpy (m_mktemp2 (NULL, invo_name, NULL, NULL));
+        if ((tmp_f = fopen (tmp_file, "w"))  &&
+            fwrite (buffer, 1, n, tmp_f) == n) {
+            fclose (tmp_f);
+        } else {
+            goto failed_to_extract_ct;
+        }
+        tmp_ct = parse_mime (tmp_file);
+
+        if (tmp_ct) {
+            /* The type and subtype were detected from the reply
+               using parse_mime() above. */
+            ct->c_type = tmp_ct->c_type;
+            ct->c_subtype = tmp_ct->c_subtype;
+            free_content (tmp_ct);
+        }
+
+        free (tmp_file);
+
+        /* Rewrite the content without the header. */
+        cp = end_of_header + 1;
+        rewind (*reply_fp);
+
+        if (fwrite (cp, 1, statbuf.st_size - (cp - buffer), *reply_fp) <
+            (size_t) (statbuf.st_size - (cp - buffer))) {
+            admonish (reply_file, "failed to write");
+            goto failed_to_extract_ct;
+        }
+
+        if (ftruncate (fileno (*reply_fp), statbuf.st_size - (cp - buffer)) !=
+            0) {
+            advise (reply_file, "ftruncate");
+            goto failed_to_extract_ct;
+        }
+    } else {
+        /* No header section, assume the reply is text/plain. */
+        ct->c_type = CT_TEXT;
+        ct->c_subtype = TEXT_PLAIN;
+        if (get_ctinfo ("text/plain", ct, 0) == NOTOK) {
+            /* This never should fail, but just in case. */
+            adios (NULL, "unable to get content info for reply");
+        }
+    }
+
+    /* free_encoding() will close reply_fp, which is passed through
+       ct->c_cefile.ce_fp. */
+    free (buffer);
+    return OK;
+
+failed_to_extract_ct:
+    if (*reply_fp) { fclose (*reply_fp); }
+    free (buffer);
+    return NOTOK;
 }
 }
index a9f03b3..ebe72ff 100644 (file)
@@ -8,9 +8,9 @@
  */
 
 #include <h/mh.h>
  */
 
 #include <h/mh.h>
+#include <h/mime.h>
 #include <h/utils.h>
 
 #include <h/utils.h>
 
-
 #define REPL_SWITCHES \
     X("group", 0, GROUPSW) \
     X("nogroup", 0, NGROUPSW) \
 #define REPL_SWITCHES \
     X("group", 0, GROUPSW) \
     X("nogroup", 0, NGROUPSW) \
@@ -23,6 +23,7 @@
     X("nodraftfolder", 0, NDFLDSW) \
     X("editor editor", 0, EDITRSW) \
     X("noedit", 0, NEDITSW) \
     X("nodraftfolder", 0, NDFLDSW) \
     X("editor editor", 0, EDITRSW) \
     X("noedit", 0, NEDITSW) \
+    X("convertargs type argstring", 0, CONVERTARGSW) \
     X("fcc folder", 0, FCCSW) \
     X("filter filterfile", 0, FILTSW) \
     X("form formfile", 0, FORMSW) \
     X("fcc folder", 0, FCCSW) \
     X("filter filterfile", 0, FILTSW) \
     X("form formfile", 0, FORMSW) \
@@ -109,6 +110,7 @@ static char *fcc    = NULL;         /* folders to add to Fcc: header */
  * prototypes
  */
 static void docc (char *, int);
  * prototypes
  */
 static void docc (char *, int);
+static void add_convert_header (const char *, char *, char *, char *);
 
 
 int
 
 
 int
@@ -123,6 +125,9 @@ main (int argc, char **argv)
     char *folder = NULL, *msg = NULL, *dfolder = NULL;
     char *dmsg = NULL, *ed = NULL, drft[BUFSIZ], buf[BUFSIZ];
     char **argp, **arguments;
     char *folder = NULL, *msg = NULL, *dfolder = NULL;
     char *dmsg = NULL, *ed = NULL, drft[BUFSIZ], buf[BUFSIZ];
     char **argp, **arguments;
+    svector_t convert_types = svector_create (10);
+    svector_t convert_args = svector_create (10);
+    size_t n;
     struct msgs *mp = NULL;
     struct stat st;
     FILE *in;
     struct msgs *mp = NULL;
     struct stat st;
     FILE *in;
@@ -186,6 +191,34 @@ main (int argc, char **argv)
                    nedit++;
                    continue;
                    
                    nedit++;
                    continue;
                    
+               case CONVERTARGSW: {
+                    char *type;
+                    size_t i;
+
+                   if (!(type = *argp++)) {
+                       adios (NULL, "missing type argument to %s", argp[-2]);
+                    }
+                   if (!(cp = *argp++)) {
+                       adios (NULL, "missing argstring argument to %s",
+                              argp[-3]);
+                    }
+
+                    for (i = 0; i < svector_size (convert_types); ++i) {
+                        if (! strcmp (svector_at (convert_types, i), type)) {
+                            /* Already saw this type, so just update
+                               its args. */
+                            svector_strs (convert_args)[i] = cp;
+                            break;
+                        }
+                    }
+
+                    if (i == svector_size (convert_types)) {
+                        svector_push_back (convert_types, type);
+                        svector_push_back (convert_args, cp);
+                    }
+                   continue;
+                }
+
                case WHATSW: 
                    if (!(whatnowproc = *argp++) || *whatnowproc == '-')
                        adios (NULL, "missing argument to %s", argp[-2]);
                case WHATSW: 
                    if (!(whatnowproc = *argp++) || *whatnowproc == '-')
                        adios (NULL, "missing argument to %s", argp[-2]);
@@ -418,10 +451,25 @@ try_it_again:
             fcc, fmtproc);
     fclose (in);
 
             fcc, fmtproc);
     fclose (in);
 
+    {
+       char *filename = concat (mp->foldpath, "/", msg, NULL);
+
+        for (n = 0; n < svector_size (convert_types); ++n) {
+            add_convert_header (svector_at (convert_types, n),
+                                svector_at (convert_args, n),
+                                filename, drft);
+        }
+       free (filename);
+    }
+
     if (nwhat)
        done (0);
     what_now (ed, nedit, NOUSE, drft, msg, 0, mp, anot ? "Replied" : NULL,
            inplace, cwd, atfile);
     if (nwhat)
        done (0);
     what_now (ed, nedit, NOUSE, drft, msg, 0, mp, anot ? "Replied" : NULL,
            inplace, cwd, atfile);
+
+    svector_free (convert_args);
+    svector_free (convert_types);
+
     done (1);
     return 1;
 }
     done (1);
     return 1;
 }
@@ -453,3 +501,25 @@ docc (char *cp, int ccflag)
            break;
     }
 }
            break;
     }
 }
+
+/*
+ * Add pseudoheaders that will pass the convert arguments to
+ * mhbuild.  They have the form:
+ *   MHBUILD_FILE_PSEUDOHEADER-text/calendar: /home/user/Mail/inbox/7
+ *   MHBUILD_ARGS_PSEUDOHEADER-text/calendar: reply -accept
+ * The ARGS pseudoheader is optional, but we always add it when
+ * -convertargs is used.
+ */
+void
+add_convert_header (const char *convert_type, char *convert_arg,
+                    char *filename, char *drft) {
+    char *field_name;
+
+    field_name = concat (MHBUILD_FILE_PSEUDOHEADER, convert_type, NULL);
+    annotate (drft, field_name, filename, 1, 0, -2, 1);
+    free (field_name);
+
+    field_name = concat (MHBUILD_ARGS_PSEUDOHEADER, convert_type, NULL);
+    annotate (drft, field_name, convert_arg, 1, 0, -2, 1);
+    free (field_name);
+}