]> diplodocus.org Git - nmh/commitdiff
Added mhfixmsg(1).
authorDavid Levine <levinedl@acm.org>
Sun, 17 Mar 2013 15:32:57 +0000 (10:32 -0500)
committerDavid Levine <levinedl@acm.org>
Sun, 17 Mar 2013 15:32:57 +0000 (10:32 -0500)
.gitignore
Makefile.am
docs/pending-release-notes
etc/mhn.defaults.sh
man/mhfixmsg.man [new file with mode: 0644]
test/mhfixmsg/test-mhfixmsg [new file with mode: 0755]
uip/mhfixmsg.c [new file with mode: 0644]

index 8d7e69d79e4856722d0c0ce2a70da10c75d14f5d..cb9f2017194ecedee7953bcd96bdb5c6f4c48aec 100644 (file)
@@ -69,6 +69,7 @@ a.out.dSYM/
 /uip/install-mh
 /uip/mark
 /uip/mhbuild
+/uip/mhfixmsg
 /uip/mhl
 /uip/mhlist
 /uip/mhn
index 0e64a27bd06056937816e4b524e6dec71df6a199..cb876894ff4ff954d97933ee57325326e08c0af8 100644 (file)
@@ -60,6 +60,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \
        test/inc/test-pop \
        test/install-mh/test-install-mh test/manpages/test-manpages \
        test/mhbuild/test-forw test/mhbuild/test-utf8-body \
+       test/mhfixmsg/test-mhfixmsg \
        test/mhlist/test-mhlist test/mhmail/test-mhmail \
        test/mhparam/test-mhparam test/mhpath/test-mhpath \
        test/mhshow/test-cte-binary test/mhshow/test-qp \
@@ -126,11 +127,11 @@ BUILT_SOURCES = sbr/sigmsg.h sbr/ctype-checked.c
 ##
 bin_PROGRAMS = uip/ali uip/anno uip/burst uip/comp uip/dist uip/flist \
               uip/fmttest uip/folder uip/forw uip/inc uip/install-mh \
-              uip/mark uip/mhbuild uip/mhlist uip/mhn uip/mhparam \
-              uip/mhpath uip/mhshow uip/mhstore uip/msgchk uip/msh uip/new \
-              uip/packf uip/pick uip/prompter uip/refile uip/repl uip/rmf \
-              uip/rmm uip/scan uip/send uip/show uip/sortm uip/whatnow \
-              uip/whom
+              uip/mark uip/mhbuild uip/mhfixmsg uip/mhlist uip/mhn \
+              uip/mhparam uip/mhpath uip/mhshow uip/mhstore uip/msgchk \
+              uip/msh uip/new uip/packf uip/pick uip/prompter uip/refile \
+              uip/repl uip/rmf uip/rmm uip/scan uip/send uip/show uip/sortm \
+              uip/whatnow uip/whom
 
 bin_SCRIPTS = uip/mhmail etc/sendfiles
 
@@ -212,9 +213,9 @@ man_MANS = man/ali.1 man/anno.1 man/ap.8 man/burst.1 man/comp.1 \
           man/forw.1 man/fprev.1 man/inc.1 man/install-mh.1 man/mark.1 \
           man/mh-alias.5 man/mh-chart.7 man/mh-draft.5 man/mh-folders.5 \
           man/mh-format.5 man/mh-mail.5 man/mh-profile.5 man/mh_profile.5 \
-          man/mh-sequence.5 man/mh-tailor.5 man/mhbuild.1 man/mhl.1 \
-          man/mhlist.1 man/mhmail.1 man/mhn.1 man/mhparam.1 man/mhpath.1 \
-          man/mhshow.1 man/mhstore.1 man/msgchk.1 man/msh.1 \
+          man/mh-sequence.5 man/mh-tailor.5 man/mhbuild.1 man/mhfixmsg.1 \
+          man/mhl.1 man/mhlist.1 man/mhmail.1 man/mhn.1 man/mhparam.1 \
+          man/mhpath.1 man/mhshow.1 man/mhstore.1 man/msgchk.1 man/msh.1 \
           man/mts.conf.5 man/new.1 man/next.1 man/nmh.7 man/packf.1 \
           man/pick.1 man/post.8 man/prev.1 man/prompter.1 man/rcvdist.1 \
           man/rcvpack.1 man/rcvstore.1 man/rcvtty.1 man/refile.1 \
@@ -233,10 +234,10 @@ man_SRCS = man/ali.man man/anno.man man/ap.man man/burst.man man/comp.man \
           man/mh-chart-gen.sh man/mh-draft.man man/mh-folders.man \
           man/mh-format.man man/mh-mail.man man/mh-profile.man \
           man/mh_profile.man man/mh-sequence.man man/mh-tailor.man \
-          man/mhbuild.man man/mhl.man man/mhlist.man man/mhmail.man \
-          man/mhn.man man/mhparam.man man/mhpath.man man/mhshow.man \
-          man/mhstore.man man/msgchk.man man/msh.man man/mts.conf.man \
-          man/new.man man/next.man man/nmh.man \
+          man/mhbuild.man man/mhfixmsg.man man/mhl.man man/mhlist.man \
+          man/mhmail.man man/mhn.man man/mhparam.man man/mhpath.man \
+          man/mhshow.man man/mhstore.man man/msgchk.man man/msh.man \
+          man/mts.conf.man man/new.man man/next.man man/nmh.man \
           man/packf.man man/pick.man man/post.man man/prev.man \
           man/prompter.man man/rcvdist.man man/rcvpack.man \
           man/rcvstore.man man/rcvtty.man man/refile.man man/repl.man \
@@ -300,6 +301,12 @@ uip_mhbuild_SOURCES = uip/mhbuild.c uip/mhbuildsbr.c uip/mhcachesbr.c \
                      uip/mhfree.c uip/mhparse.c uip/termsbr.c uip/md5.c
 uip_mhbuild_LDADD = $(LDADD) $(TERMLIB)
 
+uip_mhfixmsg_SOURCES = uip/mhfixmsg.c uip/mhparse.c uip/mhcachesbr.c \
+                      uip/mhoutsbr.c uip/mhmisc.c uip/mhfree.c \
+                      uip/mhshowsbr.c uip/mhlistsbr.c \
+                      uip/termsbr.c uip/md5.c
+uip_mhfixmsg_LDADD = $(LDADD) $(TERMLIB)
+
 uip_mhlist_SOURCES = uip/mhlist.c uip/mhparse.c uip/mhcachesbr.c \
                     uip/mhlistsbr.c uip/mhmisc.c uip/mhfree.c uip/termsbr.c \
                     uip/md5.c
index 1192e7e31cad2e37ebb594a3872054d69784cb1f..cacf2e371f7a9e033526dffe97fa11a754ab9f1a 100644 (file)
@@ -45,6 +45,8 @@ NEW FEATURES
 - mhshow/mhstore now have support for RFC-2017 (access-type=url) for
   external message bodies.
 - Added -retainsequences switch to refile(1).
+- A new program, mhfixmsg(1), is included to rewrite MIME messages with
+  various transformations.
 
 ----------------------------
 OBSOLETE/DEPRECATED FEATURES
index 4142f4cbc6f920e1c5e0fc273321b4d52b4ca900..6bc0124763aa1118c58620208b59a3e3161adcdb 100755 (executable)
@@ -25,6 +25,18 @@ fi
 TMP=/tmp/nmh_temp.$$
 trap "rm -f $TMP" 0 1 2 3 13 15
 
+
+if [ ! -z "`$SEARCHPROG $SEARCHPATH w3m`" ]; then
+  echo "mhfixmsg-format-text/html: w3m -dump -T text/html -O utf-8 '%F'" >> $TMP
+elif [ ! -z "`$SEARCHPROG $SEARCHPATH lynx`" ]; then
+  #### lynx indents with 3 spaces, remove them and any trailing spaces.
+  echo "mhfixmsg-format-text/html: lynx -child -dump -force_html '%F' | \
+expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
+elif [ ! -z "`$SEARCHPROG $SEARCHPATH elinks`" ]; then
+  echo "mhfixmsg-format-text/html: elinks -dump -force_html -no-numbering '%F'" >> $TMP
+fi
+
+
 echo "mhstore-store-text: %m%P.txt" >> $TMP
 echo "mhstore-store-text/richtext: %m%P.rt" >> $TMP
 echo "mhstore-store-video/mpeg: %m%P.mpg" >> $TMP
