--- /dev/null
+#!/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
--- /dev/null
+/*
+ * 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 */
+}