]> diplodocus.org Git - nmh/commitdiff
Merge remote-tracking branch 'origin' into convertargs
authorDavid Levine <levinedl@acm.org>
Mon, 19 Jan 2015 03:31:20 +0000 (21:31 -0600)
committerDavid Levine <levinedl@acm.org>
Mon, 19 Jan 2015 03:31:20 +0000 (21:31 -0600)
26 files changed:
.gitignore
MACHINES
Makefile.am
configure.ac
docs/README-iCalendar [deleted file]
docs/contrib/replaliases [new file with mode: 0644]
docs/pending-release-notes
etc/mhical.12hour [new file with mode: 0644]
etc/mhical.24hour [new file with mode: 0644]
etc/mhl.replywithoutbody [new file with mode: 0644]
etc/mhn.defaults.sh
h/icalendar.h [new file with mode: 0644]
h/mime.h
man/mh-mime.man
man/mhbuild.man
man/mhical.man [new file with mode: 0644]
man/repl.man
sbr/datetime.c [new file with mode: 0644]
sbr/icalendar.l [new file with mode: 0644]
sbr/icalparse.y [new file with mode: 0644]
test/mhical/test-mhical [new file with mode: 0755]
test/repl/test-convert [new file with mode: 0755]
test/repl/test-repl
uip/mhbuildsbr.c
uip/mhical.c [new file with mode: 0644]
uip/repl.c

index 4e9297dbb07e042bd42b014e8398f593b36a1436..1359f96bfafc6cef3f15a2555e46a15cc0aa8b9d 100644 (file)
@@ -22,6 +22,8 @@
 /ylwrap
 /nmh-*.tar.gz
 /nmh-*.tar.gz.sig
+/sbr/icalendar.c
+/sbr/icalparse.[hc]
 
 # Removed by maintainer-clean:
 /autom4te.cache/
@@ -78,6 +80,7 @@ a.out.dSYM/
 /uip/mark
 /uip/mhbuild
 /uip/mhfixmsg
+/uip/mhical
 /uip/mhl
 /uip/mhlist
 /uip/mhn
index 1547a4acc66fcc9c6e5060095fb554d29f483200..d930dc23e7080b2f8212ceac4c6875734ae30a01 100644 (file)
--- a/MACHINES
+++ b/MACHINES
@@ -22,9 +22,10 @@ the exceptions noted below), using an ANSI C compiler, such as gcc:
 
 On all platforms, the following programs are required to build nmh from a
 snapshot of the source code repository:
-    autoconf
-    automake
-    flex
+    autoconf 2.68 or later
+    automake 1.12 or later
+    flex 2.5.4 or later
+    bison 2.3 or later,  Berkeley yacc 1.9 or later, or Solaris yacc 4.0
 They are not required if building from an nmh distribution (.tar.gz) file.
 
 Platform-specific notes follow.
index af4a2fee17c0d543aa50260a421cbc911a86b7bc..90dee631b8481d9659df8dc36fdbfe28e4c43a11 100644 (file)
@@ -7,6 +7,8 @@
 ## We set this to get our autoconf macros in the m4 directory
 ACLOCAL_AMFLAGS = -I m4
 
+AM_YFLAGS = -d
+
 ##
 ## This is the default set of libraries all programs link against.  Some
 ## programs add extra libraries to this set, so they override this with
@@ -72,7 +74,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \
        test/mhbuild/test-ext-params \
        test/mhbuild/test-forw test/mhbuild/test-header-encode \
        test/mhbuild/test-utf8-body \
-       test/mhfixmsg/test-mhfixmsg \
+       test/mhfixmsg/test-mhfixmsg test/mhical/test-mhical \
        test/mhl/test-mhl-flags \
        test/mhlist/test-mhlist test/mhlist/test-ext-params \
        test/mhmail/test-mhmail \
@@ -89,7 +91,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \
        test/post/test-post-group test/post/test-mts test/post/test-messageid \
        test/post/test-sendfiles test/prompter/test-prompter \
        test/rcv/test-rcvdist test/rcv/test-rcvpack test/rcv/test-rcvstore \
-       test/rcv/test-rcvtty test/refile/test-refile \
+       test/rcv/test-rcvtty test/refile/test-refile test/repl/test-convert \
        test/repl/test-if-str test/repl/test-trailing-newline \
        test/repl/test-multicomp test/repl/test-repl \
        test/scan/test-scan test/scan/test-scan-multibyte \
@@ -116,7 +118,7 @@ DISTCHECK_CONFIGURE_FLAGS = DISABLE_SETGID_MAIL=1 \
 ## automake 1.12.6 on FreeBSD 9 needs the sbr/dtimep.c.
 ##
 CLEANFILES = config/version.c sbr/sigmsg.h sbr/dtimep.c etc/mts.conf \
-            etc/gen-ctype-checked sbr/ctype-checked.h sbr/ctype-checked.c \
+            etc/gen-ctype-checked sbr/ctype-checked.[hc] \
             etc/mhn.defaults man/man.sed man/mh-chart.man $(man_MANS) \
             *.plist
 clean-local:
@@ -150,7 +152,7 @@ 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/mhfixmsg uip/mhlist uip/mhn \
+              uip/mark uip/mhbuild uip/mhfixmsg uip/mhical uip/mhlist uip/mhn \
               uip/mhparam uip/mhpath uip/mhshow uip/mhstore uip/msgchk \
               uip/new uip/packf uip/pick uip/prompter uip/refile \
               uip/repl uip/rmf uip/rmm uip/scan uip/send uip/show uip/sortm \
@@ -185,7 +187,7 @@ noinst_LIBRARIES = sbr/libmh.a mts/libmts.a
 ## them, but that might change in the future.
 ##
 noinst_HEADERS = h/addrsbr.h h/aliasbr.h h/crawl_folders.h h/dropsbr.h \
-                h/fmt_compile.h h/fmt_scan.h h/md5.h h/mf.h \
+                h/fmt_compile.h h/fmt_scan.h h/icalendar.h h/md5.h h/mf.h \
                 h/mh.h h/mhcachesbr.h h/mhparse.h h/mime.h \
                 h/mts.h h/nmh.h h/picksbr.h h/popsbr.h h/prototypes.h \
                 h/rcvmail.h h/scansbr.h h/signals.h h/tws.h h/utils.h \
@@ -195,15 +197,17 @@ noinst_HEADERS = h/addrsbr.h h/aliasbr.h h/crawl_folders.h h/dropsbr.h \
 ## Extra files we need to install in various places
 ##
 dist_nmhetc_DATA = etc/MailAliases etc/components etc/digestcomps \
-                   etc/distcomps etc/forwcomps etc/mhl.body etc/mhl.digest \
-                   etc/mhl.format etc/mhl.forward etc/mhl.headers \
-                   etc/mhl.reply etc/mhshow.marker etc/rcvdistcomps \
-                   etc/rcvdistcomps.outbox etc/replcomps etc/replgroupcomps \
-                   etc/scan.MMDDYY \
-                   etc/scan.YYYYMMDD etc/scan.curses etc/scan.default \
-                   etc/scan.highlighted \
-                   etc/scan.mailx etc/scan.nomime etc/scan.size etc/scan.time \
-                   etc/scan.timely etc/scan.unseen
+                  etc/distcomps etc/forwcomps \
+                  etc/mhical.12hour etc/mhical.24hour \
+                  etc/mhl.body etc/mhl.digest etc/mhl.format etc/mhl.forward \
+                  etc/mhl.headers etc/mhl.reply etc/mhl.replywithoutbody \
+                  etc/mhshow.marker etc/rcvdistcomps etc/rcvdistcomps.outbox \
+                  etc/replcomps etc/replgroupcomps \
+                  etc/scan.MMDDYY \
+                  etc/scan.YYYYMMDD etc/scan.curses etc/scan.default \
+                  etc/scan.highlighted \
+                  etc/scan.mailx etc/scan.nomime etc/scan.size etc/scan.time \
+                  etc/scan.timely etc/scan.unseen
 
 ##
 ## The same as above, but we don't include these in the distribution
@@ -220,7 +224,7 @@ dist_doc_DATA = COPYRIGHT INSTALL NEWS README VERSION \
                docs/MAIL.FILTERING docs/MAILING-LISTS docs/README-ATTACHMENTS \
                docs/README-HOOKS docs/README-components docs/README.about \
                docs/README.SASL docs/README.developers docs/README.manpages \
-               docs/README-iCalendar docs/TODO
+               docs/TODO
 
 ##
 ## Contribs that get installed in docdir/contrib/
@@ -228,6 +232,7 @@ dist_doc_DATA = COPYRIGHT INSTALL NEWS README VERSION \
 contribdir = $(docdir)/contrib
 dist_contrib_SCRIPTS = docs/contrib/replyfilter docs/contrib/build_nmh \
                       docs/contrib/ml docs/contrib/vpick
+dist_contrib_DATA = docs/contrib/replaliases
 
 ##
 ## Our man pages
@@ -239,8 +244,8 @@ man_MANS = man/ali.1 man/anno.1 man/ap.8 man/burst.1 man/comp.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-mime.7 man/mh-profile.5 \
           man/mh_profile.5 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/mhfixmsg.1 man/mhical.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/mh-mkstemp.1 man/msgchk.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 \
@@ -259,10 +264,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-mime.man \
           man/mh-profile.man man/mh_profile.man man/mh-sequence.man \
-          man/mh-tailor.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/mh-mkstemp.man \
-          man/msgchk.man man/mts.conf.man man/new.man \
+          man/mh-tailor.man man/mhbuild.man man/mhfixmsg.man man/mhical.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/mh-mkstemp.man man/msgchk.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 \
@@ -274,8 +279,9 @@ man_SRCS = man/ali.man man/anno.man man/ap.man man/burst.man man/comp.man \
 ## Files we need to include in the distribution which aren't found by
 ## Automake using the automatic rules
 ##
-EXTRA_DIST = autogen.sh config/version.sh sbr/sigmsg.awk etc/mts.conf.in \
-            etc/mhn.defaults.sh etc/sendfiles $(MHNSEARCHPROG) DATE MACHINES \
+EXTRA_DIST = autogen.sh config/version.sh sbr/sigmsg.awk sbr/icalparse.h \
+            etc/mts.conf.in etc/mhn.defaults.sh etc/sendfiles \
+            $(MHNSEARCHPROG) DATE MACHINES \
             docs/ChangeLog_MH-3_to_MH-6.6 \
             docs/ChangeLog_MH-6.7.0_to_MH-6.8.4.html \
             test/README test/fakesendmail $(TESTS) test/inc/deb359167.mbox \
@@ -335,7 +341,8 @@ uip_mark_LDADD = $(LDADD) $(POSTLINK)
 
 uip_mhbuild_SOURCES = uip/mhbuild.c uip/mhbuildsbr.c uip/mhcachesbr.c \
                      uip/mhlistsbr.c uip/mhoutsbr.c uip/mhmisc.c  \
-                     uip/mhfree.c uip/mhparse.c uip/md5.c
+                     uip/mhfree.c uip/mhparse.c uip/md5.c \
+                     uip/mhstoresbr.c uip/mhshowsbr.c
 uip_mhbuild_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_mhfixmsg_SOURCES = uip/mhfixmsg.c uip/mhparse.c uip/mhcachesbr.c \
@@ -343,6 +350,9 @@ uip_mhfixmsg_SOURCES = uip/mhfixmsg.c uip/mhparse.c uip/mhcachesbr.c \
                       uip/mhshowsbr.c uip/mhlistsbr.c uip/md5.c
 uip_mhfixmsg_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
+uip_mhical_SOURCES = uip/mhical.c
+uip_mhical_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
+
 uip_mhlist_SOURCES = uip/mhlist.c uip/mhparse.c uip/mhcachesbr.c \
                     uip/mhlistsbr.c uip/mhmisc.c uip/mhfree.c uip/md5.c
 uip_mhlist_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
@@ -573,7 +583,9 @@ sbr_libmh_a_SOURCES = sbr/addrsbr.c sbr/ambigsw.c sbr/atooi.c sbr/arglist.c \
                      sbr/getcpy.c sbr/geteditor.c sbr/getfolder.c \
                      sbr/getpass.c \
                      sbr/fmt_addr.c sbr/fmt_compile.c sbr/fmt_new.c \
-                     sbr/fmt_rfc2047.c sbr/fmt_scan.c sbr/lock_file.c \
+                     sbr/fmt_rfc2047.c sbr/fmt_scan.c \
+                     sbr/icalparse.y sbr/icalendar.l sbr/datetime.c \
+                     sbr/lock_file.c \
                      sbr/m_atoi.c sbr/m_backup.c sbr/m_convert.c \
                      sbr/m_draft.c sbr/m_getfld.c sbr/m_gmprot.c \
                      sbr/m_maildir.c sbr/m_name.c sbr/m_popen.c sbr/m_rand.c \
index 02f0a04474f3a8d476a4270a8819de53477a4e38..754ccb6ac2979551b476c40c4cb246394e230bbb 100644 (file)
@@ -206,11 +206,12 @@ AC_HEADER_ASSERT
 dnl ------------------
 dnl CHECK FOR PROGRAMS
 dnl ------------------
-AC_PROG_MAKE_SET       dnl Does make define $MAKE
-AC_PROG_INSTALL                dnl Check for BSD compatible `install'
-AC_PROG_RANLIB         dnl Check for `ranlib'
+AC_PROG_MAKE_SET        dnl Does make define $MAKE
+AC_PROG_INSTALL         dnl Check for BSD compatible `install'
+AC_PROG_RANLIB          dnl Check for `ranlib'
 AC_PROG_AWK             dnl Check for mawk,gawk,nawk, then awk
-AC_PROG_SED            dnl Check for Posix-compliant sed
+AC_PROG_SED             dnl Check for Posix-compliant sed
+AC_PROG_YACC            dnl Check for yacc/bison
 AM_PROG_LEX             dnl Check for lex/flex
 
 AM_PROG_AR
diff --git a/docs/README-iCalendar b/docs/README-iCalendar
deleted file mode 100644 (file)
index ea275a5..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-To view an iCalendar (text/calendar, .ics, RFC 5545) attachment with
-mhshow or mhn, all I had to do was install calcurse
-(http://www.calcurse.org/), set it up once, and add this to my
-profile:
-
-#: With mhshow, the following just views the calendar attachment.
-#: With mhn, it inserts it into my personal calcurse calendar.
-#: calcurse must be set up before or at first use.  To set up beforehand,
-#:   either run it without any arguments and then "qqqy", or:
-#:   $ echo qqqy | calcurse
-#: The range of 60 is in days:  anything after that range (or before the
-#:   current date) won't be shown.
-#: The grep of TZID works around a current (v. 3.1.4) calcurse
-#:   deficiency:  it doesn't support timezones.
-#: The | cat below allows a sequence of shell commands.
-mhshow-show-text/calendar:
-  %lcalcurse -c /dev/null --read-only --import '%F' --range=60 --todo | cat;
-  calcurse --gc;
-  grep TZID: '%F'
-mhn-show-text/calendar:
-  %lcalcurse --import '%F' | cat;
-  grep TZID: '%F'
-
-
-To create and send out a calendar request:
-  1) Launch calcurse and create the calendar request.
-  2) Export the calendar request to a file, either using the
-     "x" command from within interactive calcurse or using the
-     -x/--export command-line option.  The output filename
-     should have an extension of .ics if you'll use send -attach.
-  3) For compatibility with MS Outlook, edit the calendar
-     request file and insert this line into the VCALENDAR
-     section:
-METHOD:REQUEST
-     and insert this line into the VEVENT section:
-UID:<uid string>
-     where <uid string> should be unique.
-
-     If you insert the following 5 lines just before the
-     END:VEVENT line, it will enable alarms for recipients who
-     have support for them:
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:REMINDER
-TRIGGER;RELATED=START:-PT15M
-END:VALARM
-  4) Attach the calendar request file to a message.  With
-     mhbuild, be sure to specify a Content-Disposition type
-     of "inline", using {inline}.  The send -attach support
-     handles this properly.
-
-
-To respond to a calendar request:
-  1) Store the calendar request in a file using, e.g.,
-     mhstore -type text/calendar
-  2) Edit the calendar request, changing the METHOD and your
-     ATTENDEE lines to look like:
-METHOD:REPLY
-ATTENDEE;PARTSTAT=<action>;MAILTO:<your email address>
-     where <action> is one of ACCEPTED, DECLINED, TENTATIVE,
-     or DELEGATED (or COMPLETED or IN-PROCESS for to-do's).
-  3) Send that edited request by attaching it to a reply to
-     the meeting requestor; see last step under "To create and
-     send out a calendar request" above.
diff --git a/docs/contrib/replaliases b/docs/contrib/replaliases
new file mode 100644 (file)
index 0000000..6848dc2
--- /dev/null
@@ -0,0 +1,81 @@
+#### replaliases
+####
+#### convenience functions for various repl(1) commands
+#### They're functions instead of aliases for portability.
+
+#### If using par (see mhn.defaults), it helps to have its PARINIT
+#### environment variable set.  If you really want it to be null,
+#### either comment this out or set it to, e.g., ' '.
+#### Removed "R" from PARINIT recommendation in par(1) man page so
+#### that it doesn't consider words that are too long to be an error.
+if [ -z "$PARINIT" ]; then
+    PARINIT='rTbgq B=.,?_A_a Q=_s>|'
+    export PARINIT
+fi
+
+#### Reply, including text/html (converted to text/plain) and
+#### text/plain parts.
+####
+#### Optional arguments:
+####   -h to disable conversion of text/html parts
+####   -p to disable conversion of text/plain parts
+#### All other arguments are passed to repl.
+#### The -p argument can be useful with improperly structured
+#### messages, such as those that use multipart/related when they
+#### should have used multipart/alternative.
+rt() {
+    if [ "$1" = -h ]; then
+        shift
+        repl -filter mhl.replywithoutbody -convertargs text/plain '' "$@"
+    elif [ "$1" = -p ]; then
+        shift
+        repl -filter mhl.replywithoutbody -convertargs text/html '' "$@"
+    else
+        repl -filter mhl.replywithoutbody \
+             -convertargs text/html '' -convertargs text/plain '' "$@"
+    fi
+}
+
+
+#### Add -editor mhbuild to above.  Useful only when attachments
+#### won't be added to the message.
+####
+#### To ease editing at the What now? prompt, add a line like this to
+#### your .mh_profile:
+####     mhbuild-next: $EDITOR
+#### assuming that your EDTIOR environment variable is set; if not,
+#### replace $EDITOR above with the name of your editor.  Without that
+#### profile entry, enter "e[dit] $EDITOR" at the What now? prompt.
+rtm() {
+    rt -editor mhbuild "$@"
+}
+
+
+#### accept a calendar request
+calaccept() {
+    repl -noformat -editor mhbuild \
+         -convertargs text/calendar '-reply accept -contenttype' "$@"
+}
+
+#### decline a calendar request
+caldecline() {
+    repl -noformat -editor mhbuild \
+         -convertargs text/calendar '-reply decline -contenttype' "$@"
+}
+
+#### reply as tentative to a calendar request
+caltentative() {
+    repl -noformat -editor mhbuild \
+         -convertargs text/calendar '-reply tentative -contenttype' "$@"
+}
+
+#### cancel a calendar request
+calcancel() {
+    repl -noformat -editor mhbuild \
+         -convertargs text/calendar '-cancel -contenttype' "$@"
+}
+
+
+# Local Variables:
+# mode: sh
+# End:
index d1d0d5831646add1692f7bf05072fc7e73a3fee4..efcdd072142a11de122d6345b54e027b67b949d7 100644 (file)
@@ -29,6 +29,11 @@ NEW FEATURES
   to mhfixmsg(1).
 - mhfixmsg now removes an extraneous trailing semicolon from header
   parameter lists.
