From: David Levine Date: Sun, 17 Mar 2013 15:32:57 +0000 (-0500) Subject: Added mhfixmsg(1). X-Git-Url: https://diplodocus.org/git/nmh/commitdiff_plain/63764e68d4fce20854dc2ba18a7244cb6a6769ca?ds=inline;hp=--cc Added mhfixmsg(1). --- 63764e68d4fce20854dc2ba18a7244cb6a6769ca diff --git a/.gitignore b/.gitignore index 8d7e69d7..cb9f2017 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ a.out.dSYM/ /uip/install-mh /uip/mark /uip/mhbuild +/uip/mhfixmsg /uip/mhl /uip/mhlist /uip/mhn diff --git a/Makefile.am b/Makefile.am index 0e64a27b..cb876894 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/docs/pending-release-notes b/docs/pending-release-notes index 1192e7e3..cacf2e37 100644 --- a/docs/pending-release-notes +++ b/docs/pending-release-notes @@ -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 diff --git a/etc/mhn.defaults.sh b/etc/mhn.defaults.sh index 4142f4cb..6bc01247 100755 --- a/etc/mhn.defaults.sh +++ b/etc/mhn.defaults.sh @@ -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 index 00000000..1d1d6785 --- /dev/null +++ b/man/mhfixmsg.man @@ -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 index 00000000..14b9a58a --- /dev/null +++ b/test/mhfixmsg/test-mhfixmsg @@ -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" <"$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` < +To: Some User +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 <"$expected" </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" < + + + + + + +
+
Need to go! Need ... to ... go!
+ + + +------=_nmh-multipart-- +EOF + +cat >"$MH_TEST_DIR"/Mail/inbox/12 < + + + + + + +
+
Need to go! Need ... to ... go!
+ + +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" < + + + + + + +------=_nmh-multipart
+------=_nmh-multipart-1
+------=_nmh-multipart-2
+ + + +------=_nmh-multipart-3-- +EOF + +cat >"$MH_TEST_DIR"/Mail/inbox/12 < + + + + + + +------=3D_nmh-multipart
+------=3D_nmh-multipart-1
+------=3D_nmh-multipart-2
+ + +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" < + + + + + + +
+
Need to go! Need ... to ... go!
+ + + +------=_nmh-multipart-- +EOF + +cat >"$MH_TEST_DIR"/Mail/inbox/12 < + + + + + + +
+
Need to go! Need ... to ... go!
+ + +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" <$msgfile <$expected <$msgfile <$msgfile <"$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" <$msgfile <"$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" <`mhpath new` < +To: +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" < +To: +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" < + + HTML Content + + + This is the real content. + + + +------=_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` < + + HTML Content + + + This is the real content. + + +------=_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" < +To: Some User +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 +To: +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` < +To: Some User +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 +To: +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" < +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ICONV +# include +#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 */ +}