From: David Levine Date: Mon, 19 Jan 2015 03:31:20 +0000 (-0600) Subject: Merge remote-tracking branch 'origin' into convertargs X-Git-Url: https://diplodocus.org/git/nmh/commitdiff_plain/05a5871cd219968464e2c07fbe334891e458e9b3?hp=a70006bbdf676639961877b02a19e9e1f1d0ec78 Merge remote-tracking branch 'origin' into convertargs --- diff --git a/.gitignore b/.gitignore index 4e9297db..1359f96b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/MACHINES b/MACHINES index 1547a4ac..d930dc23 100644 --- 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. diff --git a/Makefile.am b/Makefile.am index af4a2fee..90dee631 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/configure.ac b/configure.ac index 02f0a044..754ccb6a 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index ea275a5b..00000000 --- a/docs/README-iCalendar +++ /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: - where 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=;MAILTO: - where 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 index 00000000..6848dc23 --- /dev/null +++ b/docs/contrib/replaliases @@ -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: diff --git a/docs/pending-release-notes b/docs/pending-release-notes index d1d0d583..efcdd072 100644 --- a/docs/pending-release-notes +++ b/docs/pending-release-notes @@ -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 index 00000000..2ed1bd5c --- /dev/null +++ b/etc/mhical.12hour @@ -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 index 00000000..a12fc0e3 --- /dev/null +++ b/etc/mhical.24hour @@ -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 index 00000000..6abeab38 --- /dev/null +++ b/etc/mhl.replywithoutbody @@ -0,0 +1,5 @@ +; mhl.replywithoutbody +; +; message filter for `repl' (repl -format) that excludes the message body +; +from:nocomponent,formatfield="%(decode(friendly{text})) writes:" diff --git a/etc/mhn.defaults.sh b/etc/mhn.defaults.sh index 2f7ddaf8..bfc7924f 100755 --- a/etc/mhn.defaults.sh +++ b/etc/mhn.defaults.sh @@ -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-[/] 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 <>${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 index 00000000..a1a01228 --- /dev/null +++ b/h/icalendar.h @@ -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 *); diff --git a/h/mime.h b/h/mime.h index 9fcaf5d3..9cac94e5 100644 --- a/h/mime.h +++ b/h/mime.h @@ -12,9 +12,11 @@ #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) \ diff --git a/man/mh-mime.man b/man/mh-mime.man index 51f0607c..7a4bc813 100644 --- a/man/mh-mime.man +++ b/man/mh-mime.man @@ -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 diff --git a/man/mhbuild.man b/man/mhbuild.man index 4b261f33..becf6422 100644 --- a/man/mhbuild.man +++ b/man/mhbuild.man @@ -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 -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 index 00000000..8ee1c52a --- /dev/null +++ b/man/mhical.man @@ -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. diff --git a/man/repl.man b/man/repl.man index e7752226..da8f015c 100644 --- a/man/repl.man +++ b/man/repl.man @@ -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 index 00000000..8b251362 --- /dev/null +++ b/sbr/datetime.c @@ -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 +#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 index 00000000..016c6ebd --- /dev/null +++ b/sbr/icalendar.l @@ -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 + +%% + + +{CRLF} { + /* Eat any leading newlines. */ +} + + +{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; +} + + +{name} { + icallval = strdup (icaltext); + /* yy_push_state (s_name); * s_name */ + BEGIN (s_name); /* s_name */ + return ICAL_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; +} + + +{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; +} + + +{value} { + icallval = strdup (icaltext); + /* yy_pop_state (); * INITIAL */ + /* yy_push_state (s_value); * s_value */ + BEGIN (s_value); /* s_value */ + return ICAL_VALUE; +} + + +{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; +} + + +{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; +} + + +{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; +} + + +{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; +} + + +{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; +} + + +{param-value} { + icallval = strdup (icaltext); + /* yy_pop_state (); * s_name */ + BEGIN (s_name); /* s_name */ + return ICAL_PARAM_VALUE; +} + + +{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; +} + + +{CRLF} { + /* Use start condition to ensure that all newlines are where expected. */ + icallval = icaltext; + /* yy_pop_state (); * INITIAL */ + BEGIN (INITIAL); /* INITIAL */ + return ICAL_CRLF; +} + + +{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); +} + +<> { + 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 <> 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 index 00000000..f2e7b374 --- /dev/null +++ b/sbr/icalparse.y @@ -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 index 00000000..b1146cc1 --- /dev/null +++ b/test/mhical/test-mhical @@ -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" <"$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 \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 index 00000000..e85c9ccd --- /dev/null +++ b/test/repl/test-convert @@ -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 diff --git a/test/repl/test-repl b/test/repl/test-repl index e4856d08..20bb96d7 100755 --- a/test/repl/test-repl +++ b/test/repl/test-repl @@ -35,6 +35,7 @@ Usage: repl: [+folder] [msg] [switches] -nodraftfolder -editor editor -noedit + -convertargs type argstring -fcc folder -filter filterfile -form formfile diff --git a/uip/mhbuildsbr.c b/uip/mhbuildsbr.c index e046886f..2e177c47 100644 --- a/uip/mhbuildsbr.c +++ b/uip/mhbuildsbr.c @@ -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-/ 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-/ 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 index 00000000..eb26c388 --- /dev/null +++ b/uip/mhical.c @@ -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 +#include "h/addrsbr.h" +#include "h/mts.h" +#include "h/utils.h" +#include + +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; +} diff --git a/uip/repl.c b/uip/repl.c index a9f03b3e..ebe72ff2 100644 --- a/uip/repl.c +++ b/uip/repl.c @@ -8,9 +8,9 @@ */ #include +#include #include - #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); +}