+- Added -convertargs switch to repl(1), to pass arguments to programs
+  specified in the user's profile or mhn.defaults to convert message
+  content.
+- Added mhical(1), to display, reply to, and cancel iCalendar (RFC 5545)
+  event requests.
 - added multiply format function
 
 -----------------
diff --git a/etc/mhical.12hour b/etc/mhical.12hour
new file mode 100644 (file)
index 0000000..2ed1bd5
--- /dev/null
@@ -0,0 +1,49 @@
+%; mhical.12hour
+%; Form file for mhical that directs it to use 12-hour instead of 24-hour
+%; time format.
+%;
+%; See mhical.24hour for list of supported components.
+%;
+%; Here's what it does to the %{dtstart} special component:
+%;
+%; 1) Enable bold, and if the terminal supports color, green attributes.
+%; 2) If the time zone is explicit, coerce the date to the local time zone;
+%;    otherwise, assume that it's a floating time.
+%; 3) Output the date in "ddd DD MM YYYY" format.
+%; 4) Convert hours from 24- to 12-hour clock and output them as soon as
+%;    they are calculated.  Store AM or PM in the str register.
+%; 5) Output :minutes.
+%; 6) Output AM or PM.
+%; 7) If the time zone is explicit, output it.
+%; 8) Reset all terminal attributes.
+%; The timezone is not output for the dtend component because it is
+%; almost always (except across a daylight saving time transition)
+%; the same as for the dtstart component.
+%;
+%<(nonnull{method})Method: %(putstr{method})\n%>\
+%<(nonnull{organizer})Organizer: %(putstr{organizer})\n%>\
+%<(nonnull{summary})Summary: %(putstr{summary})\n%>\
+%<(nonnull{description})Description: %(putstr{description})\n%>\
+%<(nonnull{location})Location: %(putstr{location})\n%>\
+%<(nonnull{dtstart})At: \
+%<(hascolor)%(zputlit(fgcolor green))%>%(zputlit(bold))\
+%(void(szone{dtstart}))%<(gt 0)%(date2local{dtstart})%>\
+%(day{dtstart}), %02(mday{dtstart}) %(month{dtstart}) %(year{dtstart}) \
+%(void(hour{dtstart}))%<(eq 0)%(num 12)%(void(lit AM))\
+%?(eq 12)%(num 12)%(void(lit PM))\
+%?(gt 12)%2(modulo 12)%(void(lit PM))\
+%|%2(putnumf)%(void(lit AM))%>\
+:%02(min{dtstart}) \
+%(putlit)%; AM or PM
+%(void(szone{dtstart}))%<(gt 0) %(tzone{dtstart})%>\
+%(zputlit(resetterm))\n%>\
+%<(nonnull{dtstart})To: \
+%(void(szone{dtend}))%<(gt 0)%(date2local{dtend})%>\
+%(day{dtend}), %02(mday{dtend}) %(month{dtend}) %(year{dtend}) \
+%(void(hour{dtend}))%<(eq 0)%(num 12)%(void(lit AM))\
+%?(eq 12)%(num 12)%(void(lit PM))\
+%?(gt 12)%2(modulo 12)%(void(lit PM))\
+%|%2(putnumf)%(void(lit AM))%>\
+:%02(min{dtend}) \
+%(putlit)\n%>%; AM or PM
+%<(nonnull{attendees})%(putstr{attendees})\n%>\
diff --git a/etc/mhical.24hour b/etc/mhical.24hour
new file mode 100644 (file)
index 0000000..a12fc0e
--- /dev/null
@@ -0,0 +1,50 @@
+%; mhical.24hour
+%; Form file for mhical that directs it to use 24-hour time format.
+%;
+%; Default form file for mhical.  The following components
+%; are supported:
+%;     method
+%;     organizer
+%;     summary
+%;     description
+%;     location
+%;     dtstart
+%;     dtend
+%;     attendees
+%; Each corresponds to the iCalendar property of the same name as
+%; defined in RFC 5545, with the exception of "attendees".  That is a
+%; comma-delimited list of the common name (CN), if available, or
+%; email address of Attendee properties.
+%; The component names listed above are case-sensitive because
+%; fmt_findcomp() is case-sensitive.
+%;
+%; Here's what it does to the %{dtstart} special component for the
+%; event:
+%; 1) Enable bold, and if the terminal supports color, green attributes.
+%; 2) If the time zone is explicit, coerce the date to the local time zone;
+%;    otherwise, assume that it's a floating time.
+%; 3) Output the date in "ddd DD MM YYYY" format.
+%; 4) Output the time in "HH:MM" format.
+%; 5) If the time zone is explicit, output it.
+%; 6) Reset all terminal attributes.
+%; The timezone is not output for the dtend component because it is
+%; almost always (except across a daylight saving time transition)
+%; the same as for the dtstart component.
+%;
+%<(nonnull{method})Method: %(putstr{method})\n%>\
+%<(nonnull{organizer})Organizer: %(putstr{organizer})\n%>\
+%<(nonnull{summary})Summary: %(putstr{summary})\n%>\
+%<(nonnull{description})Description: %(putstr{description})\n%>\
+%<(nonnull{location})Location: %(putstr{location})\n%>\
+%<(nonnull{dtstart})At: \
+%<(hascolor)%(zputlit(fgcolor green))%>%(zputlit(bold))\
+%(void(szone{dtstart}))%<(gt 0)%(date2local{dtstart})%>\
+%(day{dtstart}), %02(mday{dtstart}) %(month{dtstart}) %(year{dtstart}) \
+%02(hour{dtstart}):%02(min{dtstart})\
+%(void(szone{dtstart}))%<(gt 0) %(tzone{dtstart})%>\
+%(zputlit(resetterm))\n%>\
+%<(nonnull{dtstart})To: \
+%(void(szone{dtend}))%<(gt 0)%(date2local{dtend})%>\
+%(day{dtend}), %02(mday{dtend}) %(month{dtend}) %(year{dtend}) \
+%02(hour{dtend}):%02(min{dtend})\n%>\
+%<(nonnull{attendees})%(putstr{attendees})\n%>\
diff --git a/etc/mhl.replywithoutbody b/etc/mhl.replywithoutbody
new file mode 100644 (file)
index 0000000..6abeab3
--- /dev/null
@@ -0,0 +1,5 @@
+; mhl.replywithoutbody
+;
+; message filter for `repl' (repl -format) that excludes the message body
+;
+from:nocomponent,formatfield="%(decode(friendly{text})) writes:"
index 2f7ddaf83a81ead4d3a405d6aeaa5d6148f534aa..bfc7924fdb5b2aec80fcf0af925184bdcf7070ab 100755 (executable)
@@ -25,8 +25,24 @@ fi
 TMP=/tmp/nmh_temp.$$
 trap "rm -f $TMP" 0 1 2 3 13 15
 
+if [ ! -z `$SEARCHPROG "$SEARCHPATH" par` ]; then
+    #### The widths here correspond to those for the text browsers below.
+    textfmt=' | par 64'
+    replfmt=" | sed 's/^\(.\)/> \1/; s/^$/>/;' | par 64"
+elif [ ! -z `$SEARCHPROG "$SEARCHPATH" fmt` ]; then
+    textfmt=' | fmt'
+    replfmt=" | fmt | sed 's/^\(.\)/> \1/; s/^$/>/;'"
+else
+    textfmt=
+    replfmt=
+fi
+[ ! -z `$SEARCHPROG "$SEARCHPATH" iconv` ]  &&
+    charsetconv=' | iconv -f ${charset:-us-ascii} -t utf-8'"${textfmt}"  ||
+    charsetconv=
+
 cat >>"$TMP" <<'EOF'
 mhstore-store-text: %m%P.txt
+mhstore-store-text/calendar: %m%P.ics
 mhstore-store-text/html: %m%P.html
 mhstore-store-text/richtext: %m%P.rt
 mhstore-store-video/mpeg: %m%P.mpg
@@ -111,9 +127,11 @@ fi
 #### mhbuild-disposition-<type>[/<subtype>] entries are used by the
 #### WhatNow attach for deciding whether the Content-Disposition
 #### should be 'attachment' or 'inline'.  Only those values are
-#### supported.
+#### supported.  mhbuild-convert-text/html is defined below.
 ####
 cat <<EOF >>${TMP}
+mhbuild-convert-text/calendar: mhical -infile %F -contenttype
+mhbuild-convert-text: charset=%{charset}; iconv -f \${charset:-us-ascii} -t utf-8 %F${replfmt}
 mhbuild-disposition-text/calendar: inline
 mhbuild-disposition-message/rfc822: inline
 EOF
@@ -163,6 +181,9 @@ else
     fi
 fi
 
+echo "mhshow-show-text/calendar: mhical -infile %F" >> $TMP
+echo "mhfixmsg-format-text/calendar: mhical %F" >> $TMP
+
 PGM=`$SEARCHPROG "$SEARCHPATH" ivs_replay`
 if [ ! -z "$PGM" ]; then
        echo "mhshow-show-application/x-ivs: %l$PGM -o %F" >> $TMP
@@ -267,6 +288,9 @@ if [ ! -z "$PGM" ]; then
 %l$PGM"' -dump ${charset:+-I "$charset"} -T text/html %F' >> $TMP
     echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-dump ${charset:+-I "$charset"} -O utf-8 -T text/html %F' >> $TMP
+    echo 'mhbuild-convert-text/html: charset=%{charset}; '"\
+$PGM "'-dump ${charset:+-I "$charset"} -O utf-8 -T text/html %F '"\
+${replfmt}" >> $TMP
 else
     PGM=`$SEARCHPROG "$SEARCHPATH" lynx`
     if [ ! -z "$PGM" ]; then
@@ -276,6 +300,9 @@ else
         echo 'mhfixmsg-format-text/html: charset=%{charset}; '"\
 $PGM "'-child -dump -force_html ${charset:+--assume_charset "$charset"} %F | '"\
 expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
+        echo 'mhbuild-convert-text/html: charset=%{charset}; '"\
+$PGM "'-child -dump -force_html ${charset:+--assume_charset "$charset"} '"\
+%F${replfmt}" >> $TMP
     else
         PGM=`$SEARCHPROG "$SEARCHPATH" elinks`
         if [ ! -z "$PGM" ]; then
@@ -283,6 +310,10 @@ expand | sed -e 's/^   //' -e 's/  *$//'" >> $TMP
 -eval 'set document.browse.margin_width = 0' %F" >> $TMP
             echo "mhfixmsg-format-text/html: $PGM -dump -force-html \
 -no-numbering -eval 'set document.browse.margin_width = 0' %F" >> $TMP
+            echo "mhbuild-convert-text/html: $PGM -dump -force-html \
+-no-numbering -eval 'set document.browse.margin_width = 0' %F${replfmt}" >> $TMP
+        else
+            echo 'mhbuild-convert-text/html: cat %F' >> $TMP
         fi
     fi
 fi
diff --git a/h/icalendar.h b/h/icalendar.h
new file mode 100644 (file)
index 0000000..a1a0122
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * icalendar.h -- data structures and common code for icalendar scanner,
+ *                parser, and application code
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+/*
+ * Types used in struct contentline below.
+ */
+typedef struct value_list {
+    char *value;
+    struct value_list *next;
+} value_list;
+
+typedef struct param_list {
+    char *param_name;          /* Name of property parameter. */
+    struct value_list *values; /* List of its values. */
+    struct param_list *next;   /* Next node in list of property parameters. */
+} param_list;
+
+typedef enum cr_indicator {
+    CR_UNSET = 0,
+    LF_ONLY,
+    CR_BEFORE_LF
+} cr_indicator;
+
+/*
+ * Each (unfolded) line in the .ics file is represented by a struct
+ * contentline.
+ */
+typedef struct contentline {
+    char *name;                /* The name of the property, calprops,
+                                  or BEGIN. */
+    struct param_list *params; /* List parameters. */
+    char *value;               /* Everything after the ':'. */
+
+    char *input_line;          /* The (unfolded) input line. */
+    size_t input_line_len;     /* Amount of text stored in input_line. */
+    size_t input_line_size;    /* Size of allocated input_line. */
+
+    cr_indicator cr_before_lf; /* To support CR before LF.  If the first
+                                  line of the input has a CR before its LF,
+                                  assume that all output lines need to.
+                                  Only used in root node. */
+
+    struct contentline *next;  /* Next node in list of content lines. */
+    struct contentline *last;  /* Last node of list.  Only used in root node. */
+} contentline;
+
+
+/*
+ * List of vevents, each of which is a list of contentlines.
+ */
+typedef struct vevent {
+    contentline *contentlines;
+    struct vevent *next;
+    /* The following is only used in the root node. */
+    struct vevent *last;
+} vevent;
+
+
+/*
+ * Exported functions.
+ */
+void remove_contentline (contentline *);
+contentline *add_contentline (contentline *, const char *);
+void add_param_name (contentline *, char *);
+void add_param_value (contentline *, char *);
+void remove_value (value_list *);
+struct contentline *find_contentline (contentline *, const char *,
+                                      const char *);
+void free_contentlines (contentline *);
+
+typedef struct tzdesc *tzdesc_t;
+tzdesc_t load_timezones (const contentline *);
+void free_timezones (tzdesc_t);
+char *format_datetime (tzdesc_t, const contentline *);
+
+/*
+ * The following provide access to, and by, the iCalendar parser.
+ */
+
+/* This YYSTYPE definition prevents problems with Solaris yacc, which
+   has declarations of two variables of type YYSTYPE * in one
+   statement. */
+typedef char *charptr;
+#define YYSTYPE charptr
+
+extern int icaldebug;
+int icalparse (void);
+extern vevent vevents;
+int icallex (void);
+
+/* And this is for the icalendar scanner. */
+extern YYSTYPE icallval;
+
+/*
+ * For directing the scanner to use a files other than stdin/stdout.
+ * These don't use the accessors provided by modern flex because
+ * flex 2.5.4 doesn't supply them.
+ */
+void icalset_inputfile (FILE *);
+void icalset_outputfile (FILE *);
index 9fcaf5d3ba956351675dbff315353e1b78da1fa6..9cac94e5ad3ba80f888322936c9860cd9bfa2f82 100644 (file)
--- a/h/mime.h
+++ b/h/mime.h
 #define        DESCR_FIELD     "Content-Description"
 #define        DISPO_FIELD     "Content-Disposition"
 #define        MD5_FIELD       "Content-MD5"
-#define PSEUDOHEADER_PREFIX            "Nmh-"
-#define        ATTACH_FIELD                    PSEUDOHEADER_PREFIX "Attach"
-#define        ATTACH_FIELD_ALT                "Attach"
+#define        PSEUDOHEADER_PREFIX        "Nmh-"
+#define        ATTACH_FIELD               PSEUDOHEADER_PREFIX "Attach"
+#define        ATTACH_FIELD_ALT           "Attach"
+#define        MHBUILD_FILE_PSEUDOHEADER  PSEUDOHEADER_PREFIX "mhbuild-file-"
+#define        MHBUILD_ARGS_PSEUDOHEADER  PSEUDOHEADER_PREFIX "mhbuild-args-"
 
 #define        isatom(c)   (isascii((unsigned char) c) \
                      && !isspace ((unsigned char) c) \
index 51f0607c38a82abf39bd18ac672b8ee61ac49ed6..7a4bc8134b899d1397bc253a779882fbd9f26d86 100644 (file)
@@ -1,4 +1,4 @@
-.TH MH\-MIME %manext7% "March 16, 2014" "%nmhversion%"
+.TH MH\-MIME %manext7% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
@@ -141,8 +141,11 @@ command at the \*(lqWhat now?\*(rq prompt to process them.
 When replying to messages using
 .IR repl (1)
 the traditional MH method of including the original text in the reply does
-not interoperate with MIME messages.  As of this writing there is no
-native solution for addressing this issue, but the contrib directory
+not interoperate with MIME messages.  The
+.B \-convertargs
+switch to
+.IR repl (1)
+provides one solution.  Another solution:  the contrib directory
 .RI ( %docdir%/contrib )
 contains a Perl program called
 .B replyfilter
index 4b261f33def3d05d61551ca2901f9d8d7ac6d1b4..becf6422cb527890479038f2b028449244b362bf 100644 (file)
@@ -1,4 +1,4 @@
-.TH MHBUILD %manext1% "March 13, 2014" "%nmhversion%"
+.TH MHBUILD %manext1% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
@@ -19,7 +19,7 @@ mhbuild \- translate MIME composition draft
 .RB [ \-verbose " | " \-noverbose ]
 .RB [ \-disposition " | " \-nodisposition ]
 .RB [ \-check " | " \-nocheck ]
-.RB [ \-headerencoding 
+.RB [ \-headerencoding
 .IR encoding\-algorithm
 .RB " | " \-autoheaderencoding ]
 .RB [ \-maxunencoded
@@ -119,6 +119,158 @@ to supply the disposition value.  The only supported values are
 .I attachment
 and
 .IR inline.
+.SS "Convert Interface"
+.nr item 1 1
+The \*(lqconvert\*(rq interface is a powerful mechanism that supports
+replying to MIME messages.  These placeholders are used in the following
+description:
+.IP TYPE
+content type/subtype
+.IP CONVERTER
+external program, and any fixed arguments, to convert content, such as
+from a request to a reply
+.IP ARGSTRING
+arguments to pass from
+.B repl
+to
+.I CONVERTER
+.IP FILE
+full path of message being replied to
+.PP
+.RE
+The convert support is based on pseudoheaders of the form
+.PP
+.RS 5
+  Nmh-mhbuild-file-TYPE: FILE
+  Nmh-mhbuild-args-TYPE: ARGSTRING
+.RE
+.PP
+in the draft.  For each such pseudoheader, mhbuild looks in the
+profile and mhn.defaults for this corresponding TYPE entry to find the
+converter that supports it:
+.PP
+.RS 5
+.RI mhbuild-convert- TYPE :
+.I CONVERTER
+.RE
+.PP
+It's a fatal error if no such entry is found for TYPE.  An empty
+entry, e.g.,
+.PP
+.RS 5
+mhbuild-convert-text/html:
+.RE
+.PP
+excludes parts of that TYPE from the draft.  The mhn.defaults file
+contains default
+.B mhbuild-convert-text/html
+and
+.BR mhbuild-convert-text/plain
+entries.  Profile entries can be used to override corresponding
+mhn.defaults entries, as usual.
+.PP
+For each
+.I TYPE
+part in
+.IR FILE ,
+.B mhbuild
+runs
+.I CONVERTER ARGSTRING
+on the content of the part.
+.PP
+Each part in
+.I FILE
+that has no corresponding TYPE entry in the profile or mhn.defaults is
+excluded from the draft; the user can include them using mhbuild
+directives as usual.
+.PP
+.B repl
+inserts Nmh-mhbuild-text/html: and Nmh-mhbuild-text/plain:
+pseudoheaders in every draft.  The user can prevent insertion of
+content parts of either of those types by putting corresponding empty
+entries in their profile.
+.PP
+Only the highest precedence alternative with a supported
+.I TYPE
+of a multipart/alternative part is used.
+.PP
+mhn.defaults.sh selects the text/html-to-text/plain converter at
+install time.  It includes
+.BR iconv "(1),"
+and
+.BR par (1)
+or
+.BR fmt "(1),"
+in the pipeline only if found.
+.PP
+Some content types require the addition of parameters to the
+Content-Type header, such as
+.I method=REPLY
+for text/calendar.  mhbuild looks for a Content-Type header, followed
+by a blank line, at the beginning of the converter output.  If one is
+found, it is used for the corresponding part in the reply draft.
+.PP
+The \*(lqconvert\*(rq interface doesn't support different
+.IR ARGSTRING s
+or different converters for different parts of the same
+.IR  TYPE .
+That would require associating parts by part number with the
+.IR ARGSTRING s
+or converters.  Instead, that can be done (currently, without using
+the convert support), with
+.B mhbuild
+directives as described below, e.g.,
+.PP
+.RS 5
+#text/html; charset=utf-8 *8bit | mhstore -noverbose -part 42.7 -outfile - | w3m -dump -cols 64 -T text/html -O utf-8
+.RE
+.PP
+The only way to mix
+.B convert
+pseudoheaders and mhbuild directives is to insert the directives before
+.B mhbuild
+is run, which is typically done by entering
+.I mime
+at the \*(lqWhat now?\*(rq prompt, or with an
+.B \-editor mhbuild
+switch.
+.PP
+These (optional) setup steps can make the convert support
+easier to use:
+.IP \n[item]. 3
+If the
+.BR par (1)
+program is installed on your system, it will be set by default
+(in mhn.defaults) to filter the converter output.  It helps to
+set the
+.B $PARINIT
+environment variable, as described in its man page.
+.IP \n+[item]. 3
+Add this line to your profile:
+.PP
+.RS 5
+.nf
+mhbuild-next: $EDITOR
+.fi
+.RE
+.PP
+.RS 3
+assuming that your EDTIOR environment variable is set; if not, replace
+$EDITOR with the name of your editor.  Without that profile entry, a
+response of \*(lqe[dit]\*(rq at the What now? prompt will require
+specification of your editor if an
+.B \-editor mhbuild
+switch is used.
+.RE
+.IP \n+[item]. 3
+If using
+.BR repl ,
+source the Bourne-shell compatible functions in
+%docdir%/contrib/replaliases.  That script also sets the
+.B $PARINIT
+environment variable if it was not set.
+.RE
+.PP
 .SS "Translating the Composition File"
 .B mhbuild
 is essentially a filter to aid in the composition of MIME
@@ -180,7 +332,7 @@ errors:
 .nf
 #off
 #include <stdio.h>
-printf("Hello, World!);
+printf("Hello, World!");
 #pop
 .fi
 .RE
@@ -522,7 +674,7 @@ The
 switch will indicate which algorithm to use when encoding any message headers
 that contain 8\-bit characters.  The valid arguments are
 .I base64
-for based\-64 encoding and 
+for based\-64 encoding and
 .I quoted
 for quoted\-printable encoding.  The
 .B \-autoheaderencoding
@@ -610,7 +762,7 @@ to translate the composition file into MIME format.
 .PP
 Normally it is an error to invoke
 .B mhbuild
-on file that already in MIME format.  The 
+on file that already in MIME format.  The
 .B \-auto
 switch will cause
 .B mhbuild
@@ -770,7 +922,10 @@ is checked.
 .SH "SEE ALSO"
 .IR mhlist (1),
 .IR mhshow (1),
-.IR mhstore (1)
+.IR mhstore (1),
+.IR fmt (1),
+.IR iconv (1),
+.IR par (1)
 .PP
 .I "Proposed Standard for Message Encapsulation"
 (RFC 934),
diff --git a/man/mhical.man b/man/mhical.man
new file mode 100644 (file)
index 0000000..8ee1c52
--- /dev/null
@@ -0,0 +1,171 @@
+.TH MHICAL %manext1% "January 4, 2015" "%nmhversion%"
+.\"
+.\" %nmhwarning%
+.\"
+.SH NAME
+mhical \- manipulates an iCalendar event request
+.SH SYNOPSIS
+.HP 5
+.na
+.B mhical
+.RB [ \-form
+.IR formatfile ]
+.RB [ \-format
+.IR formatstring ]
+.RB [[ \-reply
+.IR "accept" " | " "decline" " | " "tentative" "] |"
+.BR \-cancel ]
+.RB [ \-contenttype ]
+.RB [ \-infile
+.IR infile ]
+.RB [ \-outfile
+.IR outfile ]
+.RB [ \-unfold ]
+.RB [ \-debug ]
+.RB [ \-version ]
+.RB [ \-help ]
+.ad
+.SH DESCRIPTION
+.B mhical
+manipulates an iCalendar (.ics) event request, to display it, generate
+a reply to it, or cancel it.  iCalendar event requests and replies are
+defined by RFC 5545.
+.PP
+.SH DISPLAY
+The default operation is to display the iCalendar event request in a
+human-readable format.
+.SS "Format Program Selection"
+For the display operation, the
+.B \-format
+.I string
+and
+.B \-form
+.I formatfile
+switches may be used to specify a format string or a format file to read.
+If given a format string, it must be specified as a single argument to
+the
+.B \-format
+switch.  If given a format file name with
+.BR \-form ,
+the file is searched for using the normal
+.B nmh
+rules: absolute pathnames are accessed directly, tilde expansion is
+done on usernames, and files are searched for in the user's
+.I Mail
+directory as specified in their profile.  If not found there, the directory
+.RI \*(lq %nmhetcdir% \*(rq
+is checked.
+.B mhical
+defaults to using a format file named
+.BR mhical.24hour ,
+and will use the one installed in the
+.RI \*(lq %nmhetcdir% \*(rq
+directory if not found elsewhere.
+.PP
+The following format components are supported:
+.PP
+.RS 5
+.fc ^ ~
+.nf
+.ta \w'description 'u
+.BR ^method~^
+.BR ^organizer~^
+.BR ^summary~^
+.BR ^description~^
+.BR ^location~^
+.BR ^dtstart~^
+.BR ^dtend~^
+.BR ^attendees~^
+.fi
+.RE
+.PP
+Those format names are case-sensitive.  Each corresponds to the
+iCalendar property of the same name as defined in RFC 5545, with the
+exception of
+.BR attendees .
+That is a comma-delimited list of the common name (CN), if available,
+or email address of Attendee properties.  A maximum of 20 will be
+displayed.
+.SS Timezone
+.B mhical
+will display the event with times converted to the timezone specified
+by the
+.B TZ
+environment variable, if it is set, see tzset(3).  If not set, its
+behavior is implementation defined, but may use the user's local
+timezone.
+.SH REPLY
+The
+.B \-reply
+switch generates a reply from the event request.  The required
+action parameter must be one of
+.IR "accept" ,
+.IR "decline" ", or"
+.IR "tentative" .
+Delegation is not supported.
+.SH CANCEL
+The
+.B \-cancel
+switch generates an iCalendar event that can be used to cancel
+the event request.
+.SH INPUT/OUTPUT
+By default,
+.B mhical
+reads from standard input and writes to standard output.  The
+.B \-infile
+and
+.BR \-outfile ,
+respectively, switches can be used to override these defaults.
+.PP
+.SH MISCELLANEOUS SWITCHES
+The
+.B \-contenttype
+switch instructs
+.B mhical
+to insert a Content-Type header at the beginning of its output,
+for use by
+.BR mhbuild .
+It can only be used with
+.B \-reply
+and
+.BR \-cancel .
+.PP
+The
+.B \-unfold
+switch echoes the event request, but with all lines unfolded.
+.PP
+The
+.B \-debug
+switch reveals minute details of the parse process.
+.SH FILES
+.B mhical
+looks for format files in multiple locations:  absolute pathnames are
+accessed directly, tilde expansion is done on usernames, and files are
+searched for in the user's
+.I Mail
+directory as specified in their profile.  If not found there, the directory
+.RI \*(lq %nmhetcdir% \*(rq
+is checked.
+.PP
+.fc ^ ~
+.nf
+.ta \w'%nmhetcdir%/mhical.24hour  'u
+^%nmhetcdir%/mhical.24hour~^The default display template
+^%nmhetcdir%/mhical.12hour~^Display template that uses 12-hour clock
+.fi
+.fi
+.SH "SEE ALSO"
+.IR mhbuild (1),
+.IR mh\-format (5),
+.IR tzset (3),
+.IR repl (1)
+.SH DEFAULTS
+.nf
+.RB ` \-form "' defaults to mhical.24hour"
+.RB ` \-infile "' defaults to standard input"
+.RB ` \-outfile "' defaults to standard output"
+.fi
+.SH BUGS
+.B mhical
+supports only a very limited subset of RRULE formats.  Specifically,
+only a frequency of YEARLY and an interval of 1 are supported.
index e77522263853f2d74fcbbd261e6f6376f4e12298..da8f015cb9957758ed7e17e20df1be4965ab421e 100644 (file)
@@ -1,4 +1,4 @@
-.TH REPL %manext1% "December 13, 2014" "%nmhversion%"
+.TH REPL %manext1% "December 14, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
@@ -36,6 +36,8 @@ all/to/cc/me]
 .RB [ \-editor
 .IR editor ]
 .RB [ \-noedit ]