diff --git a/man/mhfixmsg.man b/man/mhfixmsg.man
new file mode 100644 (file)
index 0000000..1d1d678
--- /dev/null
@@ -0,0 +1,281 @@
+.TH MHFIXMSG %manext1% "March 17, 2013" "%nmhversion%"
+.\"
+.\" %nmhwarning%
+.\"
+.SH NAME
+mhfixmsg \- rewrite MIME messages with various transformations
+.SH SYNOPSIS
+.HP 5
+.na
+.B mhfixmsg
+.RI [ +folder ]
+.RI [ msgs ]
+.RB [ \-decodetext
+8bit/7bit |
+.BR \-nodecodetext ]
+.RB [ \-textcodeset
+.I codeset
+.RB "| " \-notextcodeset ]
+.RB [ \-reformat " | " \-noreformat ]
+.RB [ \-fixboundary " | " \-nofixboundary ]
+.RB [ \-fixcte " | " \-nofixcte ]
+.RB [ \-file
+.IR file ]
+.RB [ \-outfile
+.IR outfile ]
+.RB [ \-verbose " | " \-noverbose ]
+.RB [ \-version ]
+.RB [ \-help ]
+.ad
+.SH DESCRIPTION
+.B mhfixmsg
+rewrites MIME messages, applying specific transformations such as
+decoding of MIME-encoded message parts and repairing invalid MIME
+headers.
+.PP
+MIME messages are specified in RFC\-2045 to RFC\-2049
+(see
+.IR mhbuild (1)).
+The
+.B mhlist
+command is invaluable for viewing the content structure of MIME
+messages.
+.B mhfixmsg
+passes non-MIME messages through without any transformations.  If no
+transformations apply to a MIME message, the original message or file
+is not modified or removed.
+.PP
+The
+.B \-decodetext
+switch enables a transformation to decode each base64 and
+quoted-printable text message part to the selected 8bit or 7bit
+encoding.  If the decoded text would not fit the selected encoding as
+defined by RFC-2045, the part is not decoded.
+.PP
+The
+.B \-textcodeset
+switch specifies that all text/plain parts of the message(s)
+should be converted to
+.IR codeset .
+Codeset conversions require that
+.B nmh
+be built with
+.IR iconv (3).
+To convert text parts other than text/plain, an external program can
+be used, via the
+.B \-reformat
+switch.
+.PP
+The
+.B \-reformat
+switch enables a transformation for text parts in the message.  For
+each text part that is not text/plain and that does not have a
+corresponding text/plain in a multipart/alternative part,
+.B mhfixmsg
+looks for a mhfixmsg-format-text/subtype profile entry that matches
+the subtype of the part.  If one is found and can be used to
+successfully convert the part to text/plain,
+.B mhfixmsg
+inserts that text/plain part at the beginning of the containing
+multipart/alternative part, if present.  If not, it creates a
+multipart/alternative part.
+.PP
+.B \-reformat
+requires a profile entry for each text part subtype to be reformatted.
+The mhfixmsg-format-text/subtype profile entries are based on external
+conversion programs, and are used the same way that
+.B mhshow
+uses its mhshow-show-text/subtype entries.  When
+.B nmh
+is installed, it searches for a conversion program for text/html
+content, and if one is found, inserts a mhfixmsg-format-text/html
+entry in %etcdir%/mhn.defaults.  An entry of the same name in the
+user's profile takes precedence.  The user can add entries for
+other text subtypes to their profile.
+.PP
+The
+.B \-fixboundary
+switch enables a transformation to repair the boundary portion of the
+Content-Type header field of the message to match the boundaries of
+the outermost multipart part of the message, if it does not.  That
+condition is indicated by a \*(lqbogus multipart content in
+message\*(rq error message from
+.B mhlist
+and other
+.B nmh
+programs that parse MIME messages.
+.PP
+The
+.B \-fixcte
+switch enables a transformation to change the
+Content-Transfer-Encoding from an invalid value to 8bit in message
+parts with a Content-Type of multipart, as required by RFC 2045,
+Section 6.4.  That condition is indicated by a \*(lqmust be encoded in
+7bit, 8bit, or binary\*(rq error message from
+.B mhlist
+and other
+.B nmh
+programs that parse MIME messages.
+.PP
+The
+.B \-verbose
+switch directs
+.B mhfixmsg
+to output informational message for each transformation applied.
+.PP
+The
+.B \-file
+.I file
+switch directs
+.B mhfixmsg
+to use the specified
+file as the source message, rather than a message from a folder.
+If this file is \*(lq-\*(rq, then
+.B mhfixmsg
+accepts the source message on the standard input stream.  If
+the
+.B \-outfile
+switch is not enabled when using the standard input stream,
+.B mhfixmsg
+will not produce a transformed output message.
+.PP
+.BR mhfixmsg ,
+by default, transforms the message in place.  If the
+.B \-outfile
+switch is enabled, then
+.B mhfixmsg
+does not modify the input message or file, but instead places its
+output in the specified file.  An outfile name of \*(lq-\*(rq
+specifies the standard output stream.
+.PP
+Combined with the
+.B \-verbose
+switch, the
+.B \-outfile
+switch can be used to show what transformations
+.B mhfixmsg
+would apply without actually applying them, e.g.,
+.PP
+.RS 5
+mhfixmsg -outfile /dev/null -verbose
+.RE
+.PP
+As always, this usage obeys any
+.B mhfixmsg
+switches in the user's profile.
+.PP
+.B \-outfile
+can be combined with
+.B rcvstore
+to add a single transformed message to a different folder, e.g.,
+.PP
+.RS 5
+mhfixmsg -outfile - | \\
+.RS 0
+%libdir%/rcvstore +folder
+.RE
+.RE
+.SS Summary of Applicability
+The transformations apply to the parts of a message depending on
+content type and/or encoding as follows:
+.PP
+.RS 5
+.nf
+.ta \w'\-fixboundary 'u
+\-decodetext   base64 and quoted-printable encoded text parts
+\-textcodeset  text/plain parts
+\-reformat     text parts that are not text/plain
+\-fixboundary  outermost multipart part
+\-fixcte       multipart part
+.fi
+.RE
+.PP
+.SS "Backup of Original Message/File"
+If it applies any transformations to a message or file,
+and the
+.B \-outfile
+switch is not used,
+.B mhfixmsg
+backs up the original the same way as
+.BR rmm .
+That is, it uses the
+.I rmmproc
+profile component, if present.  If not present,
+.B mhfixmsg
+moves the original message to a backup file.
+.PP
+.SS "Integration with procmail"
+By way of example, here is an excerpt from a procmailrc file
+that filters messages through
+.B mhfixmsg
+before storing them in the user's
+.I nmh-workers
+folder.  It also stores the incoming message in the
+.I Backups
+folder in a filename generated by
+.BR mktemp ,
+which is a non-POSIX utility to generate a temporary file.
+If you do not have that utility, then the
+.BR mkstemp (3)
+function could form the basis for a substitute.  Or,
+.B mhfixmsg
+could be called on the message after it is stored.
+.PP
+.RS 5
+.nf
+.ta \w'\-fixboundary 'u
+PATH = %bindir%:$PATH
+MAILDIR = `mhparam path`
+MKTEMP = 'mktemp Backups/mhfixmsg.XXXXXXXX'
+MHFIXMSG = 'mhfixmsg -noverbose -file - -outfile -'
+STORE = %libdir%/rcvstore
+
+:0 w: nmh/procmail.$LOCKEXT
+* ^TOnmh-workers@nongnu.org
+| tee `$MKTEMP` | $MHFIXMSG | $STORE +nmh-workers
+.fi
+.RE
+.PP
+.SH FILES
+.fc ^ ~
+.nf
+.ta \w'%etcdir%/mhn.defaults  'u
+^$HOME/\&.mh\(ruprofile~^The user profile
+^%etcdir%/mhn.defaults~^Default mhfixmsg conversion entries
+.fi
+.SH "PROFILE COMPONENTS"
+.fc ^ ~
+.nf
+.ta 2.4i
+.ta \w'ExtraBigProfileName  'u
+^Path:~^To determine the user's nmh directory
+^Current\-Folder:~^To find the default current folder
+^rmmproc:~^Program to delete original messages or files
+.fi
+.SH "SEE ALSO"
+.IR mh-profile (5),
+.IR mhbuild (1),
+.IR mhlist (1),
+.IR mhshow (1),
+.IR mkstemp (3),
+.IR procmail (1),
+.IR procmailrc (5),
+.IR rcvstore (1),
+.IR rmm (1)
+.SH DEFAULTS
+.nf
+.RB ` +folder "' defaults to the current folder"
+.RB ` msgs "' defaults to cur"
+.RB ` "\-decodetext 8bit"'
+.RB ` \-notextcodeset '
+.RB ` \-reformat '
+.RB ` \-fixboundary '
+.RB ` \-fixcte '
+.RB ` \-noverbose '
+.fi
+.SH CONTEXT
+If a folder is given, it will become the current folder.  The last
+message selected from a folder will become the current message.  If
+the
+.B \-file
+switch is used, the context will not be modified.
diff --git a/test/mhfixmsg/test-mhfixmsg b/test/mhfixmsg/test-mhfixmsg
new file mode 100755 (executable)
index 0000000..14b9a58
--- /dev/null
@@ -0,0 +1,869 @@
+#!/bin/sh
+######################################################
+#
+# Test mhfixmsg
+#
+######################################################
+
+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-mhfixmsg$$.expected"
+expected_err="$MH_TEST_DIR/test-mhfixmsg$$.expected_err"
+actual="$MH_TEST_DIR/test-mhfixmsg$$.actual"
+actual_err="$MH_TEST_DIR/test-mhfixmsg$$.actual_err"
+
+set +e
+if grep mhfixmsg-format-text/html "${MH_TEST_DIR}/Mail/mhn.defaults" \
+  >/dev/null; then
+  can_reformat_texthtml=1
+else
+  echo "$0: skipping -reformat check because
+    mhfixmsg-format-text/html is not available"
+  can_reformat_texthtml=0
+fi
+set -e
+
+
+# check -help
+# Verified behavior consistent with compiled sendmail.
+cat >"$expected" <<EOF
+Usage: mhfixmsg [+folder] [msgs] [switches]
+  switches are:
+  -decodetext 8bit|7bit
+  -nodecodetext
+  -[no]textcodeset
+  -[no]reformat
+  -[no]fixboundary
+  -[no]fixcte
+  -file file
+  -outfile file
+  -[no]verbose
+  -version
+  -help
+EOF
+
+mhfixmsg -help >"$actual" 2>&1
+check "$expected" "$actual"
+
+
+# check -version
+# Verified same behavior as compiled mhfixmsg.
+case `mhfixmsg -version` in
+  mhfixmsg\ --*) ;;
+  *) printf '%s: mhfixmsg -version generated unexpected output\n' "$0" >&2
+     failed=`expr ${failed:-0} + 1`;;
+esac
+
+
+# check that non-MIME messages aren't modified
+# check -outfile
+run_test 'mhfixmsg first -outfile '"$actual" ''
+check "`mhpath first`" "$actual" 'keep first'
+
+
+# check that non-MIME messages with no bodies aren't modified
+# check -outfile
+cat >`mhpath new` <<EOF
+From: Test <test@example.com>
+To: Some User <user@example.com>
+Date: Fri, 29 Sep 2006 00:00:00
+Message-Id: @test.nmh
+Subject: message with no body
+EOF
+
+run_test 'mhfixmsg last -outfile '"$actual" ''
+check "`mhpath last`" "$actual"
+
+
+# check -nofixcte
+cat >"$MH_TEST_DIR"/Mail/inbox/11 <<EOF
+From: Anon
+To: Mailinglist
+Subject: =?ISO-8859-15?Q?Re=3A_H=E5lla_linuxsystem_uppdaterade?=
+User-Agent: Alpine 2.00 (DEB 1167 2008-08-23)
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;
+    BOUNDARY="----=_NextPart_000_0000_00000000.00000000"
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+
+  This message is in MIME format.  The first part should be readable
+text,
+  while the remaining parts are likely unreadable without MIME-aware
+tools.
+
+------=_NextPart_000_0000_00000000.00000000
+Content-Type: TEXT/PLAIN; CHARSET=ISO-8859-15
+Content-Transfer-Encoding: 8BIT
+
+Some text in swedish.
+
+Varf=C3=B6r inte anv=C3=A4nda...
+
+------=_NextPart_000_0000_00000000.00000000--
+
+And some text after the last part.
+EOF
+
+cp -p "$MH_TEST_DIR"/Mail/inbox/11 "$MH_TEST_DIR"/Mail/inbox/12
+
+run_test 'mhfixmsg last -nofixcte' ''
+check "$MH_TEST_DIR"/Mail/inbox/11 "$MH_TEST_DIR"/Mail/inbox/12 'keep first'
+
+
+# check -fixcte (enabled by default):  fixup of erroneous C-T-E in multipart
+# check -verbose
+cat >"$expected" <<EOF
+From: Anon
+To: Mailinglist
+Subject: =?ISO-8859-15?Q?Re=3A_H=E5lla_linuxsystem_uppdaterade?=
+User-Agent: Alpine 2.00 (DEB 1167 2008-08-23)
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;
+    BOUNDARY="----=_NextPart_000_0000_00000000.00000000"
+Nmh-REPLACED-INVALID-Content-Transfer-Encoding: QUOTED-PRINTABLE
+Content-Transfer-Encoding: 8bit
+
+  This message is in MIME format.  The first part should be readable
+text,
+  while the remaining parts are likely unreadable without MIME-aware
+tools.
+
+------=_NextPart_000_0000_00000000.00000000
+Content-Type: TEXT/PLAIN; CHARSET=ISO-8859-15
+Content-Transfer-Encoding: 8BIT
+
+Some text in swedish.
+
+Varf=C3=B6r inte anv=C3=A4nda...
+
+------=_NextPart_000_0000_00000000.00000000--
+
+And some text after the last part.
+EOF
+
+run_test 'mhfixmsg last -outfile '"$actual"' -verbose' \
+         "mhfixmsg: 11, replace Content-Transfer-Encoding of \
+QUOTED-PRINTABLE with 8 bit"
+check "$expected" "$actual" 'keep first'
+
+
+# check with no options:  checks backup
+cp "$MH_TEST_DIR"/Mail/inbox/11 "$MH_TEST_DIR"/Mail/inbox/11.original
+folder last >/dev/null
+run_test 'mhfixmsg' ''
+check "$expected" "$MH_TEST_DIR"/Mail/inbox/11 'keep first'
+cp "$MH_TEST_DIR"/Mail/inbox/11.original "$MH_TEST_DIR"/Mail/inbox/11
+check "$MH_TEST_DIR"/Mail/inbox/,11 "$MH_TEST_DIR"/Mail/inbox/11.original
+
+
+# check backup with -file
+cp "$MH_TEST_DIR"/Mail/inbox/11 "$MH_TEST_DIR"/Mail/inbox/11.original
+folder last >/dev/null
+run_test 'mhfixmsg -file '"$MH_TEST_DIR"/Mail/inbox/11 ''
+check "$MH_TEST_DIR"/Mail/inbox/11 "$expected" 'keep first'
+check "$MH_TEST_DIR"/Mail/inbox/,11 "$MH_TEST_DIR"/Mail/inbox/11.original
+
+
+# check -reformat (enabled by default):  addition of text/plain part
+# to solitary text/html part
+#
+cat >"$expected" <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: multipart/alternative; boundary="----=_nmh-multipart"
+
+------=_nmh-multipart
+Content-Type: text/plain; charset="Windows-1252"
+Content-Transfer-Encoding: 8bit
+
+Need to go! Need ... to ... go!
+
+------=_nmh-multipart
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: 8bit
+
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
+<meta name="Generator" content="Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+<div>
+<div>Need to go! Need ... to ... go!</div>
+</body>
+</html>
+
+------=_nmh-multipart--
+EOF
+
+cat >"$MH_TEST_DIR"/Mail/inbox/12 <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DWindows-1=
+252">
+<meta name=3D"Generator" content=3D"Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; pad=
+ding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+<div>
+<div>Need to go! Need ... to ... go!</div>
+</body>
+</html>
+EOF
+
+if [ $can_reformat_texthtml -eq 1 ]; then
+  printf '%s\n' "mhfixmsg: 12, insert text/plain part
+mhfixmsg: 12 part 1, decode text/html; charset=\"Windows-1252\"" \
+       >"$expected.err"
+
+  #### lynx inserts multiple blank lines, so use uniq to squeeze them.
+  mhfixmsg last -outfile - -verbose 2>"$actual.err" | uniq >"$actual"
+  check "$expected" "$actual"
+  check "$expected.err" "$actual.err"
+fi
+
+
+# check handling of boundary string that appears in message body
+#
+cat >"$expected" <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: multipart/alternative; boundary="----=_nmh-multipart-3"
+
+------=_nmh-multipart-3
+Content-Type: text/plain; charset="Windows-1252"
+Content-Transfer-Encoding: 8bit
+
+------=_nmh-multipart
+------=_nmh-multipart-1
+------=_nmh-multipart-2
+
+------=_nmh-multipart-3
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: 8bit
+
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
+<meta name="Generator" content="Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+------=_nmh-multipart<br>
+------=_nmh-multipart-1<br>
+------=_nmh-multipart-2<br>
+</body>
+</html>
+
+------=_nmh-multipart-3--
+EOF
+
+cat >"$MH_TEST_DIR"/Mail/inbox/12 <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DWindows-1=
+252">
+<meta name=3D"Generator" content=3D"Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; pad=
+ding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+------=3D_nmh-multipart<br>
+------=3D_nmh-multipart-1<br>
+------=3D_nmh-multipart-2<br>
+</body>
+</html>
+EOF
+
+if [ $can_reformat_texthtml -eq 1 ]; then
+  printf '%s\n' "mhfixmsg: 12, insert text/plain part
+mhfixmsg: 12 part 1, decode text/html; charset=\"Windows-1252\"" \
+       >"$expected.err"
+
+  #### lynx inserts multiple blank lines, so use uniq to squeeze them.
+  mhfixmsg last -outfile - -verbose 2>"$actual.err" | uniq >"$actual"
+  check "$expected" "$actual"
+  check "$expected.err" "$actual.err"
+fi
+
+
+# check -nodecode
+cat >"$expected" <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: multipart/alternative; boundary="----=_nmh-multipart"
+
+------=_nmh-multipart
+Content-Type: text/plain; charset="Windows-1252"
+Content-Transfer-Encoding: 8bit
+
+Need to go! Need ... to ... go!
+
+------=_nmh-multipart
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DWindows-=
+1252">
+<meta name=3D"Generator" content=3D"Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; pa=
+dding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+<div>
+<div>Need to go! Need ... to ... go!</div>
+</body>
+</html>
+
+------=_nmh-multipart--
+EOF
+
+cat >"$MH_TEST_DIR"/Mail/inbox/12 <<EOF
+MIME-Version: 1.0
+From: sender@example.com
+To: bonquiqui@example.com
+Subject: rue
+Date: Sat, 26 Jan 2013 17:37:53 -0500
+Content-Type: text/html; charset="Windows-1252"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DWindows-1=
+252">
+<meta name=3D"Generator" content=3D"Microsoft Exchange Server">
+<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; pad=
+ding-left: 4pt; border-left: #800000 2px solid; } --></style>
+</head>
+<body>
+<div>
+<div>Need to go! Need ... to ... go!</div>
+</body>
+</html>
+EOF
+
+if [ $can_reformat_texthtml -eq 1 ]; then
+  printf '%s\n' 'mhfixmsg: 12, insert text/plain part' >"$expected.err"
+
+  #### lynx inserts multiple blank lines, so use uniq to squeeze them.
+  mhfixmsg last -nodecode -outfile - -verbose 2>"$actual.err" | uniq >"$actual"
+  check "$expected" "$actual"
+  check "$expected.err" "$actual.err"
+fi
+
+
+# check -decode (enabled by default)
+cat >"$expected" <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg decode test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="test4.txt"
+Content-Disposition: attachment; filename="test4.txt"
+Content-Transfer-Encoding: 8bit
+
+This is a text/plain part.
+
+------- =_aaaaaaaaaa0--
+EOF
+
+msgfile=`mhpath new`
+cat >$msgfile <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg decode test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="test4.txt"
+Content-Disposition: attachment; filename="test4.txt"
+Content-Transfer-Encoding: base64
+
+VGhpcyBpcyBhIHRleHQvcGxhaW4gcGFydC4K
+
+------- =_aaaaaaaaaa0--
+EOF
+
+mhfixmsg last -outfile "$actual"
+check "$expected" "$actual"
+
+
+# check -decode with more complicated content structure
+cat >$expected <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg decode test 2
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+This is additional content before the first subpart of the multipart.
+
+------- =_aaaaaaaaaa0
+Content-Type: multipart/related;
+    type="multipart/alternative";
+    boundary="subpart__1.1"
+
+--subpart__1.1
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test1.txt"
+
+This is the first text/plain part, in a subpart.
+
+--subpart__1.1--
+
+This is additional content after the last subpart of the multipart.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test2.txt"
+Content-MD5: kq+Hnc2SD/eKwAnkFBDuEA==
+Content-Transfer-Encoding: 8bit
+
+This is the second text/plain part.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test3.txt"
+
+This is the third text/plain part.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="test4.txt"
+Content-Disposition: attachment; filename="test4.txt"
+Content-Transfer-Encoding: 8bit
+
+This is the fourth text/plain part.
+
+------- =_aaaaaaaaaa0--
+
+This is additional content after the last subpart of the multipart.
+EOF
+
+msgfile=`mhpath new`
+cat >$msgfile <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg decode test 2
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+This is additional content before the first subpart of the multipart.
+
+------- =_aaaaaaaaaa0
+Content-Type: multipart/related;
+    type="multipart/alternative";
+    boundary="subpart__1.1"
+
+--subpart__1.1
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test1.txt"
+
+This is the first text/plain part, in a subpart.
+
+--subpart__1.1--
+
+This is additional content after the last subpart of the multipart.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test2.txt"
+Content-MD5: kq+Hnc2SD/eKwAnkFBDuEA==
+Content-Transfer-Encoding: quoted-printable
+
+This is the second text/plain part.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: attachment; filename="test3.txt"
+
+This is the third text/plain part.
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="test4.txt"
+Content-Disposition: attachment; filename="test4.txt"
+Content-Transfer-Encoding: base64
+
+VGhpcyBpcyB0aGUgZm91cnRoIHRleHQvcGxhaW4gcGFydC4K
+
+------- =_aaaaaaaaaa0--
+
+This is additional content after the last subpart of the multipart.
+EOF
+mhfixmsg last -outfile "$actual"
+check "$expected" "$actual"
+
+
+# check attempted -decode of binary text
+#### Generated the encoded text below with:
+####   $ printf '\x0d\xbd\xb2=\xbc\n' | base64
+msgfile=`mhpath new`
+cat >$msgfile <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg attempted binary decode test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="square.txt"
+Content-Transfer-Encoding: base64
+
+Db2yPbwK
+
+------- =_aaaaaaaaaa0--
+EOF
+
+cp -p `mhpath last` "$expected"
+mhfixmsg last
+check `mhpath last` "$expected" 'keep first'
+
+
+# check -decode of binary text
+printf "%s\x0d\xbd\xb2=\xbc%s" "To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg binary decode test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary=\"----- =_aaaaaaaaaa0\"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset=\"iso-8859-1\"; name=\"square.txt\"
+Content-Transfer-Encoding: binary
+
+" "
+
+------- =_aaaaaaaaaa0--
+" >"$expected"
+## output_content() in mhoutsbr.c can't handle binary content.
+## mhfixmsg last -decodetext binary -outfile "$actual"
+## check "$expected" "$actual"
+rm -f "$expected"
+rmm last
+
+
+# check -textcodeset
+# Also checks preservation of attributes after one (charset) that is
+# modified.
+cat >"$expected" <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg textcodeset test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="utf-8"; name="square.txt"
+Content-Disposition: attachment; filename="square.txt"
+Content-Transfer-Encoding: 8bit
+
+½²=¼
+
+------- =_aaaaaaaaaa0--
+EOF
+
+#### Generated the encoded text below with:
+####   $ printf '\xbd\xb2=\xbc\n' | base64
+msgfile=`mhpath new`
+cat >$msgfile <<EOF
+To: recipient@example.com
+From: sender@example.com
+Subject: mhfixmsg textcodeset test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+------- =_aaaaaaaaaa0
+Content-Type: text/plain; charset="iso-8859-1"; name="square.txt"
+Content-Disposition: attachment; filename="square.txt"
+Content-Transfer-Encoding: base64
+
+vbI9vAo=
+
+------- =_aaaaaaaaaa0--
+EOF
+
+set +e
+mhfixmsg last -textcodeset utf-8 -outfile "$actual" 2>"$actual.err"
+if grep "mhfixmsg: Can't convert .* to .* without iconv" "$actual.err" \
+  >/dev/null; then
+  echo skipping -textcodeset check because nmh was built without iconv
+  set +e
+  rm -f "$expected" "$actual" "$actual.err"
+else
+  set +e
+  check "$expected" "$actual"
+  rm "$actual.err"
+fi
+
+
+# check -nofixboundary
+cat >"$expected" <<EOF
+EOF
+
+cat >`mhpath new` <<EOF
+Date: Fri, 13 May 2011 08:21:12 -0500
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_000_1781A17_01CC1147.81E9467A"
+Content-Transfer-Encoding: 8bit
+MIME-Version: 1.0
+From: <sender@example.com>
+To: <recipient@example.com>
+Subject: mhfixmsg bad boundary test
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_1781A1A_01CC1147.81EBA8D4
+Content-Type: text/plain
+
+The boundaries of this part don't match the header boundary.
+
+------=_NextPart_000_1781A1A_01CC1147.81EBA8D4--
+EOF
+
+cp -p `mhpath last` `mhpath new`
+
+run_test 'mhfixmsg last -nofixboundary' ''
+check "$MH_TEST_DIR"/Mail/inbox/16 "$MH_TEST_DIR"/Mail/inbox/17 'keep first'
+
+
+# check that message is not output when fed through stdin
+mhfixmsg -file - -verbose <`mhpath last` >"$actual" 2>"$actual.err"
+check "$expected" "$actual"
+if grep "mhfixmsg: $MH_TEST_DIR/Mail/.*, fix multipart boundary" \
+   "$actual.err" >/dev/null; then
+  rm -f "$actual.err"
+else
+  echo "$0: test failed, output is in $actual.err."
+  failed=`expr ${failed:-0} + 1`
+fi
+
+
+# check fixup of erroneous boundary in multipart (-fixboundary,
+# enabled by default)
+# check -verbose
+cat >"$expected" <<EOF
+Date: Fri, 13 May 2011 08:21:12 -0500
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_000_1781A1A_01CC1147.81EBA8D4"
+Content-Transfer-Encoding: 8bit
+MIME-Version: 1.0
+From: <sender@example.com>
+To: <recipient@example.com>
+Subject: mhfixmsg bad boundary test
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_1781A1A_01CC1147.81EBA8D4
+Content-Type: text/plain
+
+The boundaries of this part don't match the header boundary.
+
+------=_NextPart_000_1781A1A_01CC1147.81EBA8D4--
+EOF
+
+run_test 'mhfixmsg last -outfile '"$actual"' -verbose' \
+         "mhfixmsg: 16, fix multipart boundary"
+check "$expected" "$actual"
+
+
+# check that text/plain part is added to lone text/html in multipart/related
+cat >"$expected" <<EOF
+MIME-Version: 1.0
+Date: Tue, 26 Feb 2013 18:07:20 -0600
+Subject: multipart/related, not /alternative
+Content-Type: multipart/related;
+        boundary="----=_Part_90310_101292502.1"
+
+------=_Part_90310_101292502.1
+Content-Type: multipart/alternative; boundary="----=_nmh-multipart1"
+
+------=_nmh-multipart1
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+This is the real content.
+
+------=_nmh-multipart1
+Content-Type: text/html; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+<html><head>
+    <meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
+    <title>HTML Content</title>
+  </head>
+  <body>
+    This is the real content.
+  </body>
+</html>
+
+------=_nmh-multipart1--
+
+------=_Part_90310_101292502.1
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+Your email client does not support HTML messages
+
+------=_Part_90310_101292502.1--
+EOF
+
+cat >`mhpath new` <<EOF
+MIME-Version: 1.0
+Date: Tue, 26 Feb 2013 18:07:20 -0600
+Subject: multipart/related, not /alternative
+Content-Type: multipart/related;
+        boundary="----=_Part_90310_101292502.1"
+
+------=_Part_90310_101292502.1
+Content-Type: text/html; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+<html><head>
+    <meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
+    <title>HTML Content</title>
+  </head>
+  <body>
+    This is the real content.
+  </body>
+</html>
+------=_Part_90310_101292502.1
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+Your email client does not support HTML messages
+------=_Part_90310_101292502.1--
+EOF
+
+if [ $can_reformat_texthtml -eq 1 ]; then
+  #### lynx inserts multiple blank lines, so use uniq to squeeze them.
+  mhfixmsg last -outfile - | uniq >"$actual"
+  check "$expected" "$actual"
+else
+  rm -f "$expected"
+fi
+
+# check handling of rfc822 message type
+cat >"$expected" <<EOF
+From: Test <test@example.com>
+To: Some User <user@example.com>
+Date: Fri, 29 Sep 2006 00:00:00
+Message-Id: @test.nmh
+Subject: message with message/rfc822 attachment
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0"
+
+and some garbage before the attachment
+
+------- =_aaaaaaaaaa0
+Content-Type: message/rfc822; name="1552"; charset="us-ascii"
+Content-Description: 1552
+Content-Disposition: attachment; filename="1552"
+
+From: Test <test@example.com>
+To: <another_user@example.com>
+Date: Thu, 28 Sep 2006 00:00:00
+Message-Id: @test.nmh
+Subject: message/rfc822 attachment
+
+This is an RFC-822 message.
+
+------- =_aaaaaaaaaa0--
+
+and some garbage at the end
+EOF
+
+cat >`mhpath new` <<EOF
+From: Test <test@example.com>
+To: Some User <user@example.com>
+Date: Fri, 29 Sep 2006 00:00:00
+Message-Id: @test.nmh
+Subject: message with message/rfc822 attachment
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaa0"
+
+and some garbage before the attachment
+
+------- =_aaaaaaaaaa0
+Content-Type: message/rfc822; name="1552"; charset="us-ascii"
+Content-Description: 1552
+Content-Disposition: attachment; filename="1552"
+
+From: Test <test@example.com>
+To: <another_user@example.com>
+Date: Thu, 28 Sep 2006 00:00:00
+Message-Id: @test.nmh
+Subject: message/rfc822 attachment
+
+This is an RFC-822 message.
+
+------- =_aaaaaaaaaa0--
+
+and some garbage at the end
+EOF
+
+run_test 'mhfixmsg last -outfile '"$actual" ''
+check "$expected" "$actual"
+
+
+# check rmmproc
+cat >"$MH_TEST_DIR/Mail/rmmproc" <<'EOF'
+mv "$1" "$1.backup"
+EOF
+chmod a+x "${MH_TEST_DIR}/Mail/rmmproc"
+echo "rmmproc: ${MH_TEST_DIR}/Mail/rmmproc" >>"$MH"
+cp "${MH_TEST_DIR}/Mail/inbox/14" "${MH_TEST_DIR}/Mail/inbox/14.original"
+
+run_test 'mhfixmsg 14' ''
+check "${MH_TEST_DIR}/Mail/inbox/14.backup" \
+      "${MH_TEST_DIR}/Mail/inbox/14.original"
+
+
+# make sure there are no tmp files left over
+find "$MH_TEST_DIR/Mail" \( -name 'mhfix*' -o -name ',mhfix*' \) -print \
+  >"$actual"
+cat >"$expected" <<EOF
+EOF
+
+check "$expected" "$actual"
+
+
+exit $failed
diff --git a/uip/mhfixmsg.c b/uip/mhfixmsg.c
new file mode 100644 (file)
index 0000000..1f240e4
--- /dev/null
@@ -0,0 +1,1874 @@
+/*
+ * mhfixmsg.c -- rewrite a message with various tranformations
+ *
+ * This code is Copyright (c) 2002 and 2013, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include <h/mh.h>
+#include <h/mime.h>
+#include <h/mhparse.h>
+#include <h/utils.h>
+#include <h/signals.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#ifdef HAVE_ICONV
+#   include <iconv.h>
+#endif
+
+#define MHFIXMSG_SWITCHES \
+    X("decodetext 8bit|7bit", 0, DECODETEXTSW) \
+    X("nodecodetext", 0, NDECODETEXTSW) \
+    X("textcodeset", 0, TEXTCODESETSW) \
+    X("notextcodeset", 0, NTEXTCODESETSW) \
+    X("reformat", 0, REFORMATSW) \
+    X("noreformat", 0, NREFORMATSW) \
+    X("fixboundary", 0, FIXBOUNDARYSW) \
+    X("nofixboundary", 0, NFIXBOUNDARYSW) \
+    X("fixcte", 0, FIXCTESW) \
+    X("nofixcte", 0, NFIXCTESW) \
+    X("file file", 0, FILESW) \
+    X("outfile file", 0, OUTFILESW) \
+    X("verbose", 0, VERBSW) \
+    X("noverbose", 0, NVERBSW) \
+    X("version", 0, VERSIONSW) \
+    X("help", 0, HELPSW) \
+
+#define X(sw, minchars, id) id,
+DEFINE_SWITCH_ENUM(MHFIXMSG);
+#undef X
+
+#define X(sw, minchars, id) { sw, minchars, id },
+DEFINE_SWITCH_ARRAY(MHFIXMSG, switches);
+#undef X
+
+
+int verbosw;
+int debugsw; /* Needed by mhparse.c. */
+
+#define quitser pipeser
+
+/* mhparse.c */
+extern char *tmp;                             /* directory to place tmp files */
+extern int skip_mp_cte_check;                 /* flag to InitMultiPart */
+extern int suppress_bogus_mp_content_warning; /* flag to InitMultiPart */
+extern int bogus_mp_content;                  /* flag from InitMultiPart */
+CT parse_mime (char *);
+void reverse_parts (CT);
+
+/* mhoutsbr.c */
+int output_message (CT, char *);
+
+/* mhshowsbr.c */
+int show_content_aux (CT, int, int, char *, char *);
+
+/* mhmisc.c */
+void flush_errors (void);
+
+/* mhfree.c */
+extern CT *cts;
+void freects_done (int) NORETURN;
+
+/*
+ * static prototypes
+ */
+typedef struct fix_transformations {
+    int fixboundary;
+    int fixcte;
+    int reformat;
+    int decodetext;
+    char *textcodeset;
+} fix_transformations;
+
+int mhfixmsgsbr (CT *, const fix_transformations *, char *);
+static void reverse_alternative_parts (CT);
+static int fix_boundary (CT *, int *);
+static int get_multipart_boundary (CT, char **);
+static int replace_boundary (CT, char *, const char *);
+static char *update_attr (char *, const char *, const char *e);
+static int fix_multipart_cte (CT, int *);
+static int set_ce (CT, int);
+static int ensure_text_plain (CT *, CT, int *);
+static CT build_text_plain_part (CT);
+static CT divide_part (CT);
+static void copy_ctinfo (CI, CI);
+static int decode_part (CT);
+static int reformat_part (CT, char *, char *, char *, int);
+static int charset_encoding (CT);
+static CT build_multipart_alt (CT, CT, int, int);
+static int boundary_in_content (FILE **, char *, const char *);
+static void transfer_noncontent_headers (CT, CT);
+static int set_ct_type (CT, int type, int subtype, int encoding);
+static int decode_text_parts (CT, int, int *);
+static int content_encoding (CT);
+static int convert_codesets (CT, char *, int *);
+static int convert_codeset (CT, char *, int *);
+static int write_content (CT, char *, char *, int, int);
+static int remove_file (char *);
+static void report (char *, char *, char *, ...);
+static char *upcase (char *);
+static void pipeser (int);
+
+
+int
+main (int argc, char **argv) {
+    int msgnum;
+    char *cp, *file = NULL, *folder = NULL;
+    char *maildir, buf[100], *outfile = NULL;
+    char **argp, **arguments;
+    struct msgs_array msgs = { 0, 0, NULL };
+    struct msgs *mp = NULL;
+    CT *ctp;
+    FILE *fp;
+    int using_stdin = 0;
+    int status = OK;
+    fix_transformations fx;
+    fx.reformat = fx.fixcte = fx.fixboundary = 1;
+    fx.decodetext = CE_8BIT;
+    fx.textcodeset = NULL;
+
+    done = freects_done;
+
+#ifdef LOCALE
+    setlocale(LC_ALL, "");
+#endif
+    invo_name = r1bindex (argv[0], '/');
+
+    /* read user profile/context */
+    context_read();
+
+    arguments = getarguments (invo_name, argc, argv, 1);
+    argp = arguments;
+
+    /*
+     * Parse arguments
+     */
+    while ((cp = *argp++)) {
+        if (*cp == '-') {
+            switch (smatch (++cp, switches)) {
+            case AMBIGSW:
+                ambigsw (cp, switches);
+                done (1);
+            case UNKWNSW:
+                adios (NULL, "-%s unknown", cp);
+
+            case HELPSW:
+                snprintf (buf, sizeof buf, "%s [+folder] [msgs] [switches]",
+                        invo_name);
+                print_help (buf, switches, 1);
+                done (0);
+            case VERSIONSW:
+                print_version(invo_name);
+                done (0);
+
+            case DECODETEXTSW:
+                if (! (cp = *argp++)  ||  *cp == '-')
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                if (! strcasecmp (cp, "8bit")) {
+                    fx.decodetext = CE_8BIT;
+                } else if (! strcasecmp (cp, "7bit")) {
+                    fx.decodetext = CE_7BIT;
+                } else {
+                    adios (NULL, "invalid argument to %s", argp[-2]);
+                }
+                continue;
+            case NDECODETEXTSW:
+                fx.decodetext = 0;
+                continue;
+            case TEXTCODESETSW:
+                if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                fx.textcodeset = cp;
+                continue;
+            case NTEXTCODESETSW:
+                fx.textcodeset = 0;
+                continue;
+            case FIXBOUNDARYSW:
+                fx.fixboundary = 1;
+                continue;
+            case NFIXBOUNDARYSW:
+                fx.fixboundary = 0;
+                continue;
+            case FIXCTESW:
+                fx.fixcte = 1;
+                continue;
+            case NFIXCTESW:
+                fx.fixcte = 0;
+                continue;
+            case REFORMATSW:
+                fx.reformat = 1;
+                continue;
+            case NREFORMATSW:
+                fx.reformat = 0;
+                continue;
+
+            case FILESW:
+                if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                file = *cp == '-'  ?  add (cp, NULL)  :  path (cp, TFILE);
+                continue;
+
+            case OUTFILESW:
+                if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                outfile = *cp == '-'  ?  add (cp, NULL)  :  path (cp, TFILE);
+                continue;
+
+            case VERBSW:
+                verbosw = 1;
+                continue;
+            case NVERBSW:
+                verbosw = 0;
+                continue;
+            }
+        }
+        if (*cp == '+' || *cp == '@') {
+            if (folder)
+                adios (NULL, "only one folder at a time!");
+            else
+                folder = pluspath (cp);
+        } else
+                app_msgarg(&msgs, cp);
+    }
+
+    SIGNAL (SIGQUIT, quitser);
+    SIGNAL (SIGPIPE, pipeser);
+
+    /*
+     * Read the standard profile setup
+     */
+    if ((fp = fopen (cp = etcpath ("mhn.defaults"), "r"))) {
+        readconfig ((struct node **) 0, fp, cp, 0);
+        fclose (fp);
+    }
+
+    /*
+     * Check for storage directory.  If specified,
+     * then store temporary files there.  Else we
+     * store them in standard nmh directory.
+     */
+    if ((cp = context_find (nmhstorage)) && *cp)
+        tmp = concat (cp, "/", invo_name, NULL);
+    else
+        tmp = add (m_maildir (invo_name), NULL);
+
+    suppress_bogus_mp_content_warning = skip_mp_cte_check = 1;
+
+    if (! context_find ("path"))
+        free (path ("./", TFOLDER));
+
+    if (file && msgs.size)
+        adios (NULL, "cannot specify msg and file at same time!");
+
+    /*
+     * check if message is coming from file
+     */
+    if (file) {
+        /* If file is stdin, create a tmp file name before parse_mime()
+           has a chance, because it might put in on a different
+           filesystem than the output file.  Instead, put it in the
+           user's preferred tmp directory. */
+        CT ct;
+
+        if (! strcmp ("-", file)) {
+            int fd;
+            char *cp;
+
+            using_stdin = 1;
+
+            if ((cp = m_mktemp2 (tmp, invo_name, &fd, NULL)) == NULL) {
+                adios (NULL, "unable to create temporary file");
+            } else {
+                free (file);
+                file = add (cp, NULL);
+                chmod (file, 0600);
+                cpydata (STDIN_FILENO, fd, "-", file);
+            }
+
+            if (close (fd)) {
+                unlink (file);
+                adios (NULL, "failed to write temporary file");
+            }
+        }
+
+        if (! (cts = (CT *) calloc ((size_t) 2, sizeof *cts)))
+            adios (NULL, "out of memory");
+        ctp = cts;
+
+        if ((ct = parse_mime (file)))
+            *ctp++ = ct;
+    } else {
+        /*
+         * message(s) are coming from a folder
+         */
+        CT ct;
+
+        if (! msgs.size)
+            app_msgarg(&msgs, "cur");
+        if (! folder)
+            folder = getfolder (1);
+        maildir = m_maildir (folder);
+
+        if (chdir (maildir) == NOTOK)
+            adios (maildir, "unable to change directory to");
+
+        /* read folder and create message structure */
+        if (! (mp = folder_read (folder)))
+            adios (NULL, "unable to read folder %s", folder);
+
+        /* check for empty folder */
+        if (mp->nummsg == 0)
+            adios (NULL, "no messages in %s", folder);
+
+        /* parse all the message ranges/sequences and set SELECTED */
+        for (msgnum = 0; msgnum < msgs.size; msgnum++)
+            if (! m_convert (mp, msgs.msgs[msgnum]))
+                done (1);
+        seq_setprev (mp);       /* set the previous-sequence */
+
+        if (! (cts = (CT *) calloc ((size_t) (mp->numsel + 1), sizeof *cts)))
+            adios (NULL, "out of memory");
+        ctp = cts;
+
+        for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
+            if (is_selected(mp, msgnum)) {
+                char *msgnam;
+
+                msgnam = m_name (msgnum);
+                if ((ct = parse_mime (msgnam)))
+                    *ctp++ = ct;
+            }
+        }
+
+        /*
+         * This is a hack.  If we are using an external rmmproc,
+         * then save the current folder to the context file,
+         * so the external rmmproc will remove files from the correct
+         * directory.  This should be moved to folder_delmsgs().
+         */
+        if (rmmproc) {
+            context_replace (pfolder, folder);/* update current folder  */
+            seq_setcur (mp, mp->hghsel);      /* update current message */
+            seq_save (mp);                    /* synchronize sequences  */
+            context_save ();                  /* save the context file  */
+            fflush (stdout);
+        }
+    }
+
+    if (*cts) {
+        for (ctp = cts; *ctp; ++ctp) {
+            status += mhfixmsgsbr (ctp, &fx, outfile);
+
+            if (using_stdin) {
+                unlink (file);
+
+                if (! outfile) {
+                    /* Just calling m_backup() unlinks the backup file. */
+                    (void) m_backup (file);
+                }
+            }
+        }
+    } else {
+        status = 1;
+    }
+
+    free (outfile);
+    free (tmp);
+    free (file);
+
+    /* done is freects_done, which will clean up all of cts. */
+    done (status);
+    return NOTOK;
+}
+
+
+int
+mhfixmsgsbr (CT *ctp, const fix_transformations *fx, char *outfile) {
+    /* Store input filename in case one of the transformations, i.e.,
+       fix_boundary(), rewrites to a tmp file. */
+    char *input_filename = add ((*ctp)->c_file, NULL);
+    int modify_inplace = 0;
+    int message_mods = 0;
+    int status = OK;
+
+    if (outfile == NULL) {
+        modify_inplace = 1;
+
+        if ((*ctp)->c_file) {
+            outfile = add (m_mktemp2 (tmp, invo_name, NULL, NULL), NULL);
+        } else {
+            adios (NULL, "missing both input and output filenames\n");
+        }
+    }
+
+    reverse_alternative_parts (*ctp);
+    if (status == OK  &&  fx->fixboundary) {
+        status = fix_boundary (ctp, &message_mods);
+    }
+    if (status == OK  &&  fx->fixcte) {
+        status = fix_multipart_cte (*ctp, &message_mods);
+    }
+    if (status == OK  &&  fx->reformat) {
+        status = ensure_text_plain (ctp, NULL, &message_mods);
+    }
+    if (status == OK  &&  fx->decodetext) {
+        status = decode_text_parts (*ctp, fx->decodetext, &message_mods);
+    }
+    if (status == OK  &&  fx->textcodeset != NULL) {
+        status = convert_codesets (*ctp, fx->textcodeset, &message_mods);
+    }
+
+    if (! (*ctp)->c_umask) {
+        /* Set the umask for the contents file.  This currently
+           isn't used but just in case it is in the future. */
+        struct stat st;
+
+        if (stat ((*ctp)->c_file, &st) != NOTOK) {
+            (*ctp)->c_umask = ~(st.st_mode & 0777);
+        } else {
+            (*ctp)->c_umask = ~m_gmprot();
+        }
+    }
+
+    /*
+     * Write the content to a file
+     */
+    if (status == OK) {
+        status = write_content (*ctp, input_filename, outfile, modify_inplace,
+                                message_mods);
+    } else if (! modify_inplace) {
+        /* Something went wrong.  Output might be expected, such
+           as if this were run as a filter.  Just copy the input
+           to the output. */
+        int in = open (input_filename, O_RDONLY);
+        int out = strcmp (outfile, "-")
+            ?  open (outfile, O_WRONLY | O_CREAT, m_gmprot ())
+            :  STDOUT_FILENO;
+
+        if (in != -1  &&  out != -1) {
+            cpydata (in, out, input_filename, outfile);
+        } else {
+            status = NOTOK;
+        }
+
+        close (out);
+        close (in);
+    }
+
+    if (modify_inplace) {
+        free (outfile);
+        outfile = NULL;
+    }
+
+    free (input_filename);
+
+    return status;
+}
+
+
+/* parse_mime() arranges alternates in reverse (priority) order, so
+   reverse them back.  This will put a text/plain part at the front of
+   a multipart/alternative part, for example, where it belongs. */
+static void
+reverse_alternative_parts (CT ct) {
+    if (ct->c_type == CT_MULTIPART) {
+        struct multipart *m = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        if (ct->c_subtype == MULTI_ALTERNATE) {
+            reverse_parts (ct);
+        }
+
+        /* And call recursively on each part of a multipart. */
+        for (part = m->mp_parts; part; part = part->mp_next) {
+            reverse_alternative_parts (part->mp_part);
+        }
+    }
+}
+
+
+static int
+fix_boundary (CT *ct, int *message_mods) {
+    struct multipart *mp;
+    int status = OK;
+
+    if (bogus_mp_content) {
+        mp = (struct multipart *) (*ct)->c_ctparams;
+
+        /*
+         * 1) Get boundary at end of part.
+         * 2) Get boundary at beginning of part and compare to the end-of-part
+         *    boundary.
+         * 3) Write out contents of ct to tmp file, replacing boundary in
+         *    header with boundary from part.  Set c_unlink to 1.
+         * 4) Free ct.
+         * 5) Call parse_mime() on the tmp file, replacing ct.
+         */
+
+        if (mp  &&  mp->mp_start) {
+            char *part_boundary;
+
+            if (get_multipart_boundary (*ct, &part_boundary) == OK) {
+                char *fixed;
+
+                if ((fixed = m_mktemp2 (tmp, invo_name, NULL, &(*ct)->c_fp))) {
+                    if (replace_boundary (*ct, fixed, part_boundary) == OK) {
+                        char *filename = add ((*ct)->c_file, NULL);
+
+                        free_content (*ct);
+                        *ct = parse_mime (fixed);
+                        (*ct)->c_unlink = 1;
+
+                        ++*message_mods;
+                        if (verbosw) {
+                            report (NULL, filename, "fix multipart boundary");
+                        }
+                        free (filename);
+                    } else {
+                        advise (NULL, "unable to replace broken boundary");
+                        status = NOTOK;
+                    }
+                } else {
+                    advise (NULL, "unable to create temporary file");
+                    status = NOTOK;
+                }
+
+                free (part_boundary);
+            }
+        }
+    }
+
+    return status;
+}
+
+
+static int
+get_multipart_boundary (CT ct, char **part_boundary) {
+    char buffer[BUFSIZ];
+    char *end_boundary = NULL;
+    off_t begin = (off_t) ct->c_end > (off_t) (ct->c_begin + sizeof buffer)
+        ?  (off_t) (ct->c_end - sizeof buffer)
+        :  (off_t) ct->c_begin;
+    size_t bytes_read;
+    int status = OK;
+
+    /* This will fail if the boundary spans fread() calls.  BUFSIZ should
+       be big enough, even if it's just 1024, to make that unlikely. */
+
+    /* free_content() will close ct->c_fp. */
+    if (! ct->c_fp  &&  (ct->c_fp = fopen (ct->c_file, "r")) == NULL) {
+        advise (ct->c_file, "unable to open for reading");
+        return NOTOK;
+    }
+
+    /* Get boundary at end of multipart. */
+    while (begin >= (off_t) ct->c_begin) {
+        fseeko (ct->c_fp, begin, SEEK_SET);
+        while ((bytes_read = fread (buffer, 1, sizeof buffer, ct->c_fp)) > 0) {
+            char *end = buffer + bytes_read - 1;
+            char *cp;
+
+            if ((cp = rfind_str (buffer, bytes_read, "--"))) {
+                /* Trim off trailing "--" and anything beyond. */
+                *cp-- = '\0';
+                if ((end = rfind_str (buffer, cp - buffer, "\n"))) {
+                    if (strlen (end) > 3  &&  *end++ == '\n'  &&
+                        *end++ == '-'  &&  *end++ == '-') {
+                        end_boundary = add (end, NULL);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (! end_boundary  &&  begin > (off_t) (ct->c_begin + sizeof buffer)) {
+            begin -= sizeof buffer;
+        } else {
+            break;
+        }
+    }
+
+    /* Get boundary at beginning of multipart. */
+    if (end_boundary) {
+        fseeko (ct->c_fp, ct->c_begin, SEEK_SET);
+        while ((bytes_read = fread (buffer, 1, sizeof buffer, ct->c_fp)) > 0) {
+            if (bytes_read >= strlen (end_boundary)) {
+                char *cp = find_str (buffer, bytes_read, end_boundary);
+
+                if (cp  &&  cp - buffer >= 2  &&  *--cp == '-'  &&
+                    *--cp == '-'  &&  (cp > buffer  &&  *--cp == '\n')) {
+                    break;
+                }
+                /* Else the start and end boundaries didn't match, or
+                   the start boundary doesn't begin with "\n--" (or
+                   "--" if at the beginning of buffer).  Keep trying. */
+            } else {
+                status = NOTOK;
+            }
+        }
+    }
+
+    if (status == OK) {
+        *part_boundary = end_boundary;
+    } else {
+        *part_boundary = NULL;
+        free (end_boundary);
+    }
+
+    return status;
+}
+
+
+/* Open and copy ct->c_file to file, replacing the multipart boundary. */
+static int
+replace_boundary (CT ct, char *file, const char *boundary) {
+    FILE *fpin, *fpout;
+    int compnum, state;
+    char buf[BUFSIZ], name[NAMESZ];
+    char *np, *vp;
+    m_getfld_state_t gstate = 0;
+    int status = OK;
+
+    if (ct->c_file == NULL) {
+        advise (NULL, "missing input filename");
+        return NOTOK;
+    }
+
+    if ((fpin = fopen (ct->c_file, "r")) == NULL) {
+        advise (ct->c_file, "unable to open for reading");
+        return NOTOK;
+    }
+
+    if ((fpout = fopen (file, "w")) == NULL) {
+        fclose (fpin);
+        advise (file, "unable to open for writing");
+        return NOTOK;
+    }
+
+    for (compnum = 1;;) {
+        int bufsz = (int) sizeof buf;
+
+        switch (state = m_getfld (&gstate, name, buf, &bufsz, fpin)) {
+        case FLD:
+        case FLDPLUS:
+            compnum++;
+
+            /* get copies of the buffers */
+            np = add (name, NULL);
+            vp = add (buf, NULL);
+
+            /* if necessary, get rest of field */
+            while (state == FLDPLUS) {
+                bufsz = sizeof buf;
+                state = m_getfld (&gstate, name, buf, &bufsz, fpin);
+                vp = add (buf, vp);     /* add to previous value */
+            }
+
+            if (strcasecmp (TYPE_FIELD, np)) {
+                fprintf (fpout, "%s:%s", np, vp);
+            } else {
+                char *new_boundary = update_attr (vp, "boundary=", boundary);
+
+                fprintf (fpout, "%s:%s\n", np, new_boundary);
+                free (new_boundary);
+            }
+
+            free (vp);
+            free (np);
+
+            continue;
+
+        case BODY:
+            fputs ("\n", fpout);
+            /* buf will have a terminating NULL, skip it. */
+            fwrite (buf, 1, bufsz-1, fpout);
+            continue;
+
+        case FILEEOF:
+            break;
+
+        case LENERR:
+        case FMTERR:
+            advise (NULL, "message format error in component #%d", compnum);
+            status = NOTOK;
+            break;
+
+        default:
+            advise (NULL, "getfld() returned %d", state);
+            status = NOTOK;
+            break;
+        }
+
+        break;
+    }
+
+    m_getfld_state_destroy (&gstate);
+    fclose (fpout);
+    fclose (fpin);
+
+    return status;
+}
+
+
+/* Change the value of a name=value pair in a header field body.
+   If the name isn't there, append them.  In any case, a new
+   string will be allocated and must be free'd by the caller.
+   Trims any trailing newlines. */
+static char *
+update_attr (char *body, const char *name, const char *value) {
+    char *bp = nmh_strcasestr (body, name);
+    char *new_body;
+
+    if (bp) {
+        char *other_attrs = strchr (bp, ';');
+
+        *(bp + strlen (name)) = '\0';
+        new_body = concat (body, "\"", value, "\"", NULL);
+
+        if (other_attrs) {
+            char *cp;
+
+            /* Trim any trailing newlines. */
+            for (cp = &other_attrs[strlen (other_attrs) - 1];
+                 cp > other_attrs  &&  *cp == '\n';
+                 *cp-- = '\0') continue;
+            new_body = add (other_attrs, new_body);
+        }
+    } else {
+        char *cp;
+
+        /* Append name/value pair, after first removing a final newline
+           and (extraneous) semicolon. */
+        if (*(cp = &body[strlen (body) - 1]) == '\n') *cp = '\0';
+        if (*(cp = &body[strlen (body) - 1]) == ';') *cp = '\0';
+        new_body = concat (body, "; ", name, "\"", value, "\"", NULL);
+    }
+
+    return new_body;
+}
+
+
+static int
+fix_multipart_cte (CT ct, int *message_mods) {
+    int status = OK;
+
+    if (ct->c_type == CT_MULTIPART) {
+        struct multipart *m;
+        struct part *part;
+
+        if (ct->c_encoding != CE_7BIT  &&  ct->c_encoding != CE_8BIT  &&
+            ct->c_encoding != CE_BINARY) {
+            HF hf;
+
+            for (hf = ct->c_first_hf; hf; hf = hf->next) {
+                char *name = hf->name;
+                for (; *name && isspace ((unsigned char) *name); ++name) {
+                    continue;
+                }
+
+                if (! strncasecmp (name, ENCODING_FIELD,
+                                   strlen (ENCODING_FIELD))) {
+                    char *prefix = "Nmh-REPLACED-INVALID-";
+                    HF h = mh_xmalloc (sizeof *h);
+
+                    h->name = add (hf->name, NULL);
+                    h->hf_encoding = hf->hf_encoding;
+                    h->next = hf->next;
+                    hf->next = h;
+
+                    /* Retain old header but prefix its name. */
+                    free (hf->name);
+                    hf->name = concat (prefix, h->name, NULL);
+
+                    ++*message_mods;
+                    if (verbosw) {
+                        char *encoding = cpytrim (hf->value);
+                        report (ct->c_partno, ct->c_file,
+                                "replace Content-Transfer-Encoding of %s "
+                                "with 8 bit", encoding);
+                        free (encoding);
+                    }
+
+                    h->value = add (" 8bit\n", NULL);
+
+                    /* Don't need to warn for multiple C-T-E header
+                       fields, parse_mime() already does that.  But
+                       if there are any, fix them all as necessary. */
+                    hf = h;
+                }
+            }
+
+            set_ce (ct, CE_8BIT);
+        }
+
+        m = (struct multipart *) ct->c_ctparams;
+        for (part = m->mp_parts; part; part = part->mp_next) {
+            if (fix_multipart_cte (part->mp_part, message_mods) != OK) {
+                status = NOTOK;
+                break;
+            }
+        }
+    }
+
+    return status;
+}
+
+
+static int
+set_ce (CT ct, int encoding) {
+    const char *ce = ce_str (encoding);
+    const struct str2init *ctinit = get_ce_method (ce);
+
+    if (ctinit) {
+        char *cte = concat (" ", ce, "\n", NULL);
+        int found_cte = 0;
+        HF hf;
+        /* Decoded contents might be in ct->c_cefile.ce_file, if the
+           caller is decode_text_parts ().  Save because we'll
+           overwrite below. */
+        struct cefile decoded_content_info = ct->c_cefile;
+
+        ct->c_encoding = encoding;
+
+        ct->c_ctinitfnx = ctinit->si_init;
+        /* This will assign ct->c_cefile with an all-0 struct, which
+           is what we want. */
+        (*ctinit->si_init) (ct);
+        /* After returning, the caller should set
+           ct->c_cefile.ce_file to the name of the file containing
+           the contents. */
+
+        /* Restore the cefile. */
+        ct->c_cefile = decoded_content_info;
+
+        /* Update/add Content-Transfer-Encoding header field. */
+        for (hf = ct->c_first_hf; hf; hf = hf->next) {
+            if (! strcasecmp (ENCODING_FIELD, hf->name)) {
+                found_cte = 1;
+                free (hf->value);
+                hf->value = cte;
+            }
+        }
+        if (! found_cte) {
+            add_header (ct, add (ENCODING_FIELD, NULL), cte);
+        }
+
+        /* Update c_celine.  It's used only by mhlist -debug. */
+        free (ct->c_celine);
+        ct->c_celine = add (cte, NULL);
+
+        return OK;
+    } else {
+        return NOTOK;
+    }
+}
+
+
+/* Make sure each text part has a corresponding text/plain part. */
+static int
+ensure_text_plain (CT *ct, CT parent, int *message_mods) {
+    int status = OK;
+
+    switch ((*ct)->c_type) {
+    case CT_TEXT: {
+        int has_text_plain = 0;
+
+        /* Nothing to do for text/plain. */
+        if ((*ct)->c_subtype == TEXT_PLAIN) return OK;
+
+        if (parent  &&  parent->c_type == CT_MULTIPART  &&
+            parent->c_subtype == MULTI_ALTERNATE) {
+            struct multipart *mp = (struct multipart *) parent->c_ctparams;
+            struct part *part;
+            int new_subpart_number = 1;
+
+            /* See if there is a sibling text/plain. */
+            for (part = mp->mp_parts; part; part = part->mp_next) {
+                ++new_subpart_number;
+                if (part->mp_part->c_type == CT_TEXT  &&
+                    part->mp_part->c_subtype == TEXT_PLAIN) {
+                    has_text_plain = 1;
+                    break;
+                }
+            }
+
+            if (! has_text_plain) {
+                /* Parent is a multipart/alternative.  Insert a new
+                   text/plain subpart. */
+                struct part *new_part = mh_xmalloc (sizeof *new_part);
+
+                if ((new_part->mp_part = build_text_plain_part (*ct))) {
+                    char buffer[16];
+                    snprintf (buffer, sizeof buffer, "%d", new_subpart_number);
+
+                    new_part->mp_next = mp->mp_parts;
+                    mp->mp_parts = new_part;
+                    new_part->mp_part->c_partno =
+                        concat (parent->c_partno ? parent->c_partno : "1", ".",
+                                buffer, NULL);
+
+                    ++*message_mods;
+                    if (verbosw) {
+                        report (parent->c_partno, parent->c_file,
+                                "insert text/plain part");
+                    }
+                } else {
+                    free_content (new_part->mp_part);
+                    free (new_part);
+                    status = NOTOK;
+                }
+            }
+        } else {
+            /* Slip new text/plain part into a new multipart/alternative. */
+            CT tp_part = build_text_plain_part (*ct);
+            CT mp_alt = build_multipart_alt (*ct, tp_part, CT_MULTIPART,
+                                             MULTI_ALTERNATE);
+            struct multipart *mp = (struct multipart *) mp_alt->c_ctparams;
+
+            if (mp  &&  mp->mp_parts  &&  (mp->mp_parts->mp_part = tp_part)) {
+                /* Make the new multipart/alternative the parent. */
+                *ct = mp_alt;
+
+                ++*message_mods;
+                if (verbosw) {
+                    report ((*ct)->c_partno, (*ct)->c_file,
+                            "insert text/plain part");
+                }
+            } else {
+                free_content (tp_part);
+
+                /* Undo enough of what build_multipart_alt() did so
+                   that free_content() can be called on mp_alt. */
+                mp->mp_parts->mp_part = NULL;
+                mp->mp_parts->mp_next->mp_part = NULL;
+                free_content (mp_alt);
+                status = NOTOK;
+            }
+        }
+        break;
+    }
+
+    case CT_MULTIPART: {
+        struct multipart *mp = (struct multipart *) (*ct)->c_ctparams;
+        struct part *part;
+
+        for (part = mp->mp_parts; status == OK && part; part = part->mp_next) {
+            if ((*ct)->c_type == CT_MULTIPART) {
+                status = ensure_text_plain (&part->mp_part, *ct, message_mods);
+            }
+        }
+        break;
+    }
+
+    case CT_MESSAGE:
+        if ((*ct)->c_subtype == MESSAGE_EXTERNAL) {
+            struct exbody *e;
+
+            e = (struct exbody *) (*ct)->c_ctparams;
+            status = ensure_text_plain (&e->eb_content, *ct, message_mods);
+        }
+        break;
+    }
+
+    return status;
+}
+
+
+static CT
+build_text_plain_part (CT encoded_part) {
+    CT tp_part = divide_part (encoded_part);
+    char *tmp_plain_file = NULL;
+
+    if (decode_part (tp_part) == OK) {
+        /* Now, tp_part->c_cefile.ce_file is the name of the tmp file that
+           contains the decoded contents.  And the decoding function, such
+           as openQuoted, will have set ...->ce_unlink to 1 so that it will
+           be unlinked by free_content (). */
+        tmp_plain_file = add (m_mktemp2 (tmp, invo_name, NULL, NULL), NULL);
+        if (reformat_part (tp_part, tmp_plain_file,
+                           tp_part->c_ctinfo.ci_type,
+                           tp_part->c_ctinfo.ci_subtype,
+                           tp_part->c_type) == OK) {
+            return tp_part;
+        }
+    }
+
+    free_content (tp_part);
+    unlink (tmp_plain_file);
+    free (tmp_plain_file);
+
+    return NULL;
+}
+
+
+static CT
+divide_part (CT ct) {
+    CT new_part;
+
+    if ((new_part = (CT) calloc (1, sizeof *new_part)) == NULL)
+        adios (NULL, "out of memory");
+
+    /* Just copy over what is needed for decoding.  c_vrsn and
+       c_celine aren't necessary. */
+    new_part->c_file = add (ct->c_file, NULL);
+    new_part->c_begin = ct->c_begin;
+    new_part->c_end = ct->c_end;
+    copy_ctinfo (&new_part->c_ctinfo, &ct->c_ctinfo);
+    new_part->c_type = ct->c_type;
+    new_part->c_cefile = ct->c_cefile;
+    new_part->c_encoding = ct->c_encoding;
+    new_part->c_ctinitfnx = ct->c_ctinitfnx;
+    new_part->c_ceopenfnx = ct->c_ceopenfnx;
+    new_part->c_ceclosefnx = ct->c_ceclosefnx;
+    new_part->c_cesizefnx = ct->c_cesizefnx;
+
+    /* c_ctline is used by reformat__part(), so it can preserve
+       anything after the type/subtype. */
+    new_part->c_ctline = add (ct->c_ctline, NULL);
+
+    return new_part;
+}
+
+
+static void
+copy_ctinfo (CI dest, CI src) {
+    char **s_ap, **d_ap, **s_vp, **d_vp;
+
+    dest->ci_type = src->ci_type ? add (src->ci_type, NULL) : NULL;
+    dest->ci_subtype = src->ci_subtype ? add (src->ci_subtype, NULL) : NULL;
+
+    for (s_ap = src->ci_attrs, d_ap = dest->ci_attrs,
+             s_vp = src->ci_values, d_vp = dest->ci_values;
+         *s_ap;
+         ++s_ap, ++d_ap, ++s_vp, ++d_vp) {
+        *d_ap = add (*s_ap, NULL);
+        *d_vp = *s_vp;
+    }
+    *d_ap = NULL;
+
+    dest->ci_comment = src->ci_comment ? add (src->ci_comment, NULL) : NULL;
+    dest->ci_magic = src->ci_magic ? add (src->ci_magic, NULL) : NULL;
+}
+
+
+static int
+decode_part (CT ct) {
+    char *tmp_decoded;
+    int status;
+
+    tmp_decoded = add (m_mktemp2 (tmp, invo_name, NULL, NULL), NULL);
+    /* The following call will load ct->c_cefile.ce_file with the tmp
+       filename of the decoded content.  tmp_decoded will contain the
+       encoded output, get rid of that. */
+    status = output_message (ct, tmp_decoded);
+    unlink (tmp_decoded);
+    free (tmp_decoded);
+
+    return status;
+}
+
+
+/* Some of the arguments aren't really needed now, but maybe will
+   be in the future for other than text types. */
+static int
+reformat_part (CT ct, char *file, char *type, char *subtype, int c_type) {
+    int output_subtype, output_encoding;
+    char *cp, *cf;
+    int status;
+
+    /* Hacky:  this redirects the output from whatever command is used
+       to show the part to a file.  So, the user can't have any output
+       redirection in that command.
+       Could show_multi() in mhshowsbr.c avoid this? */
+
+    /* Check for invo_name-format-type/subtype. */
+    cp = concat (invo_name, "-format-", type, "/", subtype, NULL);
+    if ((cf = context_find (cp))  &&  *cf != '\0') {
+        if (strchr (cf, '>')) {
+            free (cp);
+            advise (NULL, "'>' prohibited in \"%s\",\nplease fix your "
+                    "%s-format-%s/%s profile entry", cf, invo_name, type,
+                    subtype);
+            return NOTOK;
+        }
+    } else {
+        free (cp);
+
+        /* Check for invo_name-format-type. */
+        cp = concat (invo_name, "-format-", type, NULL);
+        if (! (cf = context_find (cp))  ||  *cf == '\0') {
+            free (cp);
+            if (verbosw) {
+                advise (NULL, "Don't know how to convert %s, there is no "
+                        "%s-format-%s/%s profile entry",
+                        ct->c_file, invo_name, type, subtype);
+            }
+            return NOTOK;
+        }
+
+        if (strchr (cf, '>')) {
+            free (cp);
+            advise (NULL, "'>' prohibited in \"%s\"", cf);
+            return NOTOK;
+        }
+    }
+    free (cp);
+
+    cp = concat (cf, " >", file, NULL);
+    status = show_content_aux (ct, 1, 0, cp, NULL);
+    free (cp);
+
+    /* Unlink decoded content tmp file and free its filename to avoid
+       leaks.  The file stream should already have been closed. */
+    if (ct->c_cefile.ce_unlink) {
+        unlink (ct->c_cefile.ce_file);
+        free (ct->c_cefile.ce_file);
+        ct->c_cefile.ce_file = NULL;
+        ct->c_cefile.ce_unlink = 0;
+    }
+
+    if (c_type == CT_TEXT) {
+        output_subtype = TEXT_PLAIN;
+    } else {
+        /* Set subtype to 0, which is always an UNKNOWN subtype. */
+        output_subtype = 0;
+    }
+    output_encoding = charset_encoding (ct);
+
+    if (set_ct_type (ct, c_type, output_subtype, output_encoding) == OK) {
+        ct->c_cefile.ce_file = file;
+        ct->c_cefile.ce_unlink = 1;
+    } else {
+        ct->c_cefile.ce_unlink = 0;
+        status = NOTOK;
+    }
+
+    return status;
+}
+
+
+/* Identifies 7bit or 8bit content based on charset, if specified. */
+static int
+charset_encoding (CT ct) {
+    int encoding = CE_8BIT;
+    CI ctinfo = &ct->c_ctinfo;
+    char **ap, **vp;
+
+    for (ap = ctinfo->ci_attrs, vp = ctinfo->ci_values; *ap; ++ap, ++vp) {
+        if (! strcasecmp (*ap, "charset")) {
+            /* norm_charmap() is case sensitive. */
+            char *ch = upcase (*vp);
+
+            if (! strcmp (norm_charmap (ch), "US-ASCII")) encoding = CE_7BIT;
+            free (ch);
+            break;
+        }
+    }
+
+    return encoding;
+}
+
+
+static CT
+build_multipart_alt (CT first_alt, CT new_part, int type, int subtype) {
+    char *boundary_prefix = "----=_nmh-multipart";
+    char *boundary = concat (boundary_prefix, first_alt->c_partno, NULL);
+    char *boundary_indicator = "; boundary=";
+    char *typename, *subtypename, *name;
+    CT ct;
+    struct part *p;
+    struct multipart *m;
+    char *cp;
+    const struct str2init *ctinit;
+
+    if ((ct = (CT) calloc (1, sizeof *ct)) == NULL)
+        adios (NULL, "out of memory");
+
+    /* Set up the multipart/alternative part.  These fields of *ct were
+       initialized to 0 by calloc():
+       c_fp, c_unlink, c_begin, c_end,
+       c_vrsn, c_ctline, c_celine,
+       c_id, c_descr, c_dispo, c_partno,
+       c_ctinfo.ci_comment, c_ctinfo.ci_magic,
+       c_cefile, c_encoding,
+       c_digested, c_digest[16], c_ctexbody,
+       c_ctinitfnx, c_ceopenfnx, c_ceclosefnx, c_cesizefnx,
+       c_umask, c_pid, c_rfc934,
+       c_showproc, c_termproc, c_storeproc, c_storage, c_folder
+    */
+
+    ct->c_file = add (first_alt->c_file, NULL);
+    ct->c_type = type;
+    ct->c_subtype = subtype;
+
+    ctinit = get_ct_init (ct->c_type);
+
+    typename = ct_type_str (type);
+    subtypename = ct_subtype_str (type, subtype);
+
+    {
+        int serial = 0;
+        int found_boundary = 1;
+
+        while (found_boundary  &&  serial < 1000000) {
+            found_boundary = 0;
+
+            /* Ensure that the boundary doesn't appear in the decoded
+               content. */
+            if (new_part->c_cefile.ce_file) {
+                if ((found_boundary =
+                     boundary_in_content (&new_part->c_cefile.ce_fp,
+                                          new_part->c_cefile.ce_file,
+                                          boundary)) == -1) {
+                    return NULL;
+                }
+            }
+
+            /* Ensure that the boundary doesn't appear in the encoded
+               content. */
+            if (! found_boundary  &&  new_part->c_file) {
+                if ((found_boundary = boundary_in_content (&new_part->c_fp,
+                                                           new_part->c_file,
+                                                           boundary)) == -1) {
+                    return NULL;
+                }
+            }
+
+            if (found_boundary) {
+                /* Try a slightly different boundary. */
+                char buffer2[16];
+
+                free (boundary);
+                ++serial;
+                snprintf (buffer2, sizeof buffer2, "%d", serial);
+                boundary =
+                    concat (boundary_prefix,
+                            first_alt->c_partno ? first_alt->c_partno : "",
+                            "-", buffer2,  NULL);
+            }
+        }
+
+        if (found_boundary) {
+            advise (NULL, "giving up trying to find a unique boundary");
+            return NULL;
+        }
+    }
+
+    name = concat (" ", typename, "/", subtypename, boundary_indicator, "\"",
+                   boundary, "\"", NULL);
+
+    /* Load c_first_hf and c_last_hf. */
+    transfer_noncontent_headers (first_alt, ct);
+    add_header (ct, add (TYPE_FIELD, NULL), concat (name, "\n", NULL));
+    free (name);
+
+    /* Load c_partno. */
+    if (first_alt->c_partno) {
+        ct->c_partno = add (first_alt->c_partno, NULL);
+        free (first_alt->c_partno);
+        first_alt->c_partno = concat (ct->c_partno, ".1", NULL);
+        new_part->c_partno = concat (ct->c_partno, ".2", NULL);
+    } else {
+        first_alt->c_partno = add ("1", NULL);
+        new_part->c_partno = add ("2", NULL);
+    }
+
+    if (ctinit) {
+        ct->c_ctinfo.ci_type = add (typename, NULL);
+        ct->c_ctinfo.ci_subtype = add (subtypename, NULL);
+    }
+
+    name = concat (" ", typename, "/", subtypename, boundary_indicator,
+                   boundary, NULL);
+    if ((cp = strstr (name, boundary_indicator))) {
+        ct->c_ctinfo.ci_attrs[0] = name;
+        ct->c_ctinfo.ci_attrs[1] = NULL;
+        /* ci_values don't get free'd, so point into ci_attrs. */
+        ct->c_ctinfo.ci_values[0] = cp + strlen (boundary_indicator);
+    }
+
+    p = (struct part *) mh_xmalloc (sizeof *p);
+    p->mp_next = (struct part *) mh_xmalloc (sizeof *p->mp_next);
+    p->mp_next->mp_next = NULL;
+    p->mp_next->mp_part = first_alt;
+
+    if ((m = (struct multipart *) calloc (1, sizeof (struct multipart))) ==
+        NULL)
+        adios (NULL, "out of memory");
+    m->mp_start = concat (boundary, "\n", NULL);
+    m->mp_stop = concat (boundary, "--\n", NULL);
+    m->mp_parts = p;
+    ct->c_ctparams = (void *) m;
+
+    free (boundary);
+
+    return ct;
+}
+
+
+/* Check that the boundary does not appear in the content. */
+static int
+boundary_in_content (FILE **fp, char *file, const char *boundary) {
+    char buffer[BUFSIZ];
+    size_t bytes_read;
+    int found_boundary = 0;
+
+    /* free_content() will close *fp if we fopen it here. */
+    if (! *fp  &&  (*fp = fopen (file, "r")) == NULL) {
+        advise (file, "unable to open %s for reading", file);
+        return NOTOK;
+    }
+
+    fseeko (*fp, 0L, SEEK_SET);
+    while ((bytes_read = fread (buffer, 1, sizeof buffer, *fp)) > 0) {
+        if (find_str (buffer, bytes_read, boundary)) {
+            found_boundary = 1;
+            break;
+        }
+    }
+
+    return found_boundary;
+}
+
+
+/* Remove all non-Content headers. */
+static void
+transfer_noncontent_headers (CT old, CT new) {
+    HF hp, hp_prev;
+
+    hp_prev = hp = old->c_first_hf;
+    while (hp) {
+        HF next = hp->next;
+
+        if (strncasecmp (XXX_FIELD_PRF, hp->name, strlen (XXX_FIELD_PRF))) {
+            if (hp == old->c_last_hf) {
+                if (hp == old->c_first_hf) {
+                    old->c_last_hf =  old->c_first_hf = NULL;
+                } else {
+                    hp_prev->next = NULL;
+                    old->c_last_hf =  hp_prev;
+                }
+            } else {
+                if (hp == old->c_first_hf) {
+                    old->c_first_hf = next;
+                } else {
+                    hp_prev->next = next;
+                }
+            }
+
+            /* Put node hp in the new CT. */
+            if (new->c_first_hf == NULL) {
+                new->c_first_hf = hp;
+            } else {
+                new->c_last_hf->next = hp;
+            }
+            new->c_last_hf = hp;
+        } else {
+            /* A Content- header, leave in old. */
+            hp_prev = hp;
+        }
+
+        hp = next;
+    }
+}
+
+
+static int
+set_ct_type (CT ct, int type, int subtype, int encoding) {
+    char *typename = ct_type_str (type);
+    char *subtypename = ct_subtype_str (type, subtype);
+    /* E.g, " text/plain" */
+    char *type_subtypename = concat (" ", typename, "/", subtypename, NULL);
+    /* E.g, " text/plain\n" */
+    char *name_plus_nl = concat (type_subtypename, "\n", NULL);
+    int found_content_type = 0;
+    HF hf;
+    const char *cp = NULL;
+    char *ctline;
+    int status;
+
+    /* Update/add Content-Type header field. */
+    for (hf = ct->c_first_hf; hf; hf = hf->next) {
+        if (! strcasecmp (TYPE_FIELD, hf->name)) {
+            found_content_type = 1;
+            free (hf->value);
+            hf->value = (cp = strchr (ct->c_ctline, ';'))
+                ?  concat (type_subtypename, cp, "\n", NULL)
+                :  add (name_plus_nl, NULL);
+        }
+    }
+    if (! found_content_type) {
+        add_header (ct, add (TYPE_FIELD, NULL),
+                    (cp = strchr (ct->c_ctline, ';'))
+                    ?  concat (type_subtypename, cp, "\n", NULL)
+                    :  add (name_plus_nl, NULL));
+    }
+
+    /* Some of these might not be used, but set them anyway. */
+    ctline = cp
+        ?  concat (type_subtypename, cp, NULL)
+        :  concat (type_subtypename, NULL);
+    free (ct->c_ctline);
+    ct->c_ctline = ctline;
+    /* Leave other ctinfo members as they were. */
+    free (ct->c_ctinfo.ci_type);
+    ct->c_ctinfo.ci_type = add (typename, NULL);
+    free (ct->c_ctinfo.ci_subtype);
+    ct->c_ctinfo.ci_subtype = add (subtypename, NULL);
+    ct->c_type = type;
+    ct->c_subtype = subtype;
+
+    free (name_plus_nl);
+    free (type_subtypename);
+
+    status = set_ce (ct, encoding);
+
+    return status;
+}
+
+
+static int
+decode_text_parts (CT ct, int encoding, int *message_mods) {
+    int status = OK;
+
+    switch (ct->c_type) {
+    case CT_TEXT:
+        switch (ct->c_encoding) {
+        case CE_BASE64:
+        case CE_QUOTED: {
+            int ct_encoding;
+
+            if (decode_part (ct) == OK  &&  ct->c_cefile.ce_file) {
+                if ((ct_encoding = content_encoding (ct)) == CE_BINARY  &&
+                    encoding != CE_BINARY) {
+                    if (verbosw) {
+                        report (ct->c_partno, ct->c_file,
+                                "will not decode%s because it is binary",
+                                ct->c_partno  ?  ""
+                                              :  ct->c_ctline  ?  ct->c_ctline
+                                                               :  "");
+                    }
+                    unlink (ct->c_cefile.ce_file);
+                    free (ct->c_cefile.ce_file);
+                    ct->c_cefile.ce_file = NULL;
+                } else if (ct_encoding == CE_8BIT  &&  encoding == CE_7BIT) {
+                    if (verbosw) {
+                        report (ct->c_partno, ct->c_file,
+                                "will not decode%s because it is 8bit",
+                                ct->c_partno  ?  ""
+                                              :  ct->c_ctline  ?  ct->c_ctline
+                                                               :  "");
+                    }
+                    unlink (ct->c_cefile.ce_file);
+                    free (ct->c_cefile.ce_file);
+                    ct->c_cefile.ce_file = NULL;
+                } else {
+                    int enc = ct_encoding == CE_BINARY
+                        ?  CE_BINARY
+                        :  charset_encoding (ct);
+                    if (set_ce (ct, enc) == OK) {
+                        ++*message_mods;
+                        if (verbosw) {
+                            report (ct->c_partno, ct->c_file, "decode%s",
+                                    ct->c_ctline ? ct->c_ctline : "");
+                        }
+                    } else {
+                        status = NOTOK;
+                    }
+                }
+            } else {
+                status = NOTOK;
+            }
+            break;
+        }
+        default:
+            break;
+        }
+        break;
+
+    case CT_MULTIPART: {
+        struct multipart *m = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        /* Should check to see if the body for this part is encoded?
+           For now, it gets passed along as-is by InitMultiPart(). */
+        for (part = m->mp_parts; status == OK  &&  part; part = part->mp_next) {
+            status = decode_text_parts (part->mp_part, encoding, message_mods);
+        }
+        break;
+    }
+
+    case CT_MESSAGE:
+        if (ct->c_subtype == MESSAGE_EXTERNAL) {
+            struct exbody *e;
+
+            e = (struct exbody *) ct->c_ctparams;
+            status = decode_text_parts (e->eb_content, encoding, message_mods);
+        }
+        break;
+
+    default:
+        break;
+    }
+
+    return status;
+}
+
+
+/* See if the decoded content is 7bit, 8bit, or binary.  It's binary
+   if it has any NUL characters, a CR not followed by a LF, or lines
+   greater than 998 characters in length. */
+static int
+content_encoding (CT ct) {
+    CE ce = &ct->c_cefile;
+    int encoding = CE_7BIT;
+
+    if (ce->ce_file) {
+        char buffer[BUFSIZ];
+        size_t inbytes;
+
+        if (! ce->ce_fp  &&  (ce->ce_fp = fopen (ce->ce_file, "r")) == NULL) {
+            advise (ce->ce_file, "unable to open for reading");
+            return CE_UNKNOWN;
+        }
+
+        fseeko (ce->ce_fp, 0L, SEEK_SET);
+        while (encoding != CE_BINARY  &&
+               (inbytes = fread (buffer, 1, sizeof buffer, ce->ce_fp)) > 0) {
+            char *cp;
+            size_t i;
+            size_t line_len = 0;
+            int last_char_was_cr = 0;
+
+            for (i = 0, cp = buffer; i < inbytes; ++i, ++cp) {
+                if (*cp == '\0'  ||  ++line_len > 998  ||
+                    (*cp != '\n'  &&  last_char_was_cr)) {
+                    encoding = CE_BINARY;
+                    break;
+                } else if (*cp == '\n') {
+                    line_len = 0;
+                } else if (! isascii ((unsigned char) *cp)) {
+                    encoding = CE_8BIT;
+                }
+
+                last_char_was_cr = *cp == '\r'  ?  1  :  0;
+            }
+        }
+
+        fclose (ce->ce_fp);
+        ce->ce_fp = NULL;
+    } /* else should never happen */
+
+    return encoding;
+}
+
+
+static int
+convert_codesets (CT ct, char *dest_codeset, int *message_mods) {
+    int status = OK;
+
+    switch (ct->c_type) {
+    case CT_TEXT:
+        if (ct->c_subtype == TEXT_PLAIN) {
+            status = convert_codeset (ct, dest_codeset, message_mods);
+        }
+        break;
+
+    case CT_MULTIPART: {
+        struct multipart *m = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        /* Should check to see if the body for this part is encoded?
+           For now, it gets passed along as-is by InitMultiPart(). */
+        for (part = m->mp_parts; status == OK  &&  part; part = part->mp_next) {
+            status =
+                convert_codesets (part->mp_part, dest_codeset, message_mods);
+        }
+        break;
+    }
+
+    case CT_MESSAGE:
+        if (ct->c_subtype == MESSAGE_EXTERNAL) {
+            struct exbody *e;
+
+            e = (struct exbody *) ct->c_ctparams;
+            status =
+                convert_codesets (e->eb_content, dest_codeset, message_mods);
+        }
+        break;
+
+    default:
+        break;
+    }
+
+    return status;
+}
+
+
+static int
+convert_codeset (CT ct, char *dest_codeset, int *message_mods) {
+    const char *const charset = "charset";
+    char **src_codeset = NULL;
+    char *default_codeset = NULL;
+    CI ctinfo = &ct->c_ctinfo;
+    char **ap, **vp;
+    int status = OK;
+
+    for (ap = ctinfo->ci_attrs, vp = ctinfo->ci_values; *ap; ++ap, ++vp) {
+        if (! strcasecmp (*ap, charset)) {
+            src_codeset = vp;
+            break;
+        }
+    }
+    /* RFC 2045, Sec. 5.2:  default to us-ascii. */
+    if (src_codeset == NULL) src_codeset = &default_codeset;
+    if (*src_codeset == NULL) *src_codeset = "US-ASCII";
+
+    if (strcmp (norm_charmap (*src_codeset), norm_charmap (dest_codeset))) {
+#ifdef HAVE_ICONV
+        iconv_t conv_desc = NULL;
+        char *dest;
+        int fd = -1;
+        char **file = NULL;
+        FILE **fp = NULL;
+        long begin;
+        long end;
+        int opened_input_file = 0;
+        char src_buffer[BUFSIZ];
+        HF hf;
+
+        if ((conv_desc = iconv_open (dest_codeset, *src_codeset)) ==
+            (iconv_t) -1) {
+            advise (NULL, "Can't convert %s to %s", *src_codeset, dest_codeset);
+            return -1;
+        }
+
+        dest = add (m_mktemp2 (tmp, invo_name, &fd, NULL), NULL);
+
+        if (ct->c_cefile.ce_file) {
+            file = &ct->c_cefile.ce_file;
+            fp = &ct->c_cefile.ce_fp;
+            begin = 0;
+            end = -1;
+        } else if (ct->c_file) {
+            file = &ct->c_file;
+            fp = &ct->c_fp;
+            begin = ct->c_begin;
+            end = ct->c_end;
+        } /* else no input file: shouldn't happen */
+
+        if (file  &&  *file  &&  fp) {
+            if (! *fp) {
+                if ((*fp = fopen (*file, "r")) == NULL) {
+                    advise (*file, "unable to open for reading");
+                    status = NOTOK;
+                } else {
+                    opened_input_file = 1;
+                }
+            }
+        }
+
+        if (fp  &&  *fp) {
+            size_t inbytes;
+            size_t max = end > 0  ?  (size_t) (end-begin)  :  sizeof src_buffer;
+
+            fseeko (*fp, begin, SEEK_SET);
+            while (status == OK  &&  max > 0  &&
+                   (inbytes = fread (src_buffer, 1, max, *fp)) > 0) {
+                char dest_buffer[BUFSIZ];
+                char *ib = src_buffer, *ob = dest_buffer;
+                size_t outbytes = sizeof dest_buffer;
+                size_t outbytes_before = outbytes;
+
+                if (end > 0) max -= inbytes;
+
+                if (iconv (conv_desc, &ib, &inbytes, &ob, &outbytes) ==
+                    (size_t) -1) {
+                    status = NOTOK;
+                    break;
+                } else {
+                    write (fd, dest_buffer, outbytes_before - outbytes);
+                }
+            }
+
+            if (opened_input_file) {
+                fclose (*fp);
+                *fp = NULL;
+            }
+        }
+
+        iconv_close (conv_desc);
+        close (fd);
+
+        if (status == OK) {
+            /* Replace the decoded file with the converted one. */
+            if (ct->c_cefile.ce_file) {
+                if (ct->c_cefile.ce_unlink) {
+                    unlink (ct->c_cefile.ce_file);
+                }
+                free (ct->c_cefile.ce_file);
+            }
+            ct->c_cefile.ce_file = dest;
+            ct->c_cefile.ce_unlink = 1;
+
+            ++*message_mods;
+            if (verbosw) {
+                report (ct->c_partno, ct->c_file, "convert %s to %s",
+                        *src_codeset, dest_codeset);
+            }
+
+            /* Update ci_attrs. */
+            *src_codeset = dest_codeset;
+
+            /* Update ct->c_ctline. */
+            if (ct->c_ctline) {
+                char *ctline =
+                    update_attr (ct->c_ctline, "charset=", dest_codeset);
+
+                free (ct->c_ctline);
+                ct->c_ctline = ctline;
+            } /* else no CT line, which is odd */
+
+            /* Update Content-Type header field. */
+            for (hf = ct->c_first_hf; hf; hf = hf->next) {
+                if (! strcasecmp (TYPE_FIELD, hf->name)) {
+                    char *ctline_less_newline =
+                        update_attr (hf->value, "charset=", dest_codeset);
+                    char *ctline = concat (ctline_less_newline, "\n", NULL);
+                    free (ctline_less_newline);
+
+                    free (hf->value);
+                    hf->value = ctline;
+                    break;
+                }
+            }
+        } else {
+            unlink (dest);
+        }
+#else  /* ! HAVE_ICONV */
+        NMH_UNUSED (message_mods);
+
+        advise (NULL, "Can't convert %s to %s without iconv", *src_codeset,
+                dest_codeset);
+        status = NOTOK;
+#endif /* ! HAVE_ICONV */
+    }
+
+    return status;
+}
+
+
+static int
+write_content (CT ct, char *input_filename, char *outfile, int modify_inplace,
+               int message_mods) {
+    int status = OK;
+
+    if (modify_inplace) {
+        if (message_mods > 0) {
+            if ((status = output_message (ct, outfile)) == OK) {
+                char *infile = input_filename
+                    ?  add (input_filename, NULL)
+                    :  add (ct->c_file ? ct->c_file : "-", NULL);
+
+                if (remove_file (infile) == OK) {
+                    if (rename (outfile, infile)) {
+                        /* The -file argument processing used path() to
+                           expand filename to absolute path. */
+                        int file = ct->c_file  &&  ct->c_file[0] == '/';
+
+                        admonish (NULL, "unable to rename %s %s to %s",
+                                  file ? "file" : "message", outfile, infile);
+                        unlink (outfile);
+                        status = NOTOK;
+                    }
+                } else {
+                    admonish (NULL, "unable to remove input file %s, "
+                              "not modifying it", infile);
+                    unlink (outfile);
+                    status = NOTOK;
+                }
+
+                free (infile);
+            }
+        } else {
+            /* No modifications and didn't need the tmp outfile. */
+            unlink (outfile);
+        }
+    } else {
+        /* Output is going to some file.  Produce it whether or not
+           there were modifications. */
+        status = output_message (ct, outfile);
+    }
+
+    flush_errors ();
+    return status;
+}
+
+
+/*
+ * If "rmmproc" is defined, call that to remove the file.  Otherwise,
+ * use the standard MH backup file.
+ */
+static int
+remove_file (char *file) {
+    if (rmmproc) {
+        char *rmm_command = concat (rmmproc, " ", file, NULL);
+        int status = WIFEXITED (status = system (rmm_command))
+            ?  WEXITSTATUS (status)
+            :  NOTOK;
+
+        free (rmm_command);
+
+        return status;
+    } else {
+        /* This is OK for a non-message file, it still uses the
+           BACKUP_PREFIX form.  The backup file will be in the same
+           directory as file. */
+        return rename (file, m_backup (file));
+    }
+}
+
+
+static void
+report (char *partno, char *filename, char *message, ...) {
+    va_list args;
+    char *fmt;
+
+    if (verbosw) {
+        va_start (args, message);
+        fmt = concat (filename, partno ? " part " : ", ",
+                      partno ? partno : "", partno ? ", " : "", message, NULL);
+
+        advertise (NULL, NULL, fmt, args);
+
+        free (fmt);
+        va_end (args);
+    }
+}
+
+
+static char *
+upcase (char *str) {
+    char *up = cpytrim (str);
+    char *cp;
+
+    for (cp = up; *cp; ++cp) *cp = toupper ((unsigned char) *cp);
+
+    return up;
+}
+
+
+static void
+pipeser (int i)
+{
+    if (i == SIGQUIT) {
+        fflush (stdout);
+        fprintf (stderr, "\n");
+        fflush (stderr);
+    }
+
+    done (1);
+    /* NOTREACHED */
+}