From: Ken Hornstein Date: Thu, 21 Mar 2013 21:14:30 +0000 (-0400) Subject: Merge branch 'newlock' X-Git-Url: https://diplodocus.org/git/nmh/commitdiff_plain/17932f7aea1c21cf17738f47996345d9d0a6ce51?hp=85bb87a28dff30d9e90ec80ab33cd97a3f07cb9d Merge branch 'newlock' --- 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 074adb26..1e64f159 100644 --- a/Makefile.am +++ b/Makefile.am @@ -63,6 +63,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \ test/locking/test-datalocking test/locking/test-spoollocking \ 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 \ @@ -129,11 +130,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 @@ -215,9 +216,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 \ @@ -236,10 +237,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 \ @@ -303,6 +304,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) $(ICONVLIB) $(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 @@ -543,6 +550,13 @@ sbr_libmh_a_SOURCES = sbr/addrsbr.c sbr/ambigsw.c sbr/atooi.c sbr/arglist.c \ sbr/m_mktemp.c sbr/getansreadline.c config/config.c \ config/version.c +## +## Because these files use the definitions in the libmh rule below, +## they need to be rebuilt if the Makefile changes. +## + +config/sbr_libmh_a-config.$(OBJEXT) sbr/sbr_libmh_a-mts.$(OBJEXT): Makefile + sbr_libmh_a_CPPFLAGS = -I./sbr -DNMHETCDIR='"$(sysconfdir)"' \ -DMAILSPOOL='"$(mailspool)"' \ -DSENDMAILPATH='"$(sendmailpath)"' -DNMHBINDIR='"$(bindir)"' \ diff --git a/SPECS/nmh.spec b/SPECS/nmh.spec index 8373e039..39e87ab0 100644 --- a/SPECS/nmh.spec +++ b/SPECS/nmh.spec @@ -57,9 +57,9 @@ single comprehensive program. Instead, it consists of a number of fairly simple single-purpose programs for sending, receiving, saving, retrieving and otherwise manipulating email messages. You can freely intersperse nmh commands with other shell commands or write custom -scripts which utilize nmh commands. If you want to use nmh as a true -email user agent, you'll want to also install exmh to provide a user -interface for it--nmh only has a command line interface. +scripts which utilize nmh commands. nmh only has a command line +interface; if you want a more sophisticated user interface, you'll +want to also install exmh. %prep @@ -80,7 +80,7 @@ if [ -f %srcdir/config.status ]; then ./config.status else %if %{undefined configure_opts} - %define configure_opts --with-cyrus-sasl --with-locking=fcntl + %define configure_opts --with-cyrus-sasl %endif %configure %configure_opts fi diff --git a/configure.ac b/configure.ac index bfa3c518..de7f93f1 100644 --- a/configure.ac +++ b/configure.ac @@ -8,7 +8,7 @@ AC_PREREQ([2.68]) AC_INIT([nmh], m4_normalize(m4_include([VERSION])), [nmh-workers@nongnu.org]) AC_CONFIG_SRCDIR([h/nmh.h]) AC_CONFIG_HEADER([config.h]) -AM_INIT_AUTOMAKE([-Wall color-tests foreign serial-tests subdir-objects 1.12]) +AM_INIT_AUTOMAKE([-Wall foreign serial-tests subdir-objects 1.12]) AC_CANONICAL_HOST diff --git a/docs/TODO b/docs/TODO index 61dfbaa9..75379cd5 100644 --- a/docs/TODO +++ b/docs/TODO @@ -110,7 +110,6 @@ MHN/MHSHOW/MHLIST/MHSTORE handling type, such as multipart/mixed. * add way so user can tell mhn to use a certain `proc' such as moreproc, for certain content types. -* add support for Content-Disposition header (rfc1806, rfc2183). * merge the two places in which mhshowsbr.c reads display strings. * when storing to a folder, should we save the folder context diff --git a/docs/contrib/build_nmh b/docs/contrib/build_nmh index 0b9e582e..22c4efb3 100755 --- a/docs/contrib/build_nmh +++ b/docs/contrib/build_nmh @@ -310,7 +310,8 @@ fi grep 'Error' "$logfile" #### Ignore the warning when sbr/dtimep.c is built with flex 2.5.36 #### or 2.5.37. -grep 'warn' "$logfile" | grep -v 'sbr/dtimep.c:.*-Wsign-compare' +grep 'warn' "$logfile" | \ + grep -v 'sbr/dtimep.c:.*comparison between signed and unsigned' if [ $status -ne 0 ]; then echo build failed! echo build log is in "$logfile" diff --git a/docs/pending-release-notes b/docs/pending-release-notes index 3ff8fbd9..e275dfa6 100644 --- a/docs/pending-release-notes +++ b/docs/pending-release-notes @@ -44,6 +44,12 @@ NEW FEATURES - A new program, fmttest(1) is included to help debug format files - 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. +- Added -[no]rmmproc switches to rmm(1). +- Added support for Content-Disposition header (RFC 2183) to mhstore(1) + and mhn(1) when used with -auto. - All nmh commands now support transactional locking for sequence files. ---------------------------- 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/etc/rmmproc.messageid b/etc/rmmproc.messageid index 7bb4a26e..e23882c6 100755 --- a/etc/rmmproc.messageid +++ b/etc/rmmproc.messageid @@ -25,7 +25,7 @@ # exists, use 2). # NOTE: conversion of some characters in the filename could # result in an unintended name collision. If that is a concern, - # a program that uses mktemp(3) to create a temporary file might + # a program that uses mkstemp(3) to create a temporary file might # be the basis for a remedy. # 2) Concatenation of BACKUP_PREFIX and input filename. # NOTE: if a file of that name already exists in the destination diff --git a/h/mhparse.h b/h/mhparse.h index 57bdabd3..1f8ab719 100644 --- a/h/mhparse.h +++ b/h/mhparse.h @@ -97,7 +97,7 @@ struct Content { int c_subtype; /* internal flag for content subtype */ /* Content-Transfer-Encoding info (decoded contents) */ - CE c_cefile; /* structure holding decoded content */ + struct cefile c_cefile; /* structure holding decoded content */ int c_encoding; /* internal flag for encoding type */ /* Content-MD5 info */ @@ -286,5 +286,6 @@ char *ct_subtype_str (int, int); const struct str2init *get_ct_init (int); const char *ce_str (int); const struct str2init *get_ce_method (const char *); +int parse_header_attrs (const char *, int, char **, CI, int *); extern int checksw; /* Add Content-MD5 field */ diff --git a/man/fmttest.man b/man/fmttest.man index 011d90db..69eafde0 100644 --- a/man/fmttest.man +++ b/man/fmttest.man @@ -103,7 +103,7 @@ following default program is used: .PP In this mode .B fmttest -is equivant to +is equivalent to .BR ap (8). .PP In raw mode, no processing of the specified arguments is done. Each argument diff --git a/man/mh-profile.man b/man/mh-profile.man index 75bea09d..a85522b0 100644 --- a/man/mh-profile.man +++ b/man/mh-profile.man @@ -1,4 +1,4 @@ -.TH MH-PROFILE %manext5% "November 6, 2012" "%nmhversion%" +.TH MH-PROFILE %manext5% "March 18, 2013" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -557,9 +557,10 @@ to do address verification. none .RS 5 This is the program used by -.B rmm +.BR rmm , +.BR refile , and -.B refile +.B mhfixmsg to delete a message from a folder. .RE .PP diff --git a/man/mhfixmsg.man b/man/mhfixmsg.man new file mode 100644 index 00000000..bb80a28f --- /dev/null +++ b/man/mhfixmsg.man @@ -0,0 +1,293 @@ +.TH MHFIXMSG %manext1% "March 18, 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 [ \-rmmproc +.IR program ] +.RB [ \-normmproc ] +.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. +The +.B \-rmmproc +switch may be used to override this profile component. The +.B \-normmproc +switch disables the use of any +.I rmmproc +profile component and negates all prior +.B \-rmmproc +switches. +.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-worker/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/man/mhstore.man b/man/mhstore.man index ccf1ee9c..9216bf66 100644 --- a/man/mhstore.man +++ b/man/mhstore.man @@ -1,4 +1,4 @@ -.TH MHSTORE %manext1% "September 15, 2012" "%nmhversion%" +.TH MHSTORE %manext1% "March 20, 2013" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -157,15 +157,15 @@ If the .B \-auto switch is given, then .B mhstore -will check if -the message contains information indicating the filename that should -be used to store the content. This information should be specified -as the attribute \*(lqname=filename\*(rq in the \*(lqContent-Type\*(rq header -for the content you are storing. For security reasons, this filename -will be ignored if it begins with the character '/', '.', '|', or '!', -or if it contains the character '%'. For the sake of security, -this switch is not the default, and it is recommended that you do -NOT put the +will check if the message contains information indicating the filename +that should be used to store the content. This information should be +specified as the \*(lqfilename\*(rq attribute in the +\*(lqContent-Disposition\*(rq header or as the \*(lqname\*(rq +attribute in the \*(lqContent-Type\*(rq header for the content you are +storing. For security reasons, this filename will be ignored if it +begins with the character '/', '.', '|', or '!', or if it contains the +character '%'. For the sake of security, this switch is not the +default, and it is recommended that you do NOT put the .B \-auto switch in your .I \&.mh\(ruprofile diff --git a/man/refile.man b/man/refile.man index a111046d..213f5747 100644 --- a/man/refile.man +++ b/man/refile.man @@ -1,4 +1,4 @@ -.TH REFILE %manext1% "January 18, 2001" "%nmhversion%" +.TH REFILE %manext1% "March 18, 2013" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -12,6 +12,7 @@ refile \- file message in other folders .RB [ \-draft ] .RB [ \-link " | " \-nolink ] .RB [ \-preserve " | " \-nopreserve ] +.RB [ \-retainsequences " | " \-noretainsequences ] .RB [ \-unlink " | " \-nounlink ] .RB [ \-src .IR +folder ] @@ -113,6 +114,11 @@ switch, then will use the next available message number which is above the message number you wish to preserve. .PP +The +.B \-retainsequences +switch causes the memberships in sequences, except \*(lqcur\*(rq, of +the refiled messages to be carried over to each destination folder. +.PP If .B \-link is not specified (or @@ -184,6 +190,7 @@ to file the /draft. .fi .SH "SEE ALSO" .IR folder (1), +.IR mh-sequence (5), .IR rmf (1), .IR rmm (1) .SH DEFAULTS @@ -193,6 +200,7 @@ to file the /draft. .RB ` \-nolink ' .RB ` \-nounlink ' .RB ` \-nopreserve ' +.RB ` \-noretainsequences ' .fi .SH CONTEXT If @@ -215,7 +223,9 @@ for information concerning the previous sequence. .SH BUGS Since .B refile -uses your +and +.B rmm +use your .I rmmproc to delete the message, the @@ -224,6 +234,8 @@ must .B NOT call .B refile +or +.B rmm without specifying .BR \-normmproc , or you will create an infinite loop. diff --git a/man/rmf.man b/man/rmf.man index 3c1b464a..6a5f10bb 100644 --- a/man/rmf.man +++ b/man/rmf.man @@ -44,8 +44,8 @@ returning to the current folder from which the list was extracted. .PP If .B rmf -s used on a read\-only folder, it will delete all the -(private) sequences +is used on a read\-only folder, it will delete all the +private sequences (i.e., .RI \*(lqatr\- seq \- folder \*(rq entries) for this folder diff --git a/man/rmm.man b/man/rmm.man index 52ac7f4d..d1b12c83 100644 --- a/man/rmm.man +++ b/man/rmm.man @@ -1,4 +1,4 @@ -.TH RMM %manext1% "February 13, 2013" "%nmhversion%" +.TH RMM %manext1% "March 18, 2013" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -11,6 +11,9 @@ rmm \- remove messages .RI [ +folder ] .RI [ msgs ] .RB [ \-unlink " | " \-nounlink ] +.RB [ \-rmmproc +.IR program ] +.RB [ \-normmproc ] .RB [ \-version ] .RB [ \-help ] .ad @@ -44,11 +47,20 @@ add a profile component such as rmmproc: /home/foouser/bin/rmm_msgs .RE .PP -then instead of simply renaming the message file, +Then instead of simply renaming the message file, .B rmm will call the named program or script to handle the files that represent the messages to be deleted. +The +.B \-rmmproc +switch may be used to override this profile component. The +.B \-normmproc +switch disables the use of any +.I rmmproc +profile component and negates all prior +.B \-rmmproc +switches. .PP An example of a .I rmmproc @@ -106,8 +118,10 @@ advance to the next message in the folder as expected. If a folder is given, it will become the current folder. .SH BUGS Since +.B rmm +and .B refile -uses your +use your .I rmmproc to delete the message, the @@ -115,7 +129,9 @@ the must .B NOT call +.B rmm +or .B refile without specifying .BR \-normmproc , -or you will create an infinte loop. +or you will create an infinite loop. diff --git a/test/getcwidth.c b/test/getcwidth.c index a145b2d9..9e1ba65c 100644 --- a/test/getcwidth.c +++ b/test/getcwidth.c @@ -145,19 +145,19 @@ dumpwidth(void) wchar_t wc, low; int width, lastwidth; - for (wc = low = 1, lastwidth = wcwidth(wc); wc <= 0xffff; wc++) { - width = wcwidth(wc); + for (wc = 0, low = 1, lastwidth = wcwidth(1); wc < 0xffff; wc++) { + width = wcwidth(wc+1); if (width != lastwidth) { printf("%04lX - %04lX = %d\n", (unsigned long int) low, - (unsigned long int) (wc - 1), lastwidth); - low = wc; + (unsigned long int) (wc), lastwidth); + low = wc+1; } lastwidth = width; } - width = wcwidth(wc - 1); + width = wcwidth(wc); if (width == lastwidth) printf("%04lX - %04lX = %d\n", (unsigned long int) low, - (unsigned long int) (wc - 1), width); + (unsigned long int) (wc), width); } #endif /* MULTIBYTE_SUPPORT */ diff --git a/test/mhfixmsg/test-mhfixmsg b/test/mhfixmsg/test-mhfixmsg new file mode 100755 index 00000000..709a4062 --- /dev/null +++ b/test/mhfixmsg/test-mhfixmsg @@ -0,0 +1,887 @@ +#!/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" + + +# check -normmproc +cp "${MH_TEST_DIR}/Mail/inbox/18" "${MH_TEST_DIR}/Mail/inbox/19" + +run_test 'mhfixmsg 18 -normmproc' +check "${MH_TEST_DIR}/Mail/inbox/19" \ + "${MH_TEST_DIR}/Mail/inbox/,18" 'keep first' + + +# check -rmmproc +run_test 'mhfixmsg 19 -rmmproc true' +if test -f '${MH_TEST_DIR}/Mail/inbox/19.backup'; then + echo check of mhfixmsg -rmmproc FAILED, should not have created backup file + failed=`expr ${failed:-0} + 1` +fi + + +# make sure there are no tmp files left over +find "$MH_TEST_DIR/Mail" \( -name 'mhfix*' -o -name ',mhfix*' \) -print \ + >"$actual" +cat >"$expected" < $expected < $expected <c_cefile = ce; ct->c_ceopenfnx = open7Bit; /* since unencoded */ ct->c_ceclosefnx = close_encoding; ct->c_cesizefnx = NULL; /* since unencoded */ @@ -430,7 +424,7 @@ user_content (FILE *in, char *file, char *buf, CT *ctp) /* allocate basic structure for handling decoded content */ init_decoded_content (ct); - ce = ct->c_cefile; + ce = &ct->c_cefile; ci = &ct->c_ctinfo; set_id (ct, 0); @@ -791,7 +785,7 @@ use_forw: if ((p = (CT) calloc (1, sizeof(*p))) == NULL) adios (NULL, "out of memory"); init_decoded_content (p); - pe = p->c_cefile; + pe = &p->c_cefile; if (get_ctinfo ("message/rfc822", p, 0) == NOTOK) done (1); p->c_type = CT_MESSAGE; @@ -926,7 +920,7 @@ set_id (CT ct, int top) static int compose_content (CT ct) { - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; switch (ct->c_type) { case CT_MULTIPART: @@ -1163,7 +1157,7 @@ scan_content (CT ct) char *cp = NULL, buffer[BUFSIZ]; struct text *t = NULL; FILE *in = NULL; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; /* * handle multipart by scanning all subparts @@ -1632,7 +1626,7 @@ calculate_digest (CT ct, int asciiP) unsigned char digest[16]; unsigned char outbuf[25]; MD5_CTX mdContext; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; char *infilename = ce->ce_file ? ce->ce_file : ct->c_file; FILE *in; diff --git a/uip/mhcachesbr.c b/uip/mhcachesbr.c index 5f7e1479..f0185682 100644 --- a/uip/mhcachesbr.c +++ b/uip/mhcachesbr.c @@ -96,7 +96,7 @@ cache_content (CT ct) { int cachetype; char *file, cachefile[BUFSIZ]; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; if (!ct->c_id) { advise (NULL, "no %s: field in %s", ID_FIELD, ct->c_file); diff --git a/uip/mhfixmsg.c b/uip/mhfixmsg.c new file mode 100644 index 00000000..65e602f1 --- /dev/null +++ b/uip/mhfixmsg.c @@ -0,0 +1,1881 @@ +/* + * 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 +#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("rmmproc program", 0, RPROCSW) \ + X("normmproc", 0, NRPRCSW) \ + 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 RPROCSW: + if (!(rmmproc = *argp++) || *rmmproc == '-') + adios (NULL, "missing argument to %s", argp[-2]); + continue; + case NRPRCSW: + rmmproc = NULL; + 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 = system (rmm_command); + + free (rmm_command); + return WIFEXITED (status) ? WEXITSTATUS (status) : NOTOK; + } 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 */ +} diff --git a/uip/mhfree.c b/uip/mhfree.c index cf9e548e..2328b78a 100644 --- a/uip/mhfree.c +++ b/uip/mhfree.c @@ -262,10 +262,7 @@ free_external (CT ct) void free_encoding (CT ct, int toplevel) { - CE ce; - - if (!(ce = ct->c_cefile)) - return; + CE ce = &ct->c_cefile; if (ce->ce_fp) { fclose (ce->ce_fp); @@ -279,10 +276,7 @@ free_encoding (CT ct, int toplevel) ce->ce_file = NULL; } - if (toplevel) { - free ((char *) ce); - ct->c_cefile = NULL; - } else { + if (! toplevel) { ct->c_ceopenfnx = NULL; } } diff --git a/uip/mhlistsbr.c b/uip/mhlistsbr.c index bc29243b..b39ed3d7 100644 --- a/uip/mhlistsbr.c +++ b/uip/mhlistsbr.c @@ -280,7 +280,7 @@ list_debug (CT ct) /* print internal flags for transfer encoding */ fprintf (stderr, " transfer encoding 0x%x params 0x%x\n", - ct->c_encoding, (unsigned int)(unsigned long) ct->c_cefile); + ct->c_encoding, (unsigned int)(unsigned long) &ct->c_cefile); /* print Content-ID */ if (ct->c_id) @@ -422,12 +422,11 @@ list_application (CT ct, int toplevel, int realsize, int verbose, int debug) static int list_encoding (CT ct) { - CE ce; + CE ce = &ct->c_cefile; - if ((ce = ct->c_cefile)) - fprintf (stderr, " decoded fp 0x%x file \"%s\"\n", - (unsigned int)(unsigned long) ce->ce_fp, - ce->ce_file ? ce->ce_file : ""); + fprintf (stderr, " decoded fp 0x%x file \"%s\"\n", + (unsigned int)(unsigned long) ce->ce_fp, + ce->ce_file ? ce->ce_file : ""); return OK; } diff --git a/uip/mhoutsbr.c b/uip/mhoutsbr.c index db64c4b8..4845f7e1 100644 --- a/uip/mhoutsbr.c +++ b/uip/mhoutsbr.c @@ -300,7 +300,7 @@ write8Bit (CT ct, FILE *out) int fd; size_t inbytes; char c, *file, buffer[BUFSIZ]; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; file = NULL; if ((fd = (*ct->c_ceopenfnx) (ct, &file)) == NOTOK) @@ -329,7 +329,7 @@ writeQuoted (CT ct, FILE *out) int fd; char *cp, *file; char c, buffer[BUFSIZ]; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; file = NULL; if ((fd = (*ct->c_ceopenfnx) (ct, &file)) == NOTOK) @@ -400,7 +400,7 @@ writeBase64ct (CT ct, FILE *out) { int fd, result; char *file; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; file = NULL; if ((fd = (*ct->c_ceopenfnx) (ct, &file)) == NOTOK) diff --git a/uip/mhparse.c b/uip/mhparse.c index 4a70610e..08eaa9ab 100644 --- a/uip/mhparse.c +++ b/uip/mhparse.c @@ -104,7 +104,7 @@ void free_encoding (CT, int); * static prototypes */ static CT get_content (FILE *, char *, int); -static int get_comment (CT, char **, int); +static int get_comment (const char *, CI, char **, int); static int InitGeneric (CT); static int InitText (CT); @@ -371,7 +371,8 @@ get_content (FILE *in, char *file, int toplevel) if (debugsw) fprintf (stderr, "%s: %s\n", VRSN_FIELD, cp); - if (*cp == '(' && get_comment (ct, &cp, 0) == NOTOK) + if (*cp == '(' && + get_comment (ct->c_file, &ct->c_ctinfo, &cp, 0) == NOTOK) goto out; for (dp = cp; istoken (*dp); dp++) @@ -480,7 +481,8 @@ get_content (FILE *in, char *file, int toplevel) if (debugsw) fprintf (stderr, "%s: %s\n", MD5_FIELD, cp); - if (*cp == '(' && get_comment (ct, &cp, 0) == NOTOK) { + if (*cp == '(' && + get_comment (ct->c_file, &ct->c_ctinfo, &cp, 0) == NOTOK) { free (ep); goto out; } @@ -670,9 +672,10 @@ int get_ctinfo (char *cp, CT ct, int magic) { int i; - char *dp, **ap, **ep; + char *dp; char c; CI ci; + int status; ci = &ct->c_ctinfo; i = strlen (invo_name) + 2; @@ -696,7 +699,7 @@ get_ctinfo (char *cp, CT ct, int magic) if (debugsw) fprintf (stderr, "%s: %s\n", TYPE_FIELD, cp); - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) + if (*cp == '(' && get_comment (ct->c_file, &ct->c_ctinfo, &cp, 1) == NOTOK) return NOTOK; for (dp = cp; istoken (*dp); dp++) @@ -719,7 +722,7 @@ get_ctinfo (char *cp, CT ct, int magic) while (isspace ((unsigned char) *cp)) cp++; - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) + if (*cp == '(' && get_comment (ct->c_file, &ct->c_ctinfo, &cp, 1) == NOTOK) return NOTOK; if (*cp != '/') { @@ -732,7 +735,7 @@ get_ctinfo (char *cp, CT ct, int magic) while (isspace ((unsigned char) *cp)) cp++; - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) + if (*cp == '(' && get_comment (ct->c_file, &ct->c_ctinfo, &cp, 1) == NOTOK) return NOTOK; for (dp = cp; istoken (*dp); dp++) @@ -757,103 +760,11 @@ magic_skip: while (isspace ((unsigned char) *cp)) cp++; - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) + if (*cp == '(' && get_comment (ct->c_file, &ct->c_ctinfo, &cp, 1) == NOTOK) return NOTOK; - /* - * Parse attribute/value pairs given with Content-Type - */ - ep = (ap = ci->ci_attrs) + NPARMS; - while (*cp == ';') { - char *vp, *up; - - if (ap >= ep) { - advise (NULL, - "too many parameters in message %s's %s: field (%d max)", - ct->c_file, TYPE_FIELD, NPARMS); - return NOTOK; - } - - cp++; - while (isspace ((unsigned char) *cp)) - cp++; - - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) - return NOTOK; - - if (*cp == 0) { - advise (NULL, - "extraneous trailing ';' in message %s's %s: parameter list", - ct->c_file, TYPE_FIELD); - return OK; - } - - /* down case the attribute name */ - for (dp = cp; istoken ((unsigned char) *dp); dp++) - if (isalpha((unsigned char) *dp) && isupper ((unsigned char) *dp)) - *dp = tolower ((unsigned char) *dp); - - for (up = dp; isspace ((unsigned char) *dp);) - dp++; - if (dp == cp || *dp != '=') { - advise (NULL, - "invalid parameter in message %s's %s: field\n%*.*sparameter %s (error detected at offset %d)", - ct->c_file, TYPE_FIELD, i, i, "", cp, dp - cp); - return NOTOK; - } - - vp = (*ap = add (cp, NULL)) + (up - cp); - *vp = '\0'; - for (dp++; isspace ((unsigned char) *dp);) - dp++; - - /* now add the attribute value */ - ci->ci_values[ap - ci->ci_attrs] = vp = *ap + (dp - cp); - - if (*dp == '"') { - for (cp = ++dp, dp = vp;;) { - switch (c = *cp++) { - case '\0': -bad_quote: - advise (NULL, - "invalid quoted-string in message %s's %s: field\n%*.*s(parameter %s)", - ct->c_file, TYPE_FIELD, i, i, "", *ap); - return NOTOK; - - case '\\': - *dp++ = c; - if ((c = *cp++) == '\0') - goto bad_quote; - /* else fall... */ - - default: - *dp++ = c; - continue; - - case '"': - *dp = '\0'; - break; - } - break; - } - } else { - for (cp = dp, dp = vp; istoken (*cp); cp++, dp++) - continue; - *dp = '\0'; - } - if (!*vp) { - advise (NULL, - "invalid parameter in message %s's %s: field\n%*.*s(parameter %s)", - ct->c_file, TYPE_FIELD, i, i, "", *ap); - return NOTOK; - } - ap++; - - while (isspace ((unsigned char) *cp)) - cp++; - - if (*cp == '(' && get_comment (ct, &cp, 1) == NOTOK) - return NOTOK; + if (parse_header_attrs (ct->c_file, i, &cp, ci, &status) == NOTOK) { + return status; } /* @@ -966,14 +877,12 @@ bad_quote: static int -get_comment (CT ct, char **ap, int istype) +get_comment (const char *filename, CI ci, char **ap, int istype) { int i; char *bp, *cp; char c, buffer[BUFSIZ], *dp; - CI ci; - ci = &ct->c_ctinfo; cp = *ap; bp = buffer; cp++; @@ -983,7 +892,7 @@ get_comment (CT ct, char **ap, int istype) case '\0': invalid: advise (NULL, "invalid comment in message %s's %s: field", - ct->c_file, istype ? TYPE_FIELD : VRSN_FIELD); + filename, istype ? TYPE_FIELD : VRSN_FIELD); return NOTOK; case '\\': @@ -1631,12 +1540,6 @@ InitApplication (CT ct) static int init_encoding (CT ct, OpenCEFunc openfnx) { - CE ce; - - if ((ce = (CE) calloc (1, sizeof(*ce))) == NULL) - adios (NULL, "out of memory"); - - ct->c_cefile = ce; ct->c_ceopenfnx = openfnx; ct->c_ceclosefnx = close_encoding; ct->c_cesizefnx = size_encoding; @@ -1648,10 +1551,7 @@ init_encoding (CT ct, OpenCEFunc openfnx) void close_encoding (CT ct) { - CE ce; - - if (!(ce = ct->c_cefile)) - return; + CE ce = &ct->c_cefile; if (ce->ce_fp) { fclose (ce->ce_fp); @@ -1666,12 +1566,9 @@ size_encoding (CT ct) int fd; unsigned long size; char *file; - CE ce; + CE ce = &ct->c_cefile; struct stat st; - if (!(ce = ct->c_cefile)) - return (ct->c_end - ct->c_begin); - if (ce->ce_fp && fstat (fileno (ce->ce_fp), &st) != NOTOK) return (long) st.st_size; @@ -1740,10 +1637,9 @@ openBase64 (CT ct, char **file) char *cp, *ep, buffer[BUFSIZ]; /* sbeck -- handle suffixes */ CI ci; - CE ce; + CE ce = &ct->c_cefile; MD5_CTX mdContext; - ce = ct->c_cefile; if (ce->ce_fp) { fseek (ce->ce_fp, 0L, SEEK_SET); goto ready_to_go; @@ -1973,12 +1869,11 @@ openQuoted (CT ct, char **file) char *cp, *ep; char buffer[BUFSIZ]; unsigned char mask; - CE ce; + CE ce = &ct->c_cefile; /* sbeck -- handle suffixes */ CI ci; MD5_CTX mdContext; - ce = ct->c_cefile; if (ce->ce_fp) { fseek (ce->ce_fp, 0L, SEEK_SET); goto ready_to_go; @@ -2201,9 +2096,8 @@ open7Bit (CT ct, char **file) /* sbeck -- handle suffixes */ char *cp; CI ci; - CE ce; + CE ce = &ct->c_cefile; - ce = ct->c_cefile; if (ce->ce_fp) { fseek (ce->ce_fp, 0L, SEEK_SET); goto ready_to_go; @@ -2422,7 +2316,7 @@ openFile (CT ct, char **file) int fd, cachetype; char cachefile[BUFSIZ]; struct exbody *e = ct->c_ctexbody; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; switch (openExternal (e->eb_parent, e->eb_content, ce, file, &fd)) { case NOTOK: @@ -2505,12 +2399,11 @@ openFTP (CT ct, char **file) char *bp, *ftp, *user, *pass; char buffer[BUFSIZ], cachefile[BUFSIZ]; struct exbody *e; - CE ce; + CE ce = &ct->c_cefile; static char *username = NULL; static char *password = NULL; e = ct->c_ctexbody; - ce = ct->c_cefile; if ((ftp = context_find (nmhaccessftp)) && !*ftp) ftp = NULL; @@ -2717,7 +2610,7 @@ openMail (CT ct, char **file) int len, buflen; char *bp, buffer[BUFSIZ], *vec[7]; struct exbody *e = ct->c_ctexbody; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; switch (openExternal (e->eb_parent, e->eb_content, ce, file, &fd)) { case NOTOK: @@ -2836,7 +2729,7 @@ static int openURL (CT ct, char **file) { struct exbody *e = ct->c_ctexbody; - CE ce = ct->c_cefile; + CE ce = &ct->c_cefile; char *urlprog, *program; char buffer[BUFSIZ], cachefile[BUFSIZ]; int fd, caching, cachetype; @@ -3265,3 +3158,124 @@ get_ce_method (const char *method) { return NULL; } + +int +parse_header_attrs (const char *filename, int len, char **header_attrp, CI ci, + int *status) { + char **attr = ci->ci_attrs; + char *cp = *header_attrp; + + while (*cp == ';') { + char *dp, *vp, *up, c; + + /* Relies on knowledge of this declaration: + * char *ci_attrs[NPARMS + 2]; + */ + if (attr >= ci->ci_attrs + sizeof ci->ci_attrs/sizeof (char *) - 2) { + advise (NULL, + "too many parameters in message %s's %s: field (%d max)", + filename, TYPE_FIELD, NPARMS); + *status = NOTOK; + return NOTOK; + } + + cp++; + while (isspace ((unsigned char) *cp)) + cp++; + + if (*cp == '(' && + get_comment (filename, ci, &cp, 1) == NOTOK) { + *status = NOTOK; + return NOTOK; + } + + if (*cp == 0) { + advise (NULL, + "extraneous trailing ';' in message %s's %s: " + "parameter list", + filename, TYPE_FIELD); + *status = OK; + return NOTOK; + } + + /* down case the attribute name */ + for (dp = cp; istoken ((unsigned char) *dp); dp++) + if (isalpha((unsigned char) *dp) && isupper ((unsigned char) *dp)) + *dp = tolower ((unsigned char) *dp); + + for (up = dp; isspace ((unsigned char) *dp);) + dp++; + if (dp == cp || *dp != '=') { + advise (NULL, + "invalid parameter in message %s's %s: " + "field\n%*.*sparameter %s (error detected at offset %d)", + filename, TYPE_FIELD, len, len, "", cp, dp - cp); + *status = NOTOK; + return NOTOK; + } + + vp = (*attr = add (cp, NULL)) + (up - cp); + *vp = '\0'; + for (dp++; isspace ((unsigned char) *dp);) + dp++; + + /* Now store the attribute value. */ + ci->ci_values[attr - ci->ci_attrs] = vp = *attr + (dp - cp); + + if (*dp == '"') { + for (cp = ++dp, dp = vp;;) { + switch (c = *cp++) { + case '\0': +bad_quote: + advise (NULL, + "invalid quoted-string in message %s's %s: " + "field\n%*.*s(parameter %s)", + filename, TYPE_FIELD, len, len, "", *attr); + *status = NOTOK; + return NOTOK; + + case '\\': + *dp++ = c; + if ((c = *cp++) == '\0') + goto bad_quote; + /* else fall... */ + + default: + *dp++ = c; + continue; + + case '"': + *dp = '\0'; + break; + } + break; + } + } else { + for (cp = dp, dp = vp; istoken (*cp); cp++, dp++) + continue; + *dp = '\0'; + } + if (!*vp) { + advise (NULL, + "invalid parameter in message %s's %s: " + "field\n%*.*s(parameter %s)", + filename, TYPE_FIELD, len, len, "", *attr); + *status = NOTOK; + return NOTOK; + } + + while (isspace ((unsigned char) *cp)) + cp++; + + if (*cp == '(' && + get_comment (filename, ci, &cp, 1) == NOTOK) { + *status = NOTOK; + return NOTOK; + } + + ++attr; + } + + *header_attrp = cp; + return OK; +} diff --git a/uip/mhstoresbr.c b/uip/mhstoresbr.c index 6badafeb..57604b50 100644 --- a/uip/mhstoresbr.c +++ b/uip/mhstoresbr.c @@ -1021,7 +1021,7 @@ static void get_storeproc (CT ct) { char **ap, **ep, *cp; - CI ci = &ct->c_ctinfo; + CI ci; /* * If the storeproc has already been defined, @@ -1031,13 +1031,44 @@ get_storeproc (CT ct) if (ct->c_storeproc) return; + /* + * If there's a Content-Disposition header and it has a filename, + * use that (RFC-2183). + */ + if (ct->c_dispo) { + char *cp = strchr (ct->c_dispo, ';'); + CI ci = calloc (1, sizeof *ci); + int status; + int found_filename = 0; + + if (cp && parse_header_attrs (ct->c_file, strlen (invo_name) + 2, &cp, + ci, &status) == OK) { + for (ap = ci->ci_attrs, ep = ci->ci_values; *ap; ap++, ep++) { + if (! strcasecmp (*ap, "filename") + && *(cp = *ep) != '/' + && *cp != '.' + && *cp != '|' + && *cp != '!' + && !strchr (cp, '%')) { + ct->c_storeproc = add (cp, NULL); + found_filename = 1; + } + free (*ap); + } + } + + free (ci); + if (found_filename) return; + } + /* * Check the attribute/value pairs, for the attribute "name". * If found, do a few sanity checks and copy the value into * the storeproc. */ + ci = &ct->c_ctinfo; for (ap = ci->ci_attrs, ep = ci->ci_values; *ap; ap++, ep++) { - if (!mh_strcasecmp (*ap, "name") + if (! strcasecmp (*ap, "name") && *(cp = *ep) != '/' && *cp != '.' && *cp != '|' diff --git a/uip/refile.c b/uip/refile.c index 8dd04fb9..725a7ada 100644 --- a/uip/refile.c +++ b/uip/refile.c @@ -12,6 +12,7 @@ #include #include #include +#include #define REFILE_SWITCHES \ X("draft", 0, DRAFTSW) \ @@ -19,6 +20,8 @@ X("nolink", 0, NLINKSW) \ X("preserve", 0, PRESSW) \ X("nopreserve", 0, NPRESSW) \ + X("retainsequences", 0, RETAINSEQSSW) \ + X("noretainsequences", 0, NRETAINSEQSSW) \ X("unlink", 0, UNLINKSW) \ X("nounlink", 0, NUNLINKSW) \ X("src +folder", 0, SRCSW) \ @@ -49,13 +52,14 @@ struct st_fold { static void opnfolds (struct st_fold *, int); static void clsfolds (struct st_fold *, int); static void remove_files (int, char **); -static int m_file (char *, struct st_fold *, int, int, int); +static int m_file (struct msgs *, char *, int, struct st_fold *, int, int, int); +static void copy_seqs (struct msgs *, int, struct msgs *, int); int main (int argc, char **argv) { - int linkf = 0, preserve = 0, filep = 0; + int linkf = 0, preserve = 0, retainseqs = 0, filep = 0; int foldp = 0, isdf = 0, unlink_msgs = 0; int i, msgnum; char *cp, *folder = NULL, buf[BUFSIZ]; @@ -83,13 +87,13 @@ main (int argc, char **argv) while ((cp = *argp++)) { if (*cp == '-') { switch (smatch (++cp, switches)) { - case AMBIGSW: + case AMBIGSW: ambigsw (cp, switches); done (1); - case UNKWNSW: + case UNKWNSW: adios (NULL, "-%s unknown\n", cp); - case HELPSW: + case HELPSW: snprintf (buf, sizeof(buf), "%s [msgs] [switches] +folder ...", invo_name); print_help (buf, switches, 1); @@ -98,20 +102,27 @@ main (int argc, char **argv) print_version(invo_name); done (0); - case LINKSW: + case LINKSW: linkf++; continue; - case NLINKSW: + case NLINKSW: linkf = 0; continue; - case PRESSW: + case PRESSW: preserve++; continue; - case NPRESSW: + case NPRESSW: preserve = 0; continue; + case RETAINSEQSSW: + retainseqs = 1; + continue; + case NRETAINSEQSSW: + retainseqs = 0; + continue; + case UNLINKSW: unlink_msgs++; continue; @@ -119,7 +130,7 @@ main (int argc, char **argv) unlink_msgs = 0; continue; - case SRCSW: + case SRCSW: if (folder) adios (NULL, "only one source folder at a time!"); if (!(cp = *argp++) || *cp == '-') @@ -133,7 +144,7 @@ main (int argc, char **argv) isdf = 0; files[filep++] = getcpy (m_draft (NULL, NULL, 1, &isdf)); continue; - case FILESW: + case FILESW: if (filep > NFOLDERS) adios (NULL, "only %d files allowed!", NFOLDERS); if (!(cp = *argp++) || *cp == '-') @@ -141,11 +152,11 @@ main (int argc, char **argv) files[filep++] = path (cp, TFILE); continue; - case RPROCSW: + case RPROCSW: if (!(rmmproc = *argp++) || *rmmproc == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; - case NRPRCSW: + case NRPRCSW: rmmproc = NULL; continue; } @@ -177,7 +188,7 @@ main (int argc, char **argv) adios (NULL, "use -file or some messages, not both"); opnfolds (folders, foldp); for (i = 0; i < filep; i++) - if (m_file (files[i], folders, foldp, preserve, 0)) + if (m_file (0, files[i], 0, folders, foldp, preserve, 0)) done (1); /* If -nolink, then "remove" files */ if (!linkf) @@ -220,7 +231,8 @@ main (int argc, char **argv) for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) { if (is_selected (mp, msgnum)) { cp = getcpy (m_name (msgnum)); - if (m_file (cp, folders, foldp, preserve, !linkf)) + if (m_file (mp, cp, retainseqs ? msgnum : 0, folders, foldp, + preserve, !linkf)) done (1); free (cp); } @@ -347,12 +359,13 @@ remove_files (int filep, char **files) /* - * Link (or copy) the message into each of - * the destination folders. + * Link (or copy) the message into each of the destination folders. + * If oldmsgnum is not 0, call copy_seqs(). */ static int -m_file (char *msgfile, struct st_fold *folders, int nfolders, int preserve, int refile) +m_file (struct msgs *mp, char *msgfile, int oldmsgnum, + struct st_fold *folders, int nfolders, int preserve, int refile) { int msgnum; struct st_fold *fp, *ep; @@ -360,6 +373,29 @@ m_file (char *msgfile, struct st_fold *folders, int nfolders, int preserve, int for (fp = folders, ep = folders + nfolders; fp < ep; fp++) { if ((msgnum = folder_addmsg (&fp->f_mp, msgfile, 1, 0, preserve, nfolders == 1 && refile, maildir)) == -1) return 1; + if (oldmsgnum) copy_seqs (mp, oldmsgnum, fp->f_mp, msgnum); } return 0; } + + +/* + * Copy sequence information for a refiled message to its + * new folder. Skip the cur sequence. + */ +static void +copy_seqs (struct msgs *oldmp, int oldmsgnum, struct msgs *newmp, int newmsgnum) +{ + char **seq; + int seqnum; + + for (seq = oldmp->msgattrs, seqnum = 0; *seq; ++seq, ++seqnum) { + if (strcmp (current, *seq)) { + assert (seqnum == seq_getnum (oldmp, *seq)); + if (in_sequence (oldmp, seqnum, oldmsgnum)) { + seq_addmsg (newmp, *seq, newmsgnum, + is_seq_private (oldmp, seqnum) ? 0 : 1, 0); + } + } + } +} diff --git a/uip/rmm.c b/uip/rmm.c index 02a55910..64d85cea 100644 --- a/uip/rmm.c +++ b/uip/rmm.c @@ -13,6 +13,8 @@ #define RMM_SWITCHES \ X("unlink", 0, UNLINKSW) \ X("nounlink", 0, NUNLINKSW) \ + X("rmmproc program", 0, RPROCSW) \ + X("normmproc", 0, NRPRCSW) \ X("version", 0, VERSIONSW) \ X("help", 0, HELPSW) \ @@ -50,13 +52,13 @@ main (int argc, char **argv) while ((cp = *argp++)) { if (*cp == '-') { switch (smatch (++cp, switches)) { - case AMBIGSW: + case AMBIGSW: ambigsw (cp, switches); done (1); - case UNKWNSW: + case UNKWNSW: adios (NULL, "-%s unknown\n", cp); - case HELPSW: + case HELPSW: snprintf (buf, sizeof(buf), "%s [+folder] [msgs] [switches]", invo_name); print_help (buf, switches, 1); @@ -71,6 +73,14 @@ main (int argc, char **argv) case NUNLINKSW: unlink_msgs = 0; continue; + + case RPROCSW: + if (!(rmmproc = *argp++) || *rmmproc == '-') + adios (NULL, "missing argument to %s", argp[-2]); + continue; + case NRPRCSW: + rmmproc = NULL; + continue; } } if (*cp == '+' || *cp == '@') {