+.RB [ \-convertargs
+.IR "type argstring" ]
 .RB [ \-whatnowproc
 .IR program ]
 .RB [ \-nowhatnowproc ]
@@ -234,6 +236,25 @@ and
 .B \-noatfile
 options.
 .PP
+The
+.B \-convertargs
+switch directs
+.B repl
+to pass the arguments for
+.I type
+to
+.BR mhbuild .
+Both arguments are required;
+.I type
+must be non-empty while
+.I argstring
+can be empty, e.g., '' in a shell command line.  The
+.B \-convertargs
+switch can be used multiple times.
+.RI %docdir%/contrib/replaliases
+shows the use of
+.BR "repl \-convertargs" .
+.PP
 Although
 .B repl
 uses a forms file to direct it how to construct
@@ -516,6 +537,8 @@ is checked.
 .IR send (1),
 .IR whatnow (1),
 .IR mh\-format (5)
+.PP
+.I %docdir%/contrib/replaliases
 .SH DEFAULTS
 .nf
 .RB ` +folder "' defaults to the current folder"
diff --git a/sbr/datetime.c b/sbr/datetime.c
new file mode 100644 (file)
index 0000000..8b25136
--- /dev/null
@@ -0,0 +1,467 @@
+/*
+ * datetime.c -- functions for manipulating RFC 5545 date-time values
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include <h/fmt_scan.h>
+#include "h/tws.h"
+#include "h/utils.h"
+
+/*
+ * This doesn't try to support all of the myriad date-time formats
+ * allowed by RFC 5545.  It is only used for viewing date-times,
+ * so that shouldn't be a problem:  if a particular format can't
+ * be handled by this code, just present it to the user in its
+ * original form.
+ *
+ * And, this assumes a valid iCalendar input file.  E.g, it
+ * doesn't check that each BEGIN has a matching END and vice
+ * versa.  That should be done in the parser, though it currently
+ * isn't.
+ */
+
+typedef struct tzparams {
+    /* Pointers to values in parse tree.
+     * TZOFFSETFROM is used to calculate the absolute time at which
+     * the transition to a given observance takes place.
+     * TZOFFSETTO is the timezone offset from UTC.  Both are in HHmm
+     * format. */
+    char *offsetfrom, *offsetto;
+    const char *dtstart;
+    const char *rrule;
+
+    /* This is only used to make sure that timezone applies.  And not
+       always, because if the timezone DTSTART is before the epoch, we
+       don't try to compare to it. */
+    time_t start_dt; /* in seconds since epoch */
+} tzparams;
+
+struct tzdesc {
+    char *tzid;
+
+    /* The following are translations of the pieces of RRULE and DTSTART
+       into seconds from beginning of year. */
+    tzparams standard_params;
+    tzparams daylight_params;
+
+    struct tzdesc *next;
+};
+
+/*
+ * Parse a datetime of the form YYYYMMDDThhmmss and a string
+ * representation of the timezone in units of [+-]hhmm and load the
+ * struct tws.
+ */
+static int
+parse_datetime (const char *datetime, const char *zone, int dst,
+                struct tws *tws) {
+    char utc_indicator;
+    int form_1 = 0;
+    int items_matched =
+        sscanf (datetime, "%4d%2d%2dT%2d%2d%2d%c",
+                &tws->tw_year, &tws->tw_mon, &tws->tw_mday,
+                &tws->tw_hour, &tws->tw_min, &tws->tw_sec,
+                &utc_indicator);
+    tws->tw_flags = TW_NULL;
+
+    if (items_matched == 7) {
+        /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
+        if (utc_indicator != 'Z') {
+            advise (NULL, "%s has invalid timezone indicator of 0x%x",
+                    datetime, utc_indicator);
+            return NOTOK;
+        }
+    } else if (zone == NULL) {
+        form_1 = 1;
+    }
+
+    if (items_matched >= 6) {
+        int offset = atoi (zone ? zone : "0");
+
+        /* struct tws defines tw_mon over [0, 11]. */
+        --tws->tw_mon;
+
+        set_dotw (tws);
+        /* set_dotw() sets TW_SIMP.  Replace that with TW_SEXP so that
+           dasctime() outputs the dotw before the date instead of after. */
+        tws->tw_flags &= ~TW_SDAY, tws->tw_flags |= TW_SEXP;
+
+        /* For the call to dmktime():
+           - don't need tw_yday
+           - tw_clock must be 0 on entry, and is set by dmktime()
+           - the only flag in tw_flags used is TW_DST
+         */
+        tws->tw_yday = tws->tw_clock = 0;
+        tws->tw_zone = 60 * (offset / 100)  +  offset % 100;
+        if (dst) {
+            tws->tw_zone -= 60;  /* per dlocaltime() */
+            tws->tw_flags |= TW_DST;
+        }
+        /* dmktime() just sets tws->tw_clock. */
+        (void) dmktime (tws);
+
+        if (! form_1) {
+            /* Set TW_SZEXP so that dasctime outputs timezone, except
+               with local time (Form #1). */
+            tws->tw_flags |= TW_SZEXP;
+
+            /* Convert UTC time to time in local timezone.  However,
+               don't try for years before 1970 because dlocatime()
+               doesn't handle them well.  dlocaltime() will succeed if
+               tws->tw_clock is nonzero. */
+            if (tws->tw_year >= 1970  &&  tws->tw_clock > 0) {
+                const int was_dst = tws->tw_flags & TW_DST;
+
+                *tws = *dlocaltime (&tws->tw_clock);
+                if (was_dst  &&  ! (tws->tw_flags & TW_DST)) {
+                    /* dlocaltime() changed the DST flag from 1 to 0,
+                       which means the time is in the hour (assumed to
+                       be one hour) that is lost in the transition to
+                       DST.  So per RFC 5545 Sec. 3.3.5, "the
+                       DATE-TIME value is interpreted using the UTC
+                       offset before the gap in local times."  In
+                       other words, add an hour to it.
+                       No adjustment is necessary for the transition
+                       from DST to standard time, because dasctime()
+                       shows the first occurrence of the time. */
+                    tws->tw_clock += 3600;
+                    *tws = *dlocaltime (&tws->tw_clock);
+                }
+            }
+        }
+
+        return OK;
+    } else {
+        return NOTOK;
+    }
+}
+
+tzdesc_t
+load_timezones (const contentline *clines) {
+    tzdesc_t timezones = NULL, timezone = NULL;
+    int in_vtimezone, in_standard, in_daylight;
+    tzparams *params = NULL;
+    const contentline *node;
+
+    /* Interpret each VTIMEZONE section. */
+    in_vtimezone = in_standard = in_daylight = 0;
+    for (node = clines; node; node = node->next) {
+        /* node->name will be NULL if the line was "deleted". */
+        if (! node->name) { continue; }
+
+        if (in_daylight  ||  in_standard) {
+            if (! strcasecmp ("END", node->name)  &&
+                ((in_standard  &&  ! strcasecmp ("STANDARD", node->value))  ||
+                 (in_daylight  &&  ! strcasecmp ("DAYLIGHT", node->value)))) {
+                struct tws tws;
+
+                if (in_standard) { in_standard = 0; }
+                else if (in_daylight) { in_daylight = 0; }
+                if (parse_datetime (params->dtstart, params->offsetfrom,
+                                    in_daylight ? 1 : 0,
+                                    &tws) == OK) {
+                    if (tws.tw_year >= 1970) {
+                        /* dmktime() falls apart for, e.g., the year 1601. */
+                        params->start_dt = tws.tw_clock;
+                    }
+                } else {
+                    advise (NULL, "failed to parse start time %s for %s",
+                            params->dtstart,
+                            in_standard ? "standard" : "daylight");
+                    return NULL;
+                }
+                params = NULL;
+            } else if (! strcasecmp ("DTSTART", node->name)) {
+                /* Save DTSTART for use after getting TZOFFSETFROM. */
+                params->dtstart = node->value;
+            } else if (! strcasecmp ("TZOFFSETFROM", node->name)) {
+                params->offsetfrom = node->value;
+            } else if (! strcasecmp ("TZOFFSETTO", node->name)) {
+                params->offsetto = node->value;
+            } else if (! strcasecmp ("RRULE", node->name)) {
+                params->rrule = node->value;
+            }
+        } else if (in_vtimezone) {
+            if (! strcasecmp ("END", node->name)  &&
+                ! strcasecmp ("VTIMEZONE", node->value)) {
+                in_vtimezone = 0;
+            } else if (! strcasecmp ("BEGIN", node->name)  &&
+                ! strcasecmp ("STANDARD", node->value)) {
+                in_standard = 1;
+                params = &timezone->standard_params;
+            } else if (! strcasecmp ("BEGIN", node->name)  &&
+                ! strcasecmp ("DAYLIGHT", node->value)) {
+                in_daylight = 1;
+                params = &timezone->daylight_params;
+            } else if (! strcasecmp ("TZID", node->name)) {
+                timezone->tzid = strdup (node->value);
+            }
+        } else {
+            if (! strcasecmp ("BEGIN", node->name)  &&
+                ! strcasecmp ("VTIMEZONE", node->value)) {
+
+                in_vtimezone = 1;
+                timezone = mh_xcalloc (1, sizeof (struct tzdesc));
+                if (timezones) {
+                    tzdesc_t t;
+
+                    for (t = timezones; t && t->next; t = t->next) { continue; }
+                    /* The loop terminated at, not after, the last
+                       timezones node. */
+                    t->next = timezone;
+                } else {
+                    timezones = timezone;
+                }
+            }
+        }
+    }
+
+    return timezones;
+}
+
+void
+free_timezones (tzdesc_t timezone) {
+    tzdesc_t next;
+
+    for ( ; timezone; timezone = next) {
+        free (timezone->tzid);
+        next = timezone->next;
+        free (timezone);
+    }
+}
+
+/*
+ * Convert time to local timezone, accounting for daylight saving time:
+ * - Detect which type of datetime the node contains:
+ *     Form #1: DATE WITH LOCAL TIME
+ *     Form #2: DATE WITH UTC TIME
+ *     Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
+ * - Convert value to local time in seconds since epoch.
+ * - If there's a DST in the timezone, convert its start and end
+ *   date-times to local time in seconds, also.  Then determine
+ *   if the value is between them, and therefore DST.  Otherwise, it's
+ *   not.
+ * - Format the time value.
+ */
+
+/*
+ * Given a recurrence rule and year, calculate its time in seconds
+ * from 01 January UTC of the year.
+ */
+time_t
+rrule_clock (const char *rrule, const char *starttime, const char *zone,
+             unsigned int year) {
+    time_t clock = 0;
+
+    if (nmh_strcasestr (rrule, "FREQ=YEARLY;INTERVAL=1")) {
+        struct tws *tws;
+        const char *cp;
+        int wday = -1, month = -1;
+        int specific_day = 1; /* BYDAY integer (prefix) */
+        char buf[32];
+        int day;
+
+        if ((cp = nmh_strcasestr (rrule, "BYDAY="))) {
+            cp += 6;
+            /* BYDAY integers must be ASCII. */
+            if (*cp == '+') { ++cp; } /* +n specific day; don't support '-' */
+            else if (*cp == '-') { goto fail; }
+
+            if (isdigit ((unsigned char) *cp)) { specific_day = *cp++ - 0x30; }
+
+            if (! strncasecmp (cp, "SU", 2)) { wday = 0; }
+            else if (! strncasecmp (cp, "MO", 2)) { wday = 1; }
+            else if (! strncasecmp (cp, "TU", 2)) { wday = 2; }
+            else if (! strncasecmp (cp, "WE", 2)) { wday = 3; }
+            else if (! strncasecmp (cp, "TH", 2)) { wday = 4; }
+            else if (! strncasecmp (cp, "FR", 2)) { wday = 5; }
+            else if (! strncasecmp (cp, "SA", 2)) { wday = 6; }
+        }
+        if ((cp = nmh_strcasestr (rrule, "BYMONTH="))) {
+            month = atoi (cp + 8);
+        }
+
+        for (day = 1; day <= 7; ++day) {
+            /* E.g, 11-01-2014 02:00:00-0400 */
+            snprintf (buf, sizeof buf, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
+                      month, day + 7 * (specific_day-1), year,
+                      starttime, starttime + 2, starttime + 4,
+                      zone ? zone : "0000");
+            if ((tws = dparsetime (buf))) {
+                if (! (tws->tw_flags & (TW_SEXP|TW_SIMP))) { set_dotw (tws); }
+
+                if (tws->tw_wday == wday) {
+                    /* Found the day specified in the RRULE. */
+                    break;
+                }
+            }
+        }
+
+        if (day <= 7) {
+            clock = tws->tw_clock;
+        }
+    }
+
+fail:
+    if (clock == 0) {
+        admonish (NULL,
+                  "Unsupported RRULE format: %s, assume local timezone",
+                  rrule);
+    }
+
+    return clock;
+}
+
+char *
+format_datetime (tzdesc_t timezones, const contentline *node) {
+    param_list *p;
+    char *dt_timezone = NULL;
+    int dst = 0;
+    struct tws tws[2]; /* [standard, daylight] */
+    tzdesc_t tz;
+    char *tp_std, *tp_dst, *tp_dt;
+
+    /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
+    for (p = node->params; p && p->param_name; p = p->next) {
+        if (! strcasecmp (p->param_name, "TZID")  &&  p->values) {
+            dt_timezone = p->values->value;
+            break;
+        }
+    }
+
+    if (! dt_timezone) {
+        /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
+           Form #2: DATE WITH UTC TIME */
+        if (parse_datetime (node->value, NULL, 0, &tws[0]) == OK) {
+            return strdup (dasctime (&tws[0], 0));
+        } else {
+            advise (NULL, "unable to parse datetime %s", node->value);
+            return NULL;
+        }
+    }
+
+    /*
+     * must be
+     * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
+     */
+
+    /* Find the corresponding tzdesc. */
+    for (tz = timezones; dt_timezone && tz; tz = tz->next) {
+        /* Property parameter values are case insenstive (RFC 5545
+           Sec. 2) and time zone identifiers are property parameters
+           (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
+           different case in the same file for identifiers that are
+           supposed to be the same. */
+        if (tz->tzid  &&  ! strcasecmp (dt_timezone, tz->tzid)) { break; }
+    }
+
+    if (! tz) {
+        advise (NULL, "did not find VTIMEZONE section for %s", dt_timezone);
+        return NULL;
+    }
+
+    /* Determine if it's Daylight Saving. */
+    tp_std = strchr (tz->standard_params.dtstart, 'T');
+    tp_dt = strchr (node->value, 'T');
+
+    if (tz->daylight_params.dtstart) {
+        tp_dst = strchr (tz->daylight_params.dtstart, 'T');
+    } else {
+        /* No DAYLIGHT section. */
+        tp_dst = NULL;
+        dst = 0;
+    }
+
+    if (tp_std  &&  tp_dt) {
+        time_t transition[2] = { 0, 0 }; /* [standard, daylight] */
+        time_t dt[2]; /* [standard, daylight] */
+        unsigned int year;
+        char buf[5];
+
+        /* Datetime is form YYYYMMDDThhmmss.  Extract year. */
+        memcpy (buf, node->value, sizeof buf - 1);
+        buf[sizeof buf - 1] = '\0';
+        year = atoi (buf);
+
+        if (tz->standard_params.rrule) {
+            /* +1 to skip the T before the time */
+            transition[0] =
+                rrule_clock (tz->standard_params.rrule, tp_std + 1,
+                             tz->standard_params.offsetfrom, year);
+        }
+        if (tp_dst  &&  tz->daylight_params.rrule) {
+            /* +1 to skip the T before the time */
+            transition[1] =
+                rrule_clock (tz->daylight_params.rrule, tp_dst + 1,
+                             tz->daylight_params.offsetfrom, year);
+        }
+
+        if (transition[0] < transition[1]) {
+            advise (NULL, "format_datetime() requires that daylight "
+                    "saving time transition precede standard time "
+                    "transition");
+            return NULL;
+        }
+
+        if (parse_datetime (node->value, tz->standard_params.offsetto,
+                            0, &tws[0]) == OK) {
+            dt[0] = tws[0].tw_clock;
+        } else {
+            advise (NULL, "unable to parse datetime %s", node->value);
+            return NULL;
+        }
+
+        if (tp_dst) {
+            if (dt[0] < transition[1]) {
+                dst = 0;
+            } else {
+                if (parse_datetime (node->value,
+                                    tz->daylight_params.offsetto, 1,
+                                    &tws[1]) == OK) {
+                    dt[1] = tws[1].tw_clock;
+                } else {
+                    advise (NULL, "unable to parse datetime %s",
+                            node->value);
+                    return NULL;
+                }
+
+                dst = dt[1] > transition[0]  ?  0  :  1;
+            }
+        }
+
+        if (dst) {
+            if (tz->daylight_params.start_dt > 0  &&
+                dt[dst] < tz->daylight_params.start_dt) {
+                advise (NULL, "date-time of %s is before VTIMEZONE start "
+                        "of %s", node->value,
+                        tz->daylight_params.dtstart);
+                return NULL;
+            }
+        } else {
+            if (tz->standard_params.start_dt > 0  &&
+                dt[dst] < tz->standard_params.start_dt) {
+                advise (NULL, "date-time of %s is before VTIMEZONE start "
+                        "of %s", node->value,
+                        tz->standard_params.dtstart);
+                return NULL;
+            }
+        }
+    } else {
+        if (! tp_std) {
+            advise (NULL, "unsupported date-time format: %s",
+                    tz->standard_params.dtstart);
+            return NULL;
+        }
+        if (! tp_dt) {
+            advise (NULL, "unsupported date-time format: %s", node->value);
+            return NULL;
+        }
+    }
+
+    return strdup (dasctime (&tws[dst], 0));
+}
diff --git a/sbr/icalendar.l b/sbr/icalendar.l
new file mode 100644 (file)
index 0000000..016c6eb
--- /dev/null
@@ -0,0 +1,343 @@
+/*
+ * icalendar.l -- icalendar (RFC 5545) scanner
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+/* See porting notes at end of this file. */
+
+%{
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "sbr/icalparse.h"
+
+static char *unfold (char *, size_t *);
+static void destroy_icallex ();
+%}
+
+/*
+ * These flex options aren't used:
+ *   8bit not needed
+ *   case-insensitive not needed
+ *   align not used because this isn't performance critical
+ */
+%option outfile="lex.yy.c" prefix="ical"
+%option perf-report warn
+%option never-interactive noinput noyywrap
+
+              /*
+               * From RFC 5545 § 3.1.
+               */
+name          {iana-token}|{x-name}
+iana-token    ({ALPHA}|{DIGIT}|-)+
+x-name        X-({vendorid}-)?({ALPHA}|{DIGIT}|-)+
+vendorid      ({ALPHA}|{DIGIT}){3,}
+param-name    {iana-token}|{x-name}
+param-value   {paramtext}|{quoted-string}
+paramtext     {SAFE-CHAR}*
+value         {VALUE-CHAR}*
+quoted-string {DQUOTE}{QSAFE-CHAR}*{DQUOTE}
+QSAFE-CHAR    {WSP}|[\x21\x23-\x7E]|{NON-US-ASCII}
+SAFE-CHAR     {WSP}|[\x21\x23-\x2B\x2D-\x39\x3C-\x7E]|{NON-US-ASCII}
+VALUE-CHAR    {WSP}|[\x21-\x7E]|{NON-US-ASCII}
+              /* The following is a short-cut definition that admits more
+                 that the UNICODE characters permitted by RFC 5545. */
+NON-US-ASCII  [\x80-\xF8]{2,4}
+              /* The following excludes HTAB, unlike {CTL}. */
+CONTROL       [\x00-\x08\x0A-\x1F\x7F]
+EQUAL         =
+              /* Solaris lex requires that the , be escaped. */
+COMMA         \,
+              /*
+               * From RFC 5545 § 2.1.
+               */
+COLON         :
+SEMICOLON     ;
+
+              /*
+               * From RFC 5545 § 3.3.11.
+               */
+text          ({TSAFE-CHAR}|:|{DQUOTE}|{ESCAPED-CHAR})*
+ESCAPED-CHAR  \\\\|\\;|\\,|\\N|\\n
+TSAFE-CHAR    {WSP}|[\x21\x23-\x2B\x2D-\x39\x3C-\x5B\x5D-\x7E]|{NON-US-ASCII|
+
+              /*
+               * Core rules (definitions) from RFC 5234 Appendix B.1.
+               */
+ALPHA         [\x41-\x5A\x61-\x7A]
+BIT           [01]
+CHAR          [\x01-\x7F]
+CR            \x0D
+              /* Variance from RFC 5234:  the {CR} is required in
+                 CRLF, but it is optional below to support Unix
+                 filesystem convention. */
+CRLF          ({CR}?{LF})+
+CTL           [\x00-\x1F\x7F]
+DIGIT         [\x30-\x39]
+DQUOTE        \x22
+HEXDIG        {DIGIT}|[A-F]
+HTAB          \x09
+LF            \x0A
+LWSP          ({WSP}|({CRLF}{WSP}))*
+OCTET         [\x00-\xFF]
+SP            \x20
+VCHAR         [\x21-\x7E]
+WSP           {SP}|{HTAB}
+
+/*
+ * Our definitions.
+ */
+fold                 {CRLF}{WSP}
+folded-name          {name}({fold}+{iana-token})+
+folded-param-name    {param-name}({fold}+{iana-token})+
+folded-quoted-string {DQUOTE}{QSAFE-CHAR}*{fold}+{QSAFE-CHAR}*{DQUOTE}
+folded-param-value   {paramtext}({fold}{paramtext}*)+|{folded-quoted-string}
+folded-value         {VALUE-CHAR}*({fold}{VALUE-CHAR}*)+
+
+%s s_name s_colon s_value s_semicolon s_param_name s_equal s_comma
+
+%%
+
+<INITIAL>
+{CRLF} {
+    /* Eat any leading newlines. */
+}
+
+<INITIAL>
+{folded-name} {
+    /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+       so copy it. */
+    size_t len = icalleng;
+    unfold (icaltext, &len);
+    icalleng = len;
+
+    icallval = strdup (icaltext);
+    /* yy_push_state (s_name);         * s_name */
+    BEGIN (s_name);                   /* s_name */
+    return ICAL_NAME;
+}
+
+<INITIAL>
+{name} {
+    icallval = strdup (icaltext);
+    /* yy_push_state (s_name);         * s_name */
+    BEGIN (s_name);                   /* s_name */
+    return ICAL_NAME;
+}
+
+<s_name>
+{COLON} {
+    /* Don't need to strdup a single character. */
+    icallval = icaltext;
+    /* yy_pop_state ();                * INITIAL */
+    /* yy_push_state (s_colon);        * s_colon */
+    BEGIN (s_colon);                  /* s_colon */
+    return ICAL_COLON;
+}
+
+<s_colon>
+{folded-value} {
+    /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+       so copy it. */
+    size_t len = icalleng;
+    unfold (icaltext, &len);
+    icalleng = len;
+
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * INITIAL */
+    /* yy_push_state (s_value);        * s_value */
+    BEGIN (s_value);                  /* s_value */
+    return ICAL_VALUE;
+}
+
+<s_colon>
+{value} {
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * INITIAL */
+    /* yy_push_state (s_value);        * s_value */
+    BEGIN (s_value);                  /* s_value */
+    return ICAL_VALUE;
+}
+
+<s_name>
+{SEMICOLON} {
+    /* Don't need to strdup a single character. */
+    icallval = icaltext;
+    /* yy_push_state (s_semicolon);    * s_name, s_semicolon */
+    BEGIN (s_semicolon);              /* s_name, s_semicolon */
+    return ICAL_SEMICOLON;
+}
+
+<s_semicolon>
+{folded-param-name} {
+    /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+       so copy it. */
+    size_t len = icalleng;
+    unfold (icaltext, &len);
+    icalleng = len;
+
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * s_name */
+    /* yy_push_state (s_param_name);   * s_name, s_param_name */
+    BEGIN (s_param_name);             /* s_name, s_param_name */
+    return ICAL_PARAM_NAME;
+}
+
+<s_semicolon>
+{param-name} {
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * s_name */
+    /* yy_push_state (s_param_name);   * s_name, s_param_name */
+    BEGIN (s_param_name);             /* s_name, s_param_name */
+    return ICAL_PARAM_NAME;
+}
+
+<s_param_name>
+{EQUAL} {
+    /* Don't need to strdup a single character. */
+    icallval = icaltext;
+    /* yy_pop_state ();                * s_name */
+    /* yy_push_state (s_equal);        * s_name, s_equal */
+    BEGIN (s_equal);                  /* s_name, s_equal */
+    return ICAL_EQUAL;
+}
+
+<s_equal,s_comma>
+{folded-param-value} {
+    /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+       so copy it. */
+    size_t len = icalleng;
+    unfold (icaltext, &len);
+    icalleng = len;
+
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * s_name */
+    BEGIN (s_name);                   /* s_name */
+    return ICAL_PARAM_VALUE;
+}
+
+<s_equal,s_comma>
+{param-value} {
+    icallval = strdup (icaltext);
+    /* yy_pop_state ();                * s_name */
+    BEGIN (s_name);                   /* s_name */
+    return ICAL_PARAM_VALUE;
+}
+
+<s_name>
+{COMMA} {
+    /* Don't need to strdup a single character. */
+    icallval = icaltext;
+    /* yy_push_state (s_comma);        * s_name, s_comma */
+    BEGIN (s_comma);                  /* s_name, s_comma */
+    return ICAL_COMMA;
+}
+
+<s_value>
+{CRLF} {
+    /* Use start condition to ensure that all newlines are where expected. */
+    icallval = icaltext;
+    /* yy_pop_state ();                * INITIAL */
+    BEGIN (INITIAL);                  /* INITIAL */
+    return ICAL_CRLF;
+}
+
+<s_colon>
+{CRLF} {
+    /* Null value. */
+    icallval = strdup ("");
+    /* yy_pop_state ();                * INITIAL */
+    /* yy_push_state (s_value);        * s_value */
+    BEGIN (s_value);                  /* s_value */
+    /* Push the newline back so it can be handled in the proper state. */
+    unput ('\n');
+    return ICAL_VALUE;
+}
+
+. {
+    /* By default, flex will just pass unmatched text.  Catch it instead. */
+    advise (NULL, "unexpected input: |%s|\n", icaltext);
+}
+
+<<EOF>> {
+    destroy_icallex ();
+    yyterminate ();
+}
+
+%%
+
+static char *
+unfold (char *text, size_t *leng) {
+    /* It's legal to shorten text and modify leng (because we don't
+       use yymore()). */
+    char *cp;
+
+    /* First squash any CR-LF-WSP sequences. */
+    while ((cp = strstr (text, "\r\n "))  ||  (cp = strstr (text, "\r\n\t"))) {
+        /* Subtract any characters prior to fold sequence and 3 for
+           the fold sequence, and add 1 for the terminating null. */
+        (void) memmove (cp, cp + 3, *leng - (cp - text) - 3 + 1);
+        *leng -= 3;
+    }
+
+    /* Then squash any LF-WSP sequences. */
+    while ((cp = strstr (text, "\n "))  ||  (cp = strstr (text, "\n\t"))) {
+        /* Subtract any characters prior to fold sequence and 2 for
+           the fold sequence, and add 1 for the terminating null. */
+        (void) memmove (cp, cp + 2, *leng - (cp - text) - 2 + 1);
+        *leng -= 2;
+    }
+
+    return text;
+}
+
+
+/*
+ * To clean up memory, call the function provided by modern
+ * versions of flex.  Older versions don't have it, and of
+ * course this won't do anything if the scanner was built
+ * with something other than flex.
+ */
+static void
+destroy_icallex () {
+#if defined FLEX_SCANNER  &&  defined YY_FLEX_SUBMINOR_VERSION
+    /* Hack:  rely on fact that the the YY_FLEX_SUBMINOR_VERSION
+       #define was added to flex (flex.skl v. 2.163) after
+       #yylex_destroy() was added. */
+    icallex_destroy ();
+#endif /* FLEX_SCANNER  &&  YY_CURRENT_BUFFER_LVALUE */
+}
+
+/*
+ * See comment in h/icalendar.h about having to provide these
+ * because flex 2.5.4 doesn't.
+ */
+void
+icalset_inputfile (FILE *file) {
+    yyin = file;
+}
+
+void
+icalset_outputfile (FILE *file) {
+    yyout = file;
+}
+
+/*
+ * Porting notes
+ * -------------
+ * POSIX lex only supports an entry point name of yylex().  nmh
+ * programs can contain multiple scanners (see sbr/dtimep.l), so
+ * nmh requires the use of flex to build them.
+ * In addition, if there is a need to port this to Solaris lex:
+ *  - Use the lex -e or -w option.
+ *  - Comment out all of the %options.
+ *  - Comment out the <<EOF>> rule.
+ *  - The start condition and pattern must be on the same line.
+ *  - Comments must be inside rules, not just before them.
+ *  - Don't use start condition stack.  In the code, above BEGIN's are
+ *    used instead, and the contents of an imaginary start condition
+ *    stack are shown after each.  The stack operations are also shown
+ *    in comments.
+ */
diff --git a/sbr/icalparse.y b/sbr/icalparse.y
new file mode 100644 (file)
index 0000000..f2e7b37
--- /dev/null
@@ -0,0 +1,339 @@
+/*
+ * icalparse.y -- icalendar (RFC 5545) parser
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+%{
+    /*
+     * Use these yy* #defines, instead of the -p command line
+     * option, to allow multiple parsers in a program.  yyval
+     * is generated by Solaris yacc and is of type YYSTYPE.
+     * All other yy* symbols are data of a built-in type and
+     * are initialized by yyparse(), so they can be shared
+     * between different parsers.
+     */
+#define yydebug icaldebug
+#define yyerror icalerror
+#define yylex   icallex
+#define yylval  icallval
+#define yyval   icalval
+#define yyparse icalparse
+#define YYDEBUG 1
+#define YYERROR_DEBUG
+#define YYERROR_VERBOSE
+#define YY_NO_LEAKS
+
+    /*
+     * To quiet compile warnings with Solaris yacc:
+#ifdef sun
+# define lint 1
+# undef YYDEBUG
+#endif
+    */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "h/utils.h"
+
+static char *append (contentline *, const char *, const size_t);
+static void new_input_line (contentline **);
+static void new_vevent (vevent *);
+static void free_param_names (param_list *);
+static void free_param_values (value_list *);
+static int icalerror (const char *);
+%}
+
+%token ICAL_NAME ICAL_COLON ICAL_VALUE ICAL_CRLF ICAL_SEMICOLON
+%token ICAL_PARAM_NAME ICAL_EQUAL ICAL_PARAM_VALUE ICAL_COMMA
+
+%start contentline_list
+
+%%
+
+    /* Instead of rigorous definition, cheat based on fact that every
+       icalbody line looks like a contentline.  And we don't need to
+       parse values. */
+contentline_list
+    : contentline
+    | contentline_list contentline
+
+    /* contentline = name *(";" param ) ":" value CRLF */
+contentline
+    : ICAL_NAME {
+          new_input_line (&vevents.last->contentlines);
+          append (vevents.last->contentlines->last, $1, strlen ($1));
+          vevents.last->contentlines->last->name = $1;
+      } ICAL_COLON {
+          append (vevents.last->contentlines->last, $3, strlen ($3));
+      } ICAL_VALUE {
+          append (vevents.last->contentlines->last, $5, strlen ($5));
+          vevents.last->contentlines->last->value = $5;
+      } ICAL_CRLF {
+          append (vevents.last->contentlines->last, $7, strlen ($7));
+          if (vevents.last->contentlines->cr_before_lf == CR_UNSET) {
+              vevents.last->contentlines->cr_before_lf =
+                  $7[0] == '\r'  ?  CR_BEFORE_LF  :  LF_ONLY;
+          }
+          /* END:VEVENT doesn't have a param_list so we don't need
+             to check for it below. */
+          if (vevents.last->contentlines->last->name  &&
+              vevents.last->contentlines->last->value  &&
+              ! strcasecmp (vevents.last->contentlines->last->name, "END")  &&
+              ! strcasecmp (vevents.last->contentlines->last->value,
+                            "VEVENT")) {
+              new_vevent (&vevents);
+          }
+      }
+    | ICAL_NAME {
+          new_input_line (&vevents.last->contentlines);
+          append (vevents.last->contentlines->last, $1, strlen ($1));
+          vevents.last->contentlines->last->name = $1;
+      } param_list ICAL_COLON {
+          append (vevents.last->contentlines->last, $4, strlen ($4));
+      } ICAL_VALUE {
+          append (vevents.last->contentlines->last, $6, strlen ($6));
+          vevents.last->contentlines->last->value = $6;
+      } ICAL_CRLF {
+          append (vevents.last->contentlines->last, $8, strlen ($8));
+          if (vevents.last->contentlines->cr_before_lf == CR_UNSET) {
+              vevents.last->contentlines->cr_before_lf =
+                  $8[0] == '\r'  ?  CR_BEFORE_LF  :  LF_ONLY;
+          }
+      }
+
+param_list
+    : ICAL_SEMICOLON {
+          append (vevents.last->contentlines->last, $1, strlen ($1));
+      } param
+    | param_list ICAL_SEMICOLON {
+          append (vevents.last->contentlines->last, $2, strlen ($2));
+      } param
+
+    /* param = param-name "=" param-value *("," param-value) */
+param
+    : ICAL_PARAM_NAME {
+          append (vevents.last->contentlines->last, $1, strlen ($1));
+          add_param_name (vevents.last->contentlines->last, $1);
+      } ICAL_EQUAL {
+          append (vevents.last->contentlines->last, $3, strlen ($3));
+      } param_value_list
+
+param_value_list
+    : ICAL_PARAM_VALUE {
+          append (vevents.last->contentlines->last, $1, strlen ($1));
+          add_param_value (vevents.last->contentlines->last, $1);
+      }
+    | param_value_list ICAL_COMMA {
+          append (vevents.last->contentlines->last, $2, strlen ($2));
+      } ICAL_PARAM_VALUE {
+          append (vevents.last->contentlines->last, $4, strlen ($4));
+          add_param_value (vevents.last->contentlines->last, $4);
+      }
+
+%%
+
+/*
+ * Remove the contentline node (by setting its name to NULL).
+ */
+void
+remove_contentline (contentline *node) {
+    free (node->name);
+    node->name = NULL;
+}
+
+contentline *
+add_contentline (contentline *node, const char *name) {
+    contentline *new_node = mh_xcalloc (1, sizeof (contentline));
+
+    new_node->name  = strdup (name);
+    new_node->next = node->next;
+    node->next = new_node;
+
+    return new_node;
+}
+
+/*
+ * Remove the value from a value_list.
+ */
+void
+remove_value (value_list *node) {
+    free (node->value);
+    node->value = NULL;
+}
+
+/*
+ * Find the contentline with the specified name, and optionally,
+ * the specified value and/or parameter name.
+ */
+contentline *
+find_contentline (contentline *contentlines, const char *name,
+                  const char *val) {
+    contentline *node;
+
+    for (node = contentlines; node; node = node->next) {
+        /* node->name will be NULL if the line was "deleted". */
+        if (node->name  &&  ! strcasecmp (name, node->name)) {
+            if (val  &&  node->value) {
+                if (! strcasecmp (val, node->value)) {
+                    return node;
+                }
+            } else {
+                return node;
+            }
+        }
+    }
+
+    return NULL;
+}
+
+static char *
+append (contentline *cline, const char *src, const size_t src_len) {
+    if (src_len > 0) {
+        const size_t len = cline->input_line_len + src_len;
+
+        while (len >= cline->input_line_size) {
+            cline->input_line_size = cline->input_line_size == 0
+                ?  (BUFSIZ>=8192 ? BUFSIZ : 8192)
+                :  2 * cline->input_line_size;
+            cline->input_line =
+                mh_xrealloc (cline->input_line, cline->input_line_size);
+        }
+
+        memcpy (cline->input_line + cline->input_line_len, src, src_len);
+        cline->input_line[len] = '\0';
+        cline->input_line_len = len;
+    }
+
+    return cline->input_line;
+}
+
+static void
+new_input_line (contentline **cline) {
+    contentline *new_node = mh_xcalloc (1, sizeof (contentline));
+
+    if (*cline) {
+        /* Append the new node to the end of the list. */
+        (*cline)->last->next = new_node;
+    } else {
+        /* First line:  save the root node in *cline. */
+        *cline = new_node;
+    }
+
+    /* Only maintain the pointer to the last node in the root node. */
+    (*cline)->last = new_node;
+}
+
+static void
+new_vevent (vevent *event) {
+    vevent *new_node = mh_xcalloc (1, sizeof (vevent)), *node;
+
+    /* Append the new node to the end of the list. */
+    for (node = event; node->next; node = node->next) { continue; }
+    event->last =  node->next = new_node;
+}
+
+void
+add_param_name (contentline *cline, char *name) {
+    param_list *new_node = mh_xcalloc (1, sizeof (param_list));
+    param_list *p;
+
+    new_node->param_name = name;
+
+    if (cline->params) {
+        for (p = cline->params; p->next; p = p->next) { continue; }
+        /* The loop terminated at, not after, the last node. */
+        p->next = new_node;
+    } else {
+        cline->params = new_node;
+    }
+}
+
+/*
+ * Add a value to the last parameter seen.
+ */
+void
+add_param_value (contentline *cline, char *value) {
+    value_list *new_node = mh_xcalloc (1, sizeof (value_list));
+    param_list *p;
+    value_list *v;
+
+    new_node->value = value;
+
+    if (cline->params) {
+        for (p = cline->params; p->next; p = p->next) { continue; }
+        /* The loop terminated at, not after, the last param_list node. */
+
+        if (p->values) {
+            for (v = p->values; v->next; v = v->next) { continue; }
+            /* The loop terminated at, not after, the last value_list node. */
+            v->next = new_node;
+        } else {
+            p->values = new_node;
+        }
+    } else {
+        /* Never should get here because a param value is always
+           preceded by a param name. */
+        free (new_node);
+    }
+}
+
+void
+free_contentlines (contentline *root) {
+    contentline *i, *next;
+
+    for (i = root; i; i = next) {
+        free (i->name);
+        if (i->params) {
+            free_param_names (i->params);
+        }
+        free (i->value);
+        free (i->input_line);
+        next = i->next;
+        free (i);
+    }
+}
+
+static void
+free_param_names (param_list *p) {
+    param_list *next;
+
+    for ( ; p; p = next) {
+        free (p->param_name);
+        free_param_values (p->values);
+        next = p->next;
+        free (p);
+    }
+}
+
+static void
+free_param_values (value_list *v) {
+    value_list *next;
+
+    for ( ; v; v = next) {
+        free (v->value);
+        next = v->next;
+        free (v);
+    }
+}
+
+static int
+icalerror (const char *error) {
+    if (! strcmp ("syntax error, unexpected $end, expecting ICAL_NAME",
+                  error)) {
+        /* Empty input: produce no output. */
+    } else {
+        adios (NULL, "%s", error);
+    }
+
+    return 0;  /* The return value isn't used anyway. */
+}
+
+/*
+ * In case YYDEBUG is disabled above; mhical refers to icaldebug.
+ */
+#if ! defined YYDEBUG  ||  ! YYDEBUG
+int icaldebug;
+#endif /* ! YYDEBUG */
diff --git a/test/mhical/test-mhical b/test/mhical/test-mhical
new file mode 100755 (executable)
index 0000000..b1146cc
--- /dev/null
@@ -0,0 +1,742 @@
+#!/bin/sh
+######################################################
+#
+# Test mhical
+#
+######################################################
+
+set -e
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname $0`/../..
+    MH_OBJ_DIR=`cd $srcdir && pwd`; export MH_OBJ_DIR
+fi
+
+. "${MH_OBJ_DIR}/test/common.sh"
+
+setup_test
+
+#### Make sure that html-to-text conversion is what we expect.
+require_locale en_US.utf-8 en_US.utf8
+LC_ALL=en_US.UTF-8; export LC_ALL
+
+#### Disable colorized output.
+TERM=dumb; export TERM
+
+expected="$MH_TEST_DIR/test-mhical$$.expected"
+expected_err="$MH_TEST_DIR/test-mhical$$.expected_err"
+actual="$MH_TEST_DIR/test-mhical$$.actual"
+actual_err="$MH_TEST_DIR/test-mhical$$.actual_err"
+
+
+# check -help
+cat >"$expected" <<EOF
+Usage: mhical [switches]
+  switches are:
+  -reply accept|decline|tentative
+  -cancel
+  -form formatfile
+  -(forma)t string
+  -infile
+  -outfile
+  -[no]contenttype
+  -unfold
+  -debug
+  -version
+  -help
+EOF
+
+run_prog mhical -help >"$actual" 2>&1
+check "$expected" "$actual"
+
+
+# check -version
+case `mhical -version` in
+  mhical\ --*) ;;
+  *) printf '%s: mhical -version generated unexpected output\n' "$0" >&2
+     failed=`expr ${failed:-0} + 1`;;
+esac
+
+
+# check display with timezone that only has standard time
+cat >"$expected" <<'EOF'
+Summary: Santa Watch
+Description: See Santa here first!
+At: Wed, 24 Dec 2014 12:00 +0000
+To: Fri, 25 Dec 2015 11:59
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:test-mhical
+
+BEGIN:VTIMEZONE
+TZID:MHT-12
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:+1200
+TZOFFSETTO:+1200
+END:STANDARD
+END:VTIMEZONE
+
+BEGIN:VEVENT
+DTSTAMP:20141224T140426Z
+DTSTART;TZID=MHT-12:20141225T000000
+DTEND;TZID=MHT-12:20151225T235959
+SUMMARY:Santa Watch
+DESCRIPTION: See Santa here first!
+END:VEVENT
+
+END:VCALENDAR
+EOF
+
+TZ=UTC mhical <"$MH_TEST_DIR/test1.ics" >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check display with 24 hour time format and -outfile
+cat >"$expected" <<'EOF'
+Summary: 4 pm meeting
+At: Mon, 05 Jan 2015 16:00
+To: Mon, 05 Jan 2015 16:30
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:test-mhical
+
+BEGIN:VEVENT
+DTSTAMP:20150101T162400Z
+DTSTART:20150105T160000
+DTEND:20150105T163000
+SUMMARY:4 pm meeting
+END:VEVENT
+
+END:VCALENDAR
+EOF
+
+mhical -outfile "$MH_TEST_DIR/test1.txt" <"$MH_TEST_DIR/test1.ics"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check display with 12 hour time format and -infile
+cat >"$expected" <<'EOF'
+Summary: 4 pm meeting
+At: Mon, 05 Jan 2015  4:00 PM
+To: Mon, 05 Jan 2015  4:30 PM
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:test-mhical
+
+BEGIN:VEVENT
+DTSTAMP:20150101T162800Z
+DTSTART:20150105T160000
+DTEND:20150105T163000
+SUMMARY:4 pm meeting
+END:VEVENT
+
+END:VCALENDAR
+EOF
+
+mhical -form mhical.12hour -infile "$MH_TEST_DIR/test1.ics" \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check display with DST
+cat >"$expected" <<'EOF'
+Method: REQUEST
+Organizer: Requester
+Summary: Big Meeting
+Location: The Office
+At: Mon, 05 Jan 2015 08:00 -0500
+To: Mon, 05 Jan 2015 09:00
+Attendees: Requestee
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:Microsoft Exchange Server 2010
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=FALSE;CN=Requestee
+ :MAILTO:requestee@example.com
+DESCRIPTION;LANGUAGE=en-US:\n\n
+SUMMARY;LANGUAGE=en-US:Big Meeting
+DTSTART;TZID=Eastern Standard Time:20150105T080000
+DTEND;TZID=Eastern Standard Time:20150105T090000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20141231T235959Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:The Office
+X-MICROSOFT-CDO-APPT-SEQUENCE:0
+X-MICROSOFT-CDO-OWNERAPPTID:-0123456789
+X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
+X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
+X-MICROSOFT-CDO-ALLDAYEVENT:FALSE
+X-MICROSOFT-CDO-IMPORTANCE:1
+X-MICROSOFT-CDO-INSTTYPE:0
+X-MICROSOFT-DISALLOW-COUNTER:FALSE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+TZ=EST mhical <"$MH_TEST_DIR/test1.ics" >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check timezone boundary at transition to daylight saving time
+# The default mhical display format doesn't show the timezone for the
+# To: time, but it is different than that of the At: time.
+cat >"$expected" <<'EOF'
+Summary: EST to EDT
+At: Sun, 09 Mar 2014 01:59 -0500
+To: Sun, 09 Mar 2014 03:30
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:test-mhical
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20150101T000000Z
+DTSTART;TZID=Eastern Standard Time:20140309T015959
+DTEND;TZID=Eastern Standard Time:20140309T023000
+Summary: EST to EDT
+END:VEVENT
+END:VCALENDAR
+EOF
+
+TZ=EST5EDT mhical <"$MH_TEST_DIR/test1.ics" >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+
+
+# check -format, and that timezone is correct in end time
+cat >"$expected" <<'EOF'
+Sun, 09 Mar 2014 03:30:00 -0400
+EOF
+
+TZ=EST5EDT mhical -format '%(pretty{dtend})' \
+    -infile "$MH_TEST_DIR/test1.ics" -outfile "$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check timezone boundary at transition from daylight saving time
+cat >"$expected" <<'EOF'
+Summary: EDT to EST
+At: Sun, 02 Nov 2014 01:59 -0400
+To: Sun, 02 Nov 2014 02:00
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:test-mhical
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20150101T000000Z
+DTSTART;TZID=Eastern Standard Time:20141102T015959
+DTEND;TZID=Eastern Standard Time:20141102T020000
+Summary: EDT to EST
+END:VEVENT
+END:VCALENDAR
+EOF
+
+TZ=EST5EDT mhical <"$MH_TEST_DIR/test1.ics" >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+
+
+# check -format, and that timezone is correct in end time
+cat >"$expected" <<'EOF'
+Sun, 02 Nov 2014 02:00:00 -0500
+EOF
+
+TZ=EST5EDT mhical -format '%(pretty{dtend})' \
+    -infile "$MH_TEST_DIR/test1.ics" -outfile "$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+printf 'Local-Mailbox: Requestee2 <requestee2@example.com>\n' >> "$MH"
+
+# check accept of request
+cat >"$expected" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=ACCEPTED;CN=Requestee2:MAILTO:requestee2@example.com
+SUMMARY;LANGUAGE=en-US:Accepted: test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-mhical
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+mhical -reply accept <"$MH_TEST_DIR/test1.ics" | egrep -v '^DTSTAMP:' \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+# check accept of multiple vevent requests in single vcalendar
+cat >"$expected" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=ACCEPTED;CN=Requestee2:MAILTO:requestee2@example.com
+SUMMARY;LANGUAGE=en-US:Accepted: test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456790
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=ACCEPTED;CN=Requestee2:MAILTO:requestee2@example.com
+SUMMARY;LANGUAGE=en-US:Accepted: test request
+DTSTART;TZID=Eastern Standard Time:20150105T130000
+DTEND;TZID=Eastern Standard Time:20150105T134500
+UID:0123456791
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-mhical
+VERSION:2.0
+
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456790
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART;TZID=Eastern Standard Time:20150105T130000
+DTEND;TZID=Eastern Standard Time:20150105T134500
+UID:0123456791
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+
+END:VCALENDAR
+EOF
+
+mhical -reply accept <"$MH_TEST_DIR/test1.ics" | egrep -v '^DTSTAMP:' \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check decline of request
+cat >"$expected" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=DECLINED;CN=Requestee2:MAILTO:requestee2@example.com
+SUMMARY;LANGUAGE=en-US:Declined: test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-mhical
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+mhical -reply decline <"$MH_TEST_DIR/test1.ics" | egrep -v '^DTSTAMP:' \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check response of tentative to request, and -nocontenttype
+cat >"$expected" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=TENTATIVE;CN=Requestee2:MAILTO:requestee2@example.com
+SUMMARY;LANGUAGE=en-US:Tentative: test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-mhical
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+mhical -reply tentative -contenttype -nocontenttype \
+       -infile "$MH_TEST_DIR/test1.ics" | egrep -v '^DTSTAMP:' \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+# check cancel request, and -contenttype
+cat >"$expected" <<'EOF'
+Content-Type: text/calendar; method="CANCEL"; charset="UTF-8"
+
+BEGIN:VCALENDAR
+METHOD:CANCEL
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requestee2:MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:Cancelled:test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CANCELLED
+SEQUENCE:1
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >"$MH_TEST_DIR/test1.ics" <<'EOF'
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-mhical
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER;CN=Requestee2:MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART:20150105T090000
+DTEND:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+mhical -cancel -contenttype <"$MH_TEST_DIR/test1.ics" | egrep -v '^DTSTAMP:' \
+       >"$MH_TEST_DIR/test1.txt"
+check "$expected" "$MH_TEST_DIR/test1.txt"
+rm -f "$MH_TEST_DIR/test1.ics"
+
+
+exit $failed
diff --git a/test/repl/test-convert b/test/repl/test-convert
new file mode 100755 (executable)
index 0000000..e85c9cc
--- /dev/null
@@ -0,0 +1,275 @@
+#!/bin/sh
+######################################################
+#
+# Test repl -convertarg
+#
+######################################################
+
+set -e
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname $0`/../..
+    MH_OBJ_DIR=`cd $srcdir && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/post/test-post-common.sh"
+
+expected="$MH_TEST_DIR/test-convert$$.expected"
+actual=`mhpath +`/draft
+
+printf 'Local-Mailbox: recipient@example.com' >>"$MH"
+
+
+# check -convertarg with multiple parts and additional text in draft
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test
+Comments: In-reply-to sender@example.com
+   message dated "Thu, 11 Dec 2014 08:19:02 -0600."
+MIME-Version: 1.0
+Content-Type: text/plain; charset="US-ASCII"
+
+> This is part 1.
+
+> This is part 2.
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test
+Date: Thu, 11 Dec 2014 08:19:02 -0600
+Content-Type: multipart/mixed; boundary="_001_"
+MIME-Version: 1.0
+
+--_001_
+Content-Type: text/plain
+
+This is part 1.
+
+--_001_
+Content-Type: text/plain
+
+This is part 2.
+
+--_001_
+Content-Type: text/enriched
+
+This should not appear in the reply
+because the content type isn't matched.
+
+--_001_--
+EOF
+
+repl -noformat -convertarg text/plain '' -nowhatnowproc last
+mhbuild "$actual"
+check "$actual" "$expected"
+
+
+#### Make sure that this works with 8-bit encoding.
+require_locale en_US.utf-8 en_US.utf8
+LC_ALL=en_US.UTF-8; export LC_ALL
+
+
+# check -convertarg with multiple parts and no additional text in draft
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test
+Comments: In-reply-to sender@example.com
+   message dated "Thu, 11 Dec 2014 08:19:02 -0600."
+MIME-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 8bit
+
+sender@example.com writes:
+
+> This is part 1.
+
+> This is part 2.
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test
+Date: Thu, 11 Dec 2014 08:19:02 -0600
+Content-Type: multipart/mixed; boundary="_001_"
+MIME-Version: 1.0
+
+--_001_
+Content-Type: text/plain
+
+This is part 1.
+
+--_001_
+Content-Type: text/plain
+
+This is part 2.
+
+--_001_--
+EOF
+
+repl -filter mhl.replywithoutbody -convertarg text/plain '' -nowhatnowproc last
+mhbuild "$actual"
+check "$actual" "$expected"
+
+
+# check message with text part in multipart/related
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test with text part in multipart/related
+Comments: In-reply-to sender@example.com
+   message dated "."
+MIME-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 8bit
+
+sender@example.com writes:
+
+> This is a test.
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test with text part in multipart/related
+Content-Type: multipart/alternative; boundary="_001_"
+MIME-Version: 1.0
+
+--_001_
+Content-Type: multipart/related; type="text/plain"; boundary="_002_"
+
+--_002_
+Content-Type: text/plain
+
+This is a test.
+
+--_002_--
+
+--_001_--
+EOF
+
+repl -filter mhl.replywithoutbody -convertarg text/plain '' -nowhatnowproc last
+mhbuild "$actual"
+check "$actual" "$expected"
+
+
+# check reply to calendar request
+cat >"$expected" <<'EOF'
+From: recipient@example.com
+To: sender@example.com
+cc: 
+Fcc: +outbox
+Subject: Re: test iCalendar reply
+Comments: In-reply-to sender@example.com
+   message dated "."
+MIME-Version: 1.0
+Content-Type: text/calendar; method="REPLY"; charset="UTF-8"
+
+BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:nmh mhical v0.1
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;PARTSTAT=ACCEPTED;CN=Recip:MAILTO:recipient@example.com
+SUMMARY;LANGUAGE=en-US:Accepted: test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+END:VEVENT
+END:VCALENDAR
+EOF
+
+cat >`mhpath new` <<'EOF'
+From: sender@example.com
+To: recipient@example.com
+Subject: test iCalendar reply
+Content-Type: text/calendar; charset="UTF-8"
+MIME-Version: 1.0
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+PRODID:test-convert
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Eastern Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Requester:MAILTO:requester@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee1
+ :MAILTO:requestee1@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee2
+ :MAILTO:requestee2@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Requestee3
+ :MAILTO:requestee3@example.com
+SUMMARY;LANGUAGE=en-US:test request
+DTSTART;TZID=Eastern Standard Time:20150105T090000
+DTEND;TZID=Eastern Standard Time:20150105T093000
+UID:0123456789
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20150101T171600Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+LOCATION;LANGUAGE=en-US:
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:REMINDER
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOF
+
+actual="$MH_TEST_DIR/test-convert$$.actual"
+repl -noformat \
+     -convertargs text/calendar '-reply accept -contenttype' -nowhatnowproc last
+SIGNATURE=Recip mhbuild - <`mhpath +`/draft | egrep -v '^DTSTAMP:' >"$actual"
+check "$actual" "$expected"
+
+
+exit $failed
index e4856d08d26daf360f2db5450aa894d8b532f8bb..20bb96d79e2a12ba4080946925b4db855f3fe0bb 100755 (executable)
@@ -35,6 +35,7 @@ Usage: repl: [+folder] [msg] [switches]
   -nodraftfolder
   -editor editor
   -noedit
+  -convertargs type argstring
   -fcc folder
   -filter filterfile
   -form formfile
index e046886f3e5a9e333016a8f75959e6fcb66366db..2e177c47a76f8c3e5ee2523a012617a5ae7a3280 100644 (file)
@@ -48,6 +48,13 @@ struct attach_list {
     struct attach_list *next;
 };
 
+typedef struct convert_list {
+    char *type;
+    char *filename;
+    char *argstring;
+    struct convert_list *next;
+} convert_list;
+
 /*
  * Maximum size of URL token in message/external-body
  */
@@ -62,6 +69,8 @@ void content_error (char *, CT, char *, ...);
 int find_cache (CT, int, int *, char *, char *, int);
 
 /* mhfree.c */
+extern CT *cts;
+void freects_done (int) NORETURN;
 void free_ctinfo (CT);
 void free_encoding (CT, int);
 
@@ -70,6 +79,12 @@ void free_encoding (CT, int);
  */
 static int init_decoded_content (CT, const char *);
 static void setup_attach_content(CT, char *);
+static void set_disposition (CT);
+static void set_charset (CT, int);
+static void expand_pseudoheaders (CT, struct multipart *, const char *,
+                                  const convert_list *);
+static void expand_pseudoheader (CT, CT *, struct multipart *, const char *,
+                                 const char *, const char *);
 static char *fgetstr (char *, int, FILE *);
 static int user_content (FILE *, char *, CT *, const char *infilename);
 static void set_id (CT, int);
@@ -77,6 +92,7 @@ static int compose_content (CT, int);
 static int scan_content (CT, size_t);
 static int build_headers (CT, int);
 static char *calculate_digest (CT, int);
+static int extract_headers (CT, char *, FILE **);
 
 
 static unsigned char directives_stack[32];
@@ -133,6 +149,7 @@ build_mime (char *infile, int autobuild, int dist, int directives,
     HF hp;
     m_getfld_state_t gstate = 0;
     struct attach_list *attach_head = NULL, *attach_tail = NULL, *at_entry;
+    convert_list *convert_head = NULL, *convert_tail = NULL, *convert;
 
     directive_init(directives);
 
@@ -239,6 +256,86 @@ build_mime (char *infile, int autobuild, int dist, int directives,
                } else {
                    attach_head = attach_tail = entry;
                }
+           } else if (strncasecmp(MHBUILD_FILE_PSEUDOHEADER, np,
+                                   strlen (MHBUILD_FILE_PSEUDOHEADER)) == 0) {
+                /* E.g.,
+                 * Nmh-mhbuild-file-text/calendar: /home/user/Mail/inbox/9
+                 */
+                char *type = np + strlen (MHBUILD_FILE_PSEUDOHEADER);
+                char *filename = vp;
+
+                /* vp should begin with a space because m_getfld()
+                   includes the space after the colon in buf. */
+                while (isspace((unsigned char) *filename)) { ++filename; }
+                /* Trim trailing newline and any other whitespace. */
+                rtrim (filename);
+
+                for (convert = convert_head; convert; convert = convert->next) {
+                    if (strcasecmp (convert->type, type) == 0) { break; }
+                }
+                if (convert) {
+                    if (convert->filename  &&
+                        strcasecmp (convert->filename, filename)) {
+                        adios (NULL, "Multiple %s headers with different files"
+                               " not allowed", type);
+                    } else {
+                        convert->filename = getcpy (filename);
+                    }
+                } else {
+                    convert = mh_xcalloc (sizeof *convert, 1);
+                    convert->filename = getcpy (filename);
+                    convert->type = getcpy (type);
+
+                    if (convert_tail) {
+                        convert_tail->next = convert;
+                    } else {
+                        convert_head = convert;
+                    }
+                    convert_tail = convert;
+                }
+
+                free (vp);
+                free (np);
+            } else if (strncasecmp(MHBUILD_ARGS_PSEUDOHEADER, np,
+                                   strlen (MHBUILD_ARGS_PSEUDOHEADER)) == 0) {
+                /* E.g.,
+                 * Nmh-mhbuild-args-text/calendar: -reply accept
+                 */
+                char *type = np + strlen (MHBUILD_ARGS_PSEUDOHEADER);
+                char *argstring = vp;
+
+                /* vp should begin with a space because m_getfld()
+                   includes the space after the colon in buf. */
+                while (isspace((unsigned char) *argstring)) { ++argstring; }
+                /* Trim trailing newline and any other whitespace. */
+                rtrim (argstring);
+
+                for (convert = convert_head; convert; convert = convert->next) {
+                    if (strcasecmp (convert->type, type) == 0) { break; }
+                }
+                if (convert) {
+                    if (convert->argstring  &&
+                        strcasecmp (convert->argstring, argstring)) {
+                        adios (NULL, "Multiple %s headers with different "
+                               "argstrings not allowed", type);
+                    } else {
+                        convert->argstring = getcpy (argstring);
+                    }
+                } else {
+                    convert = mh_xcalloc (sizeof *convert, 1);
+                    convert->type = getcpy (type);
+                    convert->argstring = getcpy (argstring);
+
+                    if (convert_tail) {
+                        convert_tail->next = convert;
+                    } else {
+                        convert_head = convert;
+                    }
+                    convert_tail = convert;
+                }
+
+                free (vp);
+                free (np);
            } else {
                add_header (ct, np, vp);
            }
@@ -361,6 +458,40 @@ finish_field:
        free(at_prev);
     }
 
+    /*
+     * Handle the mhbuild pseudoheaders, which deal with specific
+     * content types.
+     */
+    if (convert_head) {
+        CT *ctp;
+        convert_list *next;
+
+        done = freects_done;
+
+        /* In case there are multiple calls that land here, prevent leak. */
+        for (ctp = cts; ctp && *ctp; ++ctp) { free_content (*ctp); }
+        free (cts);
+
+        /* Extract the type part (as a CT) from filename. */
+        if (! (cts = (CT *) mh_xcalloc ((size_t) 2, sizeof *cts))) {
+            adios (NULL, "out of memory");
+        } else if (! (cts[0] = parse_mime (convert_head->filename))) {
+            adios (NULL, "failed to parse %s", convert_head->filename);
+        }
+
+        expand_pseudoheaders (cts[0], m, infile, convert_head);
+
+        /* Free the convert list. */
+        for (convert = convert_head; convert; convert = next) {
+            next = convert->next;
+            free (convert->type);
+            free (convert->filename);
+            free (convert->argstring);
+            free (convert);
+        }
+        convert_head = NULL;
+    }
+
     /*
      * To allow for empty message bodies, if we've found NO content at all
      * yet cook up an empty text/plain part.
@@ -1471,24 +1602,7 @@ scan_content (CT ct, size_t maxunencoded)
      * If the content is text and didn't specify a character set,
      * we need to figure out which one was used.
      */
-
-    if (ct->c_type == CT_TEXT) {
-       t = (struct text *) ct->c_ctparams;
-       if (t->tx_charset == CHARSET_UNSPECIFIED) {
-           CI ci = &ct->c_ctinfo;
-           char *eightbitcharset = write_charset_8bit();
-
-           if (contains8bit && strcasecmp(eightbitcharset, "US-ASCII") == 0) {
-               adios(NULL, "Text content contains 8 bit characters, but "
-                     "character set is US-ASCII");
-           }
-
-           add_param(&ci->ci_first_pm, &ci->ci_last_pm, "charset",
-                       contains8bit ? eightbitcharset : "us-ascii", 0);
-
-           t->tx_charset = CHARSET_SPECIFIED;
-       }
-    }
+    set_charset (ct, contains8bit);
 
     /*
      * Decide which transfer encoding to use.
@@ -1866,7 +1980,6 @@ setup_attach_content(CT ct, char *filename)
     char *type, *simplename = r1bindex(filename, '/');
     struct str2init *s2i;
     PM pm;
-    char *cp;
 
     if (! (type = mime_type(filename))) {
        adios(NULL, "Unable to determine MIME type of \"%s\"", filename);
@@ -1939,17 +2052,26 @@ setup_attach_content(CT ct, char *filename)
     ct->c_descr = add("\n", ct->c_descr);
     ct->c_cefile.ce_file = getcpy(filename);
 
-    /*
-     * Look for mhbuild-disposition-<type>/<subtype> entry
-     * that specifies Content-Disposition type.  Only
-     * 'attachment' and 'inline' are allowed.  Default to
-     * 'attachment'.
-     */
+    set_disposition (ct);
+
+    add_param(&ct->c_dispo_first, &ct->c_dispo_last, "filename", simplename, 0);
+}
 
-    cp = context_find_by_type ("disposition", ct->c_ctinfo.ci_type,
-                               ct->c_ctinfo.ci_subtype);
-    if (cp != NULL) {
-        if (strcasecmp (cp, "attachment")  &&  strcasecmp (cp, "inline")) {
+/*
+ * If disposition type hasn't already been set in ct:
+ * Look for mhbuild-disposition-<type>/<subtype> entry
+ * that specifies Content-Disposition type.  Only
+ * 'attachment' and 'inline' are allowed.  Default to
+ * 'attachment'.
+ */
+void
+set_disposition (CT ct) {
+    if (ct->c_dispo_type == NULL) {
+        char *cp = context_find_by_type ("disposition", ct->c_ctinfo.ci_type,
+                                         ct->c_ctinfo.ci_subtype);
+
+        if (cp  &&  strcasecmp (cp, "attachment")  &&
+            strcasecmp (cp, "inline")) {
             admonish (NULL, "configuration problem: %s-disposition-%s%s%s "
                       "specifies '%s' but only 'attachment' and 'inline' are "
                       "allowed", invo_name,
@@ -1958,13 +2080,396 @@ setup_attach_content(CT ct, char *filename)
                       ct->c_ctinfo.ci_subtype ? ct->c_ctinfo.ci_subtype : "",
                       cp);
         }
+
+        ct->c_dispo_type = cp  ?  getcpy (cp)  :  getcpy ("attachment");
+    }
+}
+
+/*
+ * Set text content charset if it was unspecified.  contains8bit
+ * selctions:
+ * 0: content does not contain 8-bit characters
+ * 1: content contains 8-bit characters
+ * -1: ignore content and use user's locale to determine charset
+ */
+void
+set_charset (CT ct, int contains8bit) {
+    if (ct->c_type == CT_TEXT) {
+        struct text *t;
+
+        if (ct->c_ctparams == NULL) {
+            if ((t = ct->c_ctparams =
+                 (struct text *) mh_xcalloc (1, sizeof (struct text))) ==
+                NULL) {
+                adios (NULL, "out of memory");
+            }
+            t->tx_charset = CHARSET_UNSPECIFIED;
+        } else {
+            t = (struct text *) ct->c_ctparams;
+        }
+
+        if (t->tx_charset == CHARSET_UNSPECIFIED) {
+            CI ci = &ct->c_ctinfo;
+            char *eightbitcharset = write_charset_8bit();
+            char *charset = contains8bit ? eightbitcharset : "us-ascii";
+
+            if (contains8bit == 1  &&
+                strcasecmp (eightbitcharset, "US-ASCII") == 0) {
+                adios (NULL, "Text content contains 8 bit characters, but "
+                       "character set is US-ASCII");
+            }
+
+            add_param (&ci->ci_first_pm, &ci->ci_last_pm, "charset", charset,
+                       0);
+
+            t->tx_charset = CHARSET_SPECIFIED;
+        }
+    }
+}
+
+
+/*
+ * Look at all of the replied-to message parts and expand any that
+ * are matched by a pseudoheader.  Except don't descend into
+ * message parts.
+ */
+void
+expand_pseudoheaders (CT ct, struct multipart *m, const char *infile,
+                      const convert_list *convert_head) {
+    /* text_plain_ct is used to concatenate all of the text/plain
+       replies into one part, instead of having each one in a separate
+       part. */
+    CT text_plain_ct = NULL;
+
+    switch (ct->c_type) {
+    case CT_MULTIPART: {
+        struct multipart *mp = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        if (ct->c_subtype == MULTI_ALTERNATE) {
+            int matched = 0;
+
+            /* The parts are in descending priority order (defined by
+               RFC 2046 Sec. 5.1.4) because they were reversed by
+               parse_mime ().  So, stop looking for matches with
+               immediate subparts after the first match of an
+               alternative. */
+            for (part = mp->mp_parts; ! matched && part; part = part->mp_next) {
+                char *type_subtype =
+                    concat (part->mp_part->c_ctinfo.ci_type, "/",
+                            part->mp_part->c_ctinfo.ci_subtype, NULL);
+
+                if (part->mp_part->c_type == CT_MULTIPART) {
+                    expand_pseudoheaders (part->mp_part, m, infile,
+                                          convert_head);
+                } else {
+                    const convert_list *c;
+
+                    for (c = convert_head; c; c = c->next) {
+                        if (! strcasecmp (type_subtype, c->type)) {
+                            expand_pseudoheader (part->mp_part, &text_plain_ct,
+                                                 m, infile,
+                                                 c->type, c->argstring);
+                            matched = 1;
+                            break;
+                        }
+                    }
+                }
+                free (type_subtype);
+            }
+        } else {
+            for (part = mp->mp_parts; part; part = part->mp_next) {
+                expand_pseudoheaders (part->mp_part, m, infile, convert_head);
+            }
+        }
+        break;
+    }
+
+    default: {
+        char *type_subtype =
+            concat (ct->c_ctinfo.ci_type, "/", ct->c_ctinfo.ci_subtype,
+                    NULL);
+        const convert_list *c;
+
+        for (c = convert_head; c; c = c->next) {
+            if (! strcasecmp (type_subtype, c->type)) {
+                expand_pseudoheader (ct, &text_plain_ct, m, infile, c->type,
+                                     c->argstring);
+                break;
+            }
+        }
+        free (type_subtype);
+        break;
+    }
+    }
+}
+
+
+/*
+ * Expand a single pseudoheader.  It's for the specified type.
+ */
+void
+expand_pseudoheader (CT ct, CT *text_plain_ct, struct multipart *m,
+                     const char *infile, const char *type,
+                     const char *argstring) {
+    char *reply_file;
+    FILE *reply_fp = NULL;
+    char *convert, *type_p, *subtype_p;
+    char *convert_command;
+    char *charset = NULL;
+    char *cp;
+    struct str2init *s2i;
+    CT reply_ct;
+    struct part *part;
+    int status;
+
+    type_p = getcpy (type);
+    if ((subtype_p = strchr (type_p, '/'))) {
+        *subtype_p++ = '\0';
+        convert = context_find_by_type ("convert", type_p, subtype_p);
+    } else {
+        free (type_p);
+        type_p = concat ("mhbuild-convert-", type, NULL);
+        convert = context_find (type_p);
+    }
+    free (type_p);
+
+    if (! (convert)) {
+        /* No mhbuild-convert- entry in mhn.defaults or profile
+           for type. */
+        return;
+    }
+    /* reply_file is used to pass the output of the convert. */
+    reply_file = getcpy (m_mktemp2 (NULL, invo_name, NULL, NULL));
+    convert_command =
+        concat (convert, " ", argstring ? argstring : "", " >", reply_file,
+                NULL);
+
+    /* Convert here . . . */
+    ct->c_storeproc = getcpy (convert_command);
+    ct->c_umask = ~m_gmprot ();
+
+    if ((status = show_content_aux (ct, 0, convert_command, NULL, NULL)) !=
+        OK) {
+        admonish (NULL, "store of %s content failed", type);
+    }
+    free (convert_command);
+
+    /* Fill out the the new ct, reply_ct. */
+    reply_ct = (CT) mh_xcalloc (1, sizeof *reply_ct);
+    init_decoded_content (reply_ct, infile);
+
+    if (extract_headers (reply_ct, reply_file, &reply_fp) == NOTOK) {
+        free (reply_file);
+        admonish (NULL,
+                  "failed to extract headers from convert output in %s",
+                  reply_file);
+        return;
+    }
+
+    /* This sets reply_ct->c_ctparams, and reply_ct->c_termproc if the
+       charset can't be handled natively. */
+    for (s2i = str2cts; s2i->si_key; s2i++) {
+        if (strcasecmp(reply_ct->c_ctinfo.ci_type, s2i->si_key) == 0) {
+            break;
+        }
+    }
+
+    if ((reply_ct->c_ctinitfnx = s2i->si_init)) {
+        (*reply_ct->c_ctinitfnx)(reply_ct);
     }
 
-    if (cp) {
-       ct->c_dispo_type = getcpy(cp);
+    if ((cp =
+         get_param (reply_ct->c_ctinfo.ci_first_pm, "charset", '?', 1))) {
+        /* The reply Content-Type had the charset. */
+        charset = cp;
     } else {
-       ct->c_dispo_type = getcpy("attachment");
+        set_charset (reply_ct, -1);
+        charset = get_param (reply_ct->c_ctinfo.ci_first_pm, "charset", '?', 1);
+        if (reply_ct->c_reqencoding == CE_UNKNOWN) {
+            /* Assume that 8bit is sufficient (for text). */
+            reply_ct->c_reqencoding =
+                strcasecmp (charset, "US-ASCII")  ?  CE_8BIT  :  CE_7BIT;
+        }
     }
 
-    add_param(&ct->c_dispo_first, &ct->c_dispo_last, "filename", simplename, 0);
+    /* Concatenate text/plain parts. */
+    if (reply_ct->c_type == CT_TEXT  &&
+        reply_ct->c_subtype == TEXT_PLAIN) {
+        if (! *text_plain_ct  &&  m->mp_parts  &&  m->mp_parts->mp_part  &&
+            m->mp_parts->mp_part->c_type == CT_TEXT  &&
+            m->mp_parts->mp_part->c_subtype == TEXT_PLAIN) {
+            *text_plain_ct = m->mp_parts->mp_part;
+            /* Make sure that the charset is set in the text/plain
+               part. */
+            set_charset (*text_plain_ct, -1);
+            if ((*text_plain_ct)->c_reqencoding == CE_UNKNOWN) {
+                /* Assume that 8bit is sufficient (for text). */
+                (*text_plain_ct)->c_reqencoding =
+                    strcasecmp (charset, "US-ASCII")  ?  CE_8BIT  :  CE_7BIT;
+            }
+        }
+
+        if (*text_plain_ct) {
+            /* Only concatenate if the charsets are identical. */
+            char *text_plain_ct_charset =
+                get_param ((*text_plain_ct)->c_ctinfo.ci_first_pm, "charset",
+                           '?', 1);
+
+            if (strcasecmp (text_plain_ct_charset, charset) == 0) {
+                /* Append this text/plain reply to the first one.
+                   If there's a problem anywhere along the way,
+                   instead attach it is a separate part. */
+                int text_plain_reply =
+                    open ((*text_plain_ct)->c_cefile.ce_file,
+                          O_WRONLY | O_APPEND);
+                int addl_reply = open (reply_file, O_RDONLY);
+
+                if (text_plain_reply != NOTOK  &&  addl_reply != NOTOK) {
+                    /* Insert blank line before each addl part. */
+                    /* It would be nice not to do this for the first one. */
+                    if (write (text_plain_reply, "\n", 1) == 1) {
+                        /* Copy the text from the new reply and
+                           then free its Content struct. */
+                        cpydata (addl_reply, text_plain_reply,
+                                 (*text_plain_ct)->c_cefile.ce_file,
+                                 reply_file);
+                        if (close (text_plain_reply) == OK  &&
+                            close (addl_reply) == OK) {
+                            if (reply_fp) { fclose (reply_fp); }
+                            free (reply_file);
+                            free_content (reply_ct);
+                            return;
+                        }
+                    }
+                }
+            }
+        } else {
+            *text_plain_ct = reply_ct;
+        }
+    }
+
+    reply_ct->c_cefile.ce_file = reply_file;
+    reply_ct->c_cefile.ce_fp = reply_fp;
+    reply_ct->c_cefile.ce_unlink = 1;
+
+    /* Attach the new part to the parent mulitpart/mixed, "m". */
+    part = (struct part *) mh_xcalloc (1, sizeof *part);
+    part->mp_part = reply_ct;
+    if (m->mp_parts) {
+        struct part *p;
+
+        for (p = m->mp_parts; p && p->mp_next; p = p->mp_next) { continue; }
+        p->mp_next = part;
+    } else {
+        m->mp_parts = part;
+    }
+}
+
+
+/* Extract any Content-Type header from beginning of convert output. */
+int
+extract_headers (CT ct, char *reply_file, FILE **reply_fp) {
+    char *buffer = NULL, *cp, *end_of_header;
+    int found_header = 0;
+    struct stat statbuf;
+
+    /* Read the convert reply from the file to memory. */
+    if (stat (reply_file, &statbuf) == NOTOK) {
+        admonish (reply_file, "failed to stat");
+        goto failed_to_extract_ct;
+    }
+
+    buffer = mh_xmalloc (statbuf.st_size + 1);
+
+    if ((*reply_fp = fopen (reply_file, "r+")) == NULL  ||
+        fread (buffer, 1, (size_t) statbuf.st_size, *reply_fp) <
+            (size_t) statbuf.st_size) {
+        admonish (reply_file, "failed to read");
+        goto failed_to_extract_ct;
+    }
+    buffer[statbuf.st_size] = '\0';
+
+    /* Look for a header in the convert reply. */
+    if (strncasecmp (buffer, TYPE_FIELD, strlen (TYPE_FIELD)) == 0  &&
+        buffer[strlen (TYPE_FIELD)] == ':') {
+        if ((end_of_header = strstr (buffer, "\r\n\r\n"))) {
+            end_of_header += 2;
+            found_header = 1;
+        } else if ((end_of_header = strstr (buffer, "\n\n"))) {
+            ++end_of_header;
+            found_header = 1;
+        }
+    }
+
+    if (found_header) {
+        CT tmp_ct;
+        char *tmp_file;
+        FILE *tmp_f;
+        size_t n;
+
+        /* Truncate buffer to just the C-T. */
+        *end_of_header = '\0';
+        n = strlen (buffer);
+
+        if (get_ctinfo (buffer + 14, ct, 0) != OK) {
+            admonish (NULL, "unable to get content info for reply");
+            goto failed_to_extract_ct;
+        }
+
+        /* Hack.  Use parse_mime() to detect the type/subtype of the
+           reply, which we'll use below. */
+        tmp_file = getcpy (m_mktemp2 (NULL, invo_name, NULL, NULL));
+        if ((tmp_f = fopen (tmp_file, "w"))  &&
+            fwrite (buffer, 1, n, tmp_f) == n) {
+            fclose (tmp_f);
+        } else {
+            goto failed_to_extract_ct;
+        }
+        tmp_ct = parse_mime (tmp_file);
+
+        if (tmp_ct) {
+            /* The type and subtype were detected from the reply
+               using parse_mime() above. */
+            ct->c_type = tmp_ct->c_type;
+            ct->c_subtype = tmp_ct->c_subtype;
+            free_content (tmp_ct);
+        }
+
+        free (tmp_file);
+
+        /* Rewrite the content without the header. */
+        cp = end_of_header + 1;
+        rewind (*reply_fp);
+
+        if (fwrite (cp, 1, statbuf.st_size - (cp - buffer), *reply_fp) <
+            (size_t) (statbuf.st_size - (cp - buffer))) {
+            admonish (reply_file, "failed to write");
+            goto failed_to_extract_ct;
+        }
+
+        if (ftruncate (fileno (*reply_fp), statbuf.st_size - (cp - buffer)) !=
+            0) {
+            advise (reply_file, "ftruncate");
+            goto failed_to_extract_ct;
+        }
+    } else {
+        /* No header section, assume the reply is text/plain. */
+        ct->c_type = CT_TEXT;
+        ct->c_subtype = TEXT_PLAIN;
+        if (get_ctinfo ("text/plain", ct, 0) == NOTOK) {
+            /* This never should fail, but just in case. */
+            adios (NULL, "unable to get content info for reply");
+        }
+    }
+
+    /* free_encoding() will close reply_fp, which is passed through
+       ct->c_cefile.ce_fp. */
+    free (buffer);
+    return OK;
+
+failed_to_extract_ct:
+    if (*reply_fp) { fclose (*reply_fp); }
+    free (buffer);
+    return NOTOK;
 }
diff --git a/uip/mhical.c b/uip/mhical.c
new file mode 100644 (file)
index 0000000..eb26c38
--- /dev/null
@@ -0,0 +1,816 @@
+/*
+ * mhical.c -- operate on an iCalendar request
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "sbr/icalparse.h"
+#include <h/fmt_scan.h>
+#include "h/addrsbr.h"
+#include "h/mts.h"
+#include "h/utils.h"
+#include <time.h>
+
+typedef enum act {
+    ACT_NONE,
+    ACT_ACCEPT,
+    ACE_DECLINE,
+    ACT_TENTATIVE,
+    ACT_DELEGATE,
+    ACT_CANCEL
+} act;
+
+static void convert_to_reply (contentline *, act);
+static void convert_to_cancellation (contentline *);
+static void convert_common (contentline *, act);
+static void dump_unfolded (FILE *, contentline *);
+static void output (FILE *, contentline *, int);
+static void display (FILE *, contentline *, char *);
+static const char *identity (const contentline *);
+static char *format_params (char *, param_list *);
+static char *fold (char *, int);
+
+#define MHICAL_SWITCHES \
+    X("reply accept|decline|tentative", 0, REPLYSW) \
+    X("cancel", 0, CANCELSW) \
+    X("form formatfile", 0, FORMSW) \
+    X("format string", 5, FMTSW) \
+    X("infile", 0, INFILESW) \
+    X("outfile", 0, OUTFILESW) \
+    X("contenttype", 0, CONTENTTYPESW) \
+    X("nocontenttype", 0, NCONTENTTYPESW) \
+    X("unfold", 0, UNFOLDSW) \
+    X("debug", 0, DEBUGSW) \
+    X("version", 0, VERSIONSW) \
+    X("help", 0, HELPSW) \
+
+#define X(sw, minchars, id) id,
+DEFINE_SWITCH_ENUM(MHICAL);
+#undef X
+
+#define X(sw, minchars, id) { sw, minchars, id },
+DEFINE_SWITCH_ARRAY(MHICAL, switches);
+#undef X
+
+vevent vevents = { NULL, NULL, NULL};
+
+int
+main (int argc, char *argv[]) {
+    /* RFC 5322 § 3.3 date-time format, including the optional
+       day-of-week and not including the optional seconds.  The
+       zone is required by the RFC but not always output by this
+       format, because RFC 5545 § 3.3.5 allows date-times not
+       bound to any time zone. */
+
+    act action = ACT_NONE;
+    char *infile = NULL, *outfile = NULL;
+    FILE *inputfile = NULL, *outputfile = NULL;
+    int contenttype = 0, unfold = 0;
+    vevent *v, *nextvevent;
+    char *form = "mhical.24hour", *format = NULL;
+    char **argp, **arguments, *cp;
+
+    icaldebug = 0;  /* Global provided by bison (with name-prefix "ical"). */
+
+    if (nmh_init(argv[0], 1)) { return 1; }
+
+    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: {
+                char buf[128];
+                snprintf (buf, sizeof buf, "%s [switches]", invo_name);
+                print_help (buf, switches, 1);
+                done (0);
+            }
+            case VERSIONSW:
+                print_version(invo_name);
+                done (0);
+            case DEBUGSW:
+                icaldebug = 1;
+                continue;
+
+            case REPLYSW:
+                if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                if (! strcasecmp (cp, "accept")) {
+                    action = ACT_ACCEPT;
+                } else if (! strcasecmp (cp, "decline")) {
+                    action = ACE_DECLINE;
+                } else if (! strcasecmp (cp, "tentative")) {
+                    action = ACT_TENTATIVE;
+                } else if (! strcasecmp (cp, "delegate")) {
+                    action = ACT_DELEGATE;
+                } else {
+                    adios (NULL, "Unknown action: %s", cp);
+                }
+                continue;
+
+            case CANCELSW:
+                action = ACT_CANCEL;
+                continue;
+
+            case FORMSW:
+                if (! (form = *argp++) || *form == '-')
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                format = NULL;
+                continue;
+            case FMTSW:
+                if (! (format = *argp++) || *format == '-')
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                form = NULL;
+                continue;
+
+            case INFILESW:
+                if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                infile = *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 CONTENTTYPESW:
+                contenttype = 1;
+                continue;
+            case NCONTENTTYPESW:
+                contenttype = 0;
+                continue;
+
+            case UNFOLDSW:
+                unfold = 1;
+                continue;
+            }
+        }
+    }
+
+    free (arguments);
+
+    if (infile) {
+        if ((inputfile = fopen (infile, "r"))) {
+            icalset_inputfile (inputfile);
+        } else {
+            adios (infile, "error opening");
+        }
+    } else {
+        inputfile = stdin;
+    }
+
+    if (outfile) {
+        if ((outputfile = fopen (outfile, "w"))) {
+            icalset_outputfile (outputfile);
+        } else {
+            adios (outfile, "error opening");
+        }
+    } else {
+        outputfile = stdout;
+    }
+
+    vevents.last = &vevents;
+    /* vevents is accessed by parser as global. */
+    icalparse ();
+
+    for (v = &vevents; v; v = nextvevent) {
+        if (! unfold  &&  v != &vevents  &&  v->contentlines  &&
+            v->contentlines->name  &&
+            strcasecmp (v->contentlines->name, "END")  &&
+            v->contentlines->value  &&
+            strcasecmp (v->contentlines->value, "VCALENDAR")) {
+            /* Output blank line between vevents.  Not before
+               first vevent and not after last. */
+            putc ('\n', outputfile);
+        }
+
+        if (action == ACT_NONE) {
+            if (unfold) {
+                dump_unfolded (outputfile, v->contentlines);
+            } else {
+                char *nfs = new_fs (form, format, NULL);
+
+                display (outputfile, v->contentlines, nfs);
+                free_fs ();
+            }
+        } else {
+            if (action == ACT_CANCEL) {
+                convert_to_cancellation (v->contentlines);
+            } else {
+                convert_to_reply (v->contentlines, action);
+            }
+            output (outputfile, v->contentlines, contenttype);
+        }
+
+        free_contentlines (v->contentlines);
+        nextvevent = v->next;
+        if (v != &vevents) {
+            free (v);
+        }
+    }
+
+    if (infile) {
+        if (fclose (inputfile) != 0) {
+            advise (infile, "error closing");
+        }
+        free (infile);
+    }
+    if (outfile) {
+        if (fclose (outputfile) != 0) {
+            advise (outfile, "error closing");
+        }
+        free (outfile);
+    }
+
+    return 0;
+}
+
+/*
+ * - Change METHOD from REQUEST to REPLY.
+ * - Change PRODID.
+ * - Remove all ATTENDEE lines for other users (based on ismymbox ()).
+ * - For the user's ATTENDEE line:
+ *   - Remove ROLE and RSVP parameters.
+ *   - Change PARTSTAT value to indicate reply action, e.g., ACCEPTED,
+ *     DECLINED, or TENTATIVE.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Remove all DESCRIPTION lines.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_reply (contentline *clines, act action) {
+    char *partstat = NULL;
+    int found_my_attendee_line = 0;
+    contentline *node;
+
+    convert_common (clines, action);
+
+    switch (action) {
+    case ACT_ACCEPT:
+        partstat = "ACCEPTED";
+        break;
+    case ACE_DECLINE:
+        partstat = "DECLINED";
+        break;
+    case ACT_TENTATIVE:
+        partstat = "TENTATIVE";
+        break;
+    default:
+        ;
+    }
+
+    /* Call find_contentline () with node as argument to find multiple
+       matching contentlines. */
+    for (node = clines;
+         (node = find_contentline (node, "ATTENDEE", 0));
+         node = node->next) {
+        param_list *p;
+
+        ismymbox (NULL); /* need to prime ismymbox() */
+
+        /* According to RFC 5545 § 3.3.3, an email address in the
+           value must be a mailto URI. */
+        if (! strncasecmp (node->value, "mailto:", 7)) {
+            char *addr = node->value + 7;
+            struct mailname *mn;
+
+            /* Skip any leading whitespace. */
+            for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+            addr = getname (addr);
+            mn = getm (addr, NULL, 0, NULL, 0);
+
+            /* Need to flush getname after use. */
+            while (getname ("")) { continue; }
+
+            if (ismymbox (mn)) {
+                found_my_attendee_line = 1;
+                for (p = node->params; p && p->param_name; p = p->next) {
+                    value_list *v;
+
+                    for (v = p->values; v; v = v->next) {
+                        if (! strcasecmp (p->param_name, "ROLE")  ||
+                            ! strcasecmp (p->param_name, "RSVP")) {
+                            remove_value (v);
+                        } else if (! strcasecmp (p->param_name, "PARTSTAT")) {
+                            free (v->value);
+                            v->value = strdup (partstat);
+                        }
+                    }
+                }
+            } else {
+                remove_contentline (node);
+            }
+
+            mnfree (mn);
+        }
+    }
+
+    if (! found_my_attendee_line) {
+        /* Generate and attach an ATTENDEE line for me. */
+        contentline *node;
+
+        /* Add it after the ORGANIZER line, or if none, BEGIN:VEVENT line. */
+        if ((node = find_contentline (clines, "ORGANIZER", 0))  ||
+            (node = find_contentline (clines, "BEGIN", "VEVENT"))) {
+            contentline *new_node = add_contentline (node, "ATTENDEE");
+
+            add_param_name (new_node, strdup ("PARTSTAT"));
+            add_param_value (new_node, strdup (partstat));
+            add_param_name (new_node, strdup ("CN"));
+            add_param_value (new_node, strdup (getfullname ()));
+            new_node->value = concat ("MAILTO:", getlocalmbox (), NULL);
+        }
+    }
+
+    /* Call find_contentline () with node as argument to find multiple
+       matching contentlines. */
+    for (node = clines;
+         (node = find_contentline (node, "DESCRIPTION", 0));
+         node = node->next) {
+        /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
+        remove_contentline (node);
+    }
+}
+
+/*
+ * - Change METHOD from REQUEST to CANCEL.
+ * - Change PRODID.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Change STATUS from CONFIRMED to CANCELLED.
+ * - Increment value of SEQUENCE.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_cancellation (contentline *clines) {
+    contentline *node;
+
+    convert_common (clines, ACT_CANCEL);
+
+    if ((node = find_contentline (clines, "STATUS", 0))  &&
+        ! strcasecmp (node->value, "CONFIRMED")) {
+        free (node->value);
+        node->value = strdup ("CANCELLED");
+    }
+
+    if ((node = find_contentline (clines, "SEQUENCE", 0))) {
+        int sequence = atoi (node->value);
+        char buf[32];
+
+        (void) snprintf (buf, sizeof buf, "%d", sequence + 1);
+        free (node->value);
+        node->value = strdup (buf);
+    }
+}
+
+static void
+convert_common (contentline *clines, act action) {
+    contentline *node;
+    int in_valarm;
+
+    if ((node = find_contentline (clines, "METHOD", 0))) {
+        free (node->value);
+        node->value = strdup (action == ACT_CANCEL  ?  "CANCEL"  :  "REPLY");
+    }
+
+    if ((node = find_contentline (clines, "PRODID", 0))) {
+        free (node->value);
+        node->value = strdup ("nmh mhical v0.1");
+    }
+
+    if ((node = find_contentline (clines, "VERSION", 0))) {
+        if (! node->value) {
+            admonish (NULL, "Version property is missing value, assume 2.0");
+            node->value = strdup ("2.0");
+        }
+
+        if (strcmp (node->value, "2.0")) {
+            admonish (NULL, "supports the Version 2.0 specified by RFC 5545 "
+                            "but iCalendar object has Version %s", node->value);
+            node->value = strdup ("2.0");
+        }
+    }
+
+    if ((node = find_contentline (clines, "SUMMARY", 0))) {
+        char *insert = NULL;
+
+        switch (action) {
+        case ACT_ACCEPT:
+            insert = "Accepted: ";
+            break;
+        case ACE_DECLINE:
+            insert = "Declined: ";
+            break;
+        case ACT_TENTATIVE:
+            insert = "Tentative: ";
+            break;
+        case ACT_DELEGATE:
+            adios (NULL, "Delegate replies are not supported");
+            break;
+        case ACT_CANCEL:
+            insert = "Cancelled:";
+            break;
+        default:
+            ;
+        }
+
+        if (insert) {
+            const size_t len = strlen (insert) + strlen (node->value) + 1;
+            char *tmp = mh_xmalloc (len);
+
+            (void) strncpy (tmp, insert, len);
+            (void) strncat (tmp, node->value, len - strlen (insert) - 1);
+            free (node->value);
+            node->value = tmp;
+        } else {
+            /* Should never get here. */
+            adios (NULL, "Unknown action: %d", action);
+        }
+    }
+
+    if ((node = find_contentline (clines, "DTSTAMP", 0))) {
+        const time_t now = time (NULL);
+        struct tm now_tm;
+
+        if (gmtime_r (&now, &now_tm)) {
+            /* 17 would be sufficient given that RFC 5545 § 3.3.4
+               supports only a 4 digit year. */
+            char buf[32];
+
+            if (strftime (buf, sizeof buf, "%Y%m%dT%H%M%SZ", &now_tm)) {
+                free (node->value);
+                node->value = strdup (buf);
+            } else {
+                admonish (NULL, "strftime unable to format current time");
+            }
+        } else {
+            admonish (NULL, "gmtime_r failed on current time");
+        }
+    }
+
+    /* Excise X- lines and VALARM section(s). */
+    in_valarm = 0;
+    for (node = clines; node; node = node->next) {
+        /* node->name will be NULL if the line was deleted. */
+        if (! node->name) { continue; }
+
+        if (in_valarm) {
+            if (! strcasecmp ("END", node->name)  &&
+                ! strcasecmp ("VALARM", node->value)) {
+                in_valarm = 0;
+            }
+            remove_contentline (node);
+        } else {
+            if (! strcasecmp ("BEGIN", node->name)  &&
+                ! strcasecmp ("VALARM", node->value)) {
+                in_valarm = 1;
+                remove_contentline (node);
+            } else if (! strncasecmp ("X-", node->name, 2)) {
+                remove_contentline (node);
+            }
+        }
+    }
+}
+
+/* Echo the input, but with unfolded lines. */
+static void
+dump_unfolded (FILE *file, contentline *clines) {
+    contentline *node;
+
+    for (node = clines; node; node = node->next) {
+        fputs (node->input_line, file);
+    }
+}
+
+static void
+output (FILE *file, contentline *clines, int contenttype) {
+    contentline *node;
+
+    if (contenttype) {
+        /* Generate a Content-Type header to pass the method parameter
+           to mhbuild.  Per RFC 5545 Secs. 6 and 8.1, it must be
+           UTF-8.  But we don't attempt to do any conversion of the
+           input. */
+        if ((node = find_contentline (clines, "METHOD", 0))) {
+            fprintf (file,
+                     "Content-Type: text/calendar; method=\"%s\"; "
+                     "charset=\"UTF-8\"\n\n",
+                     node->value);
+        }
+    }
+
+    for (node = clines; node; node = node->next) {
+        if (node->name) {
+            char *line = NULL;
+            size_t len;
+
+            line = strdup (node->name);
+            line = format_params (line, node->params);
+
+            len = strlen (line);
+            line = mh_xrealloc (line, len + 2);
+            line[len] = ':';
+            line[len + 1] = '\0';
+
+            line = fold (add (node->value, line),
+                         clines->cr_before_lf == CR_BEFORE_LF);
+
+            if (clines->cr_before_lf == LF_ONLY) {
+                fprintf (file, "%s\n", line);
+            } else {
+                fprintf (file, "%s\r\n", line);
+            }
+            free (line);
+        }
+    }
+}
+
+/*
+ * Display these fields of the iCalendar event:
+ *   - method
+ *   - organizer
+ *   - summary
+ *   - description, except for "\n\n" and in VALARM
+ *   - location
+ *   - dtstart in local timezone
+ *   - dtend in local timezone
+ *   - attendees (limited to number specified in initialization)
+ */
+static void
+display (FILE *file, contentline *clines, char *nfs) {
+    tzdesc_t timezones = load_timezones (clines);
+    int in_vtimezone;
+    int in_valarm;
+    contentline *node;
+    struct format *fmt;
+    int dat[5] = { 0, 0, 0, INT_MAX, 0 };
+    struct comp *c;
+    charstring_t buffer = charstring_create (BUFSIZ);
+    charstring_t attendees = charstring_create (BUFSIZ);
+    const unsigned int max_attendees = 20;
+    unsigned int num_attendees;
+
+    /* Don't call on the END:VCALENDAR line. */
+    if (clines  &&  clines->next) {
+      (void) fmt_compile (nfs, &fmt, 1);
+    }
+
+    if ((c = fmt_findcomp ("method"))) {
+        if ((node = find_contentline (clines, "METHOD", 0))  &&  node->value) {
+            c->c_text = strdup (node->value);
+        }
+    }
+
+    if ((c = fmt_findcomp ("organizer"))) {
+        if ((node = find_contentline (clines, "ORGANIZER", 0))  &&
+            node->value) {
+            c->c_text = strdup (identity (node));
+        }
+    }
+
+    if ((c = fmt_findcomp ("summary"))) {
+        if ((node = find_contentline (clines, "SUMMARY", 0))  &&  node->value) {
+            c->c_text = strdup (node->value);
+        }
+    }
+
+    /* Only display DESCRIPTION lines that are outside VALARM section(s). */
+    in_valarm = 0;
+    if ((c = fmt_findcomp ("description"))) {
+        for (node = clines; node; node = node->next) {
+            /* node->name will be NULL if the line was deleted. */
+            if (node->name  &&  node->value  &&  ! in_valarm  &&
+                ! strcasecmp ("DESCRIPTION", node->name)  &&
+                strcasecmp (node->value, "\\n\\n")) {
+                c->c_text = strdup (node->value);
+            } else if (in_valarm) {
+                if (! strcasecmp ("END", node->name)  &&
+                    ! strcasecmp ("VALARM", node->value)) {
+                    in_valarm = 0;
+                }
+            } else {
+                if (! strcasecmp ("BEGIN", node->name)  &&
+                    ! strcasecmp ("VALARM", node->value)) {
+                    in_valarm = 1;
+                }
+            }
+        }
+    }
+
+    if ((c = fmt_findcomp ("location"))) {
+        if ((node = find_contentline (clines, "LOCATION", 0))  &&
+            node->value) {
+            c->c_text = strdup (node->value);
+        }
+    }
+
+    if ((c = fmt_findcomp ("dtstart"))) {
+        /* Find DTSTART outsize of a VTIMEZONE section. */
+        in_vtimezone = 0;
+        for (node = clines; node; node = node->next) {
+            /* node->name will be NULL if the line was deleted. */
+            if (! node->name) { continue; }
+
+            if (in_vtimezone) {
+                if (! strcasecmp ("END", node->name)  &&
+                    ! strcasecmp ("VTIMEZONE", node->value)) {
+                    in_vtimezone = 0;
+                }
+            } else {
+                if (! strcasecmp ("BEGIN", node->name)  &&
+                    ! strcasecmp ("VTIMEZONE", node->value)) {
+                    in_vtimezone = 1;
+                } else if (! strcasecmp ("DTSTART", node->name)) {
+                    /* Got it:  DTSTART outside of a VTIMEZONE section. */
+                    char *datetime = format_datetime (timezones, node);
+                    c->c_text = datetime ? datetime : node->value;
+                }
+            }
+        }
+    }
+
+    if ((c = fmt_findcomp ("dtend"))) {
+        if ((node = find_contentline (clines, "DTEND", 0))  &&  node->value) {
+            char *datetime = format_datetime (timezones, node);
+            c->c_text = datetime ? datetime : node->value;
+        }
+    }
+
+    if ((c = fmt_findcomp ("attendees"))) {
+        /* Call find_contentline () with node as argument to find multiple
+           matching contentlines. */
+        charstring_append_cstring (attendees, "Attendees: ");
+        for (node = clines, num_attendees = 0;
+             (node = find_contentline (node, "ATTENDEE", 0))  &&
+                 num_attendees++ < max_attendees;
+             node = node->next) {
+            const char *id = identity (node);
+
+            if (num_attendees > 1) {
+                charstring_append_cstring (attendees, ", ");
+            }
+            charstring_append_cstring (attendees, id);
+        }
+
+        if (num_attendees >= max_attendees) {
+            unsigned int not_shown = 0;
+
+            for ( ;
+                  (node = find_contentline (node, "ATTENDEE", 0));
+                  node = node->next) {
+                ++not_shown;
+            }
+
+            if (not_shown > 0) {
+                char buf[32];
+
+                (void) snprintf (buf, sizeof buf, ", and %d more", not_shown);
+                charstring_append_cstring (attendees, buf);
+            }
+        }
+
+        if (num_attendees > 0) {
+            c->c_text = charstring_buffer_copy (attendees);
+        }
+    }
+
+    /* Don't call on the END:VCALENDAR line. */
+    if (clines->next) {
+      (void) fmt_scan (fmt, buffer, INT_MAX, dat, NULL);
+      fputs (charstring_buffer (buffer), file);
+      fmt_free (fmt, 1);
+    }
+
+    charstring_free (attendees);
+    charstring_free (buffer);
+    free_timezones (timezones);
+}
+
+static const char *
+identity (const contentline *node) {
+    /* According to RFC 5545 § 3.3.3, an email address in the value
+       must be a mailto URI. */
+    if (! strncasecmp (node->value, "mailto:", 7)) {
+        char *addr;
+        param_list *p;
+
+        for (p = node->params; p && p->param_name; p = p->next) {
+            value_list *v;
+
+            for (v = p->values; v; v = v->next) {
+                if (! strcasecmp (p->param_name, "CN")) {
+                    return v->value;
+                }
+            }
+        }
+
+        /* Did not find a CN parameter, so output the address. */
+        addr = node->value + 7;
+
+        /* Skip any leading whitespace. */
+        for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+        return addr;
+    }
+
+    return "unknown";
+}
+
+static char *
+format_params (char *line, param_list *p) {
+    for ( ; p && p->param_name; p = p->next) {
+        value_list *v;
+        size_t num_values = 0;
+
+        for (v = p->values; v; v = v->next) {
+            if (v->value) { ++num_values; }
+        }
+
+        if (num_values) {
+            size_t len = strlen (line);
+
+            line = mh_xrealloc (line, len + 2);
+            line[len] = ';';
+            line[len + 1] = '\0';
+
+            line = add (p->param_name, line);
+
+            for (v = p->values; v; v = v->next) {
+                len = strlen (line);
+                line = mh_xrealloc (line, len + 2);
+                line[len] = v == p->values ? '=' : ',';
+                line[len + 1] = '\0';
+
+                line = add (v->value, line);
+            }
+        }
+    }
+
+    return line;
+}
+
+static char *
+fold (char *line, int uses_cr) {
+    size_t remaining = strlen (line);
+    size_t current_line_len = 0;
+    charstring_t folded_line = charstring_create (2 * remaining);
+    const char *cp = line;
+
+#ifdef MULTIBYTE_SUPPORT
+    if (mbtowc (NULL, NULL, 0)) {} /* reset shift state */
+#endif
+
+    while (*cp  &&  remaining > 0) {
+#ifdef MULTIBYTE_SUPPORT
+        int char_len = mbtowc (NULL, cp, (size_t) MB_CUR_MAX < remaining
+                                         ? (size_t) MB_CUR_MAX
+                                         : remaining);
+        if (char_len == -1) { char_len = 1; }
+#else
+        const int char_len = 1;
+#endif
+
+        charstring_push_back_chars (folded_line, cp, char_len, 1);
+        remaining -= char_len > 0 ? char_len : 1;
+
+        /* remaining must be > 0 to pass the loop condition above, so
+           if it's not > 1, it is == 1. */
+        if (++current_line_len >= 75) {
+            if (remaining > 1  ||  (*(cp+1) != '\0'  &&  *(cp+1) != '\r'  &&
+                                    *(cp+1) != '\n')) {
+                /* fold */
+                if (uses_cr) { charstring_push_back (folded_line, '\r'); }
+                charstring_push_back (folded_line, '\n');
+                charstring_push_back (folded_line, ' ');
+                current_line_len = 0;
+            }
+        }
+
+        cp += char_len > 0 ? char_len : 1;
+    }
+
+    free (line);
+    line = charstring_buffer_copy (folded_line);
+    charstring_free (folded_line);
+
+    return line;
+}
index a9f03b3efa77c48361f29a90f7439625ce2d60f2..ebe72ff2bbd1cb2d3f8e8c1b4d4a05c7ff55d03a 100644 (file)
@@ -8,9 +8,9 @@
  */
 
 #include <h/mh.h>
+#include <h/mime.h>
 #include <h/utils.h>
 
-
 #define REPL_SWITCHES \
     X("group", 0, GROUPSW) \
     X("nogroup", 0, NGROUPSW) \
@@ -23,6 +23,7 @@
     X("nodraftfolder", 0, NDFLDSW) \
     X("editor editor", 0, EDITRSW) \
     X("noedit", 0, NEDITSW) \
+    X("convertargs type argstring", 0, CONVERTARGSW) \
     X("fcc folder", 0, FCCSW) \
     X("filter filterfile", 0, FILTSW) \
     X("form formfile", 0, FORMSW) \
@@ -109,6 +110,7 @@ static char *fcc    = NULL;         /* folders to add to Fcc: header */
  * prototypes
  */
 static void docc (char *, int);
+static void add_convert_header (const char *, char *, char *, char *);
 
 
 int
@@ -123,6 +125,9 @@ main (int argc, char **argv)
     char *folder = NULL, *msg = NULL, *dfolder = NULL;
     char *dmsg = NULL, *ed = NULL, drft[BUFSIZ], buf[BUFSIZ];
     char **argp, **arguments;
+    svector_t convert_types = svector_create (10);
+    svector_t convert_args = svector_create (10);
+    size_t n;
     struct msgs *mp = NULL;
     struct stat st;
     FILE *in;
@@ -186,6 +191,34 @@ main (int argc, char **argv)
                    nedit++;
                    continue;
                    
+               case CONVERTARGSW: {
+                    char *type;
+                    size_t i;
+
+                   if (!(type = *argp++)) {
+                       adios (NULL, "missing type argument to %s", argp[-2]);
+                    }
+                   if (!(cp = *argp++)) {
+                       adios (NULL, "missing argstring argument to %s",
+                              argp[-3]);
+                    }
+
+                    for (i = 0; i < svector_size (convert_types); ++i) {
+                        if (! strcmp (svector_at (convert_types, i), type)) {
+                            /* Already saw this type, so just update
+                               its args. */
+                            svector_strs (convert_args)[i] = cp;
+                            break;
+                        }
+                    }
+
+                    if (i == svector_size (convert_types)) {
+                        svector_push_back (convert_types, type);
+                        svector_push_back (convert_args, cp);
+                    }
+                   continue;
+                }
+
                case WHATSW: 
                    if (!(whatnowproc = *argp++) || *whatnowproc == '-')
                        adios (NULL, "missing argument to %s", argp[-2]);
@@ -418,10 +451,25 @@ try_it_again:
             fcc, fmtproc);
     fclose (in);
 
+    {
+       char *filename = concat (mp->foldpath, "/", msg, NULL);
+
+        for (n = 0; n < svector_size (convert_types); ++n) {
+            add_convert_header (svector_at (convert_types, n),
+                                svector_at (convert_args, n),
+                                filename, drft);
+        }
+       free (filename);
+    }
+
     if (nwhat)
        done (0);
     what_now (ed, nedit, NOUSE, drft, msg, 0, mp, anot ? "Replied" : NULL,
            inplace, cwd, atfile);
+
+    svector_free (convert_args);
+    svector_free (convert_types);
+
     done (1);
     return 1;
 }
@@ -453,3 +501,25 @@ docc (char *cp, int ccflag)
            break;
     }
 }
+
+/*
+ * Add pseudoheaders that will pass the convert arguments to
+ * mhbuild.  They have the form:
+ *   MHBUILD_FILE_PSEUDOHEADER-text/calendar: /home/user/Mail/inbox/7
+ *   MHBUILD_ARGS_PSEUDOHEADER-text/calendar: reply -accept
+ * The ARGS pseudoheader is optional, but we always add it when
+ * -convertargs is used.
+ */
+void
+add_convert_header (const char *convert_type, char *convert_arg,
+                    char *filename, char *drft) {
+    char *field_name;
+
+    field_name = concat (MHBUILD_FILE_PSEUDOHEADER, convert_type, NULL);
+    annotate (drft, field_name, filename, 1, 0, -2, 1);
+    free (field_name);
+
+    field_name = concat (MHBUILD_ARGS_PSEUDOHEADER, convert_type, NULL);
+    annotate (drft, field_name, convert_arg, 1, 0, -2, 1);
+    free (field_name);
+}