event requests.
/ylwrap
/nmh-*.tar.gz
/nmh-*.tar.gz.sig
+/sbr/icalendar.c
+/sbr/icalparse.[hc]
# Removed by maintainer-clean:
/autom4te.cache/
/uip/mark
/uip/mhbuild
/uip/mhfixmsg
+/uip/mhical
/uip/mhl
/uip/mhlist
/uip/mhn
## 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
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 \
##
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 \
## 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 \
## 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/mhl.replywithoutbody \
+ 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 \
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/
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 \
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 \
## 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 \
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)
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 \
+++ /dev/null
-To view an iCalendar (text/calendar, .ics, RFC 5545) attachment with
-mhshow or mhn, all I had to do was install calcurse
-(http://www.calcurse.org/), set it up once, and add this to my
-profile:
-
-#: With mhshow, the following just views the calendar attachment.
-#: With mhn, it inserts it into my personal calcurse calendar.
-#: calcurse must be set up before or at first use. To set up beforehand,
-#: either run it without any arguments and then "qqqy", or:
-#: $ echo qqqy | calcurse
-#: The range of 60 is in days: anything after that range (or before the
-#: current date) won't be shown.
-#: The grep of TZID works around a current (v. 3.1.4) calcurse
-#: deficiency: it doesn't support timezones.
-#: The | cat below allows a sequence of shell commands.
-mhshow-show-text/calendar:
- %lcalcurse -c /dev/null --read-only --import '%F' --range=60 --todo | cat;
- calcurse --gc;
- grep TZID: '%F'
-mhn-show-text/calendar:
- %lcalcurse --import '%F' | cat;
- grep TZID: '%F'
-
-
-To create and send out a calendar request:
- 1) Launch calcurse and create the calendar request.
- 2) Export the calendar request to a file, either using the
- "x" command from within interactive calcurse or using the
- -x/--export command-line option. The output filename
- should have an extension of .ics if you'll use send -attach.
- 3) For compatibility with MS Outlook, edit the calendar
- request file and insert this line into the VCALENDAR
- section:
-METHOD:REQUEST
- and insert this line into the VEVENT section:
-UID:<uid string>
- where <uid string> should be unique.
-
- If you insert the following 5 lines just before the
- END:VEVENT line, it will enable alarms for recipients who
- have support for them:
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:REMINDER
-TRIGGER;RELATED=START:-PT15M
-END:VALARM
- 4) Attach the calendar request file to a message. With
- mhbuild, be sure to specify a Content-Disposition type
- of "inline", using {inline}. The send -attach support
- handles this properly.
-
-
-To respond to a calendar request:
- 1) Store the calendar request in a file using, e.g.,
- mhstore -type text/calendar
- 2) Edit the calendar request, changing the METHOD and your
- ATTENDEE lines to look like:
-METHOD:REPLY
-ATTENDEE;PARTSTAT=<action>;MAILTO:<your email address>
- where <action> is one of ACCEPTED, DECLINED, TENTATIVE,
- or DELEGATED (or COMPLETED or IN-PROCESS for to-do's).
- 3) Send that edited request by attaching it to a reply to
- the meeting requestor; see last step under "To create and
- send out a calendar request" above.
}
+#### 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:
- 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.
-----------------
OBSOLETE FEATURES
--- /dev/null
+%; 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%>\
--- /dev/null
+%; 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%>\
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
#### supported. mhbuild-convert-text/html is defined below.
####
cat <<EOF >>${TMP}
+mhbuild-convert-text/calendar: mhical -infile %F -contenttype
mhbuild-convert-text: charset=%{charset}; iconv -f \${charset:-us-ascii} -t utf-8 %F${replfmt}
mhbuild-disposition-text/calendar: inline
mhbuild-disposition-message/rfc822: inline
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
--- /dev/null
+/*
+ * 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 *);
--- /dev/null
+.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.
--- /dev/null
+/*
+ * datetime.c -- functions for manipulating RFC 5545 date-time values
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include <h/fmt_scan.h>
+#include "h/tws.h"
+#include "h/utils.h"
+
+/*
+ * This doesn't try to support all of the myriad date-time formats
+ * allowed by RFC 5545. It is only used for viewing date-times,
+ * so that shouldn't be a problem: if a particular format can't
+ * be handled by this code, just present it to the user in its
+ * original form.
+ *
+ * And, this assumes a valid iCalendar input file. E.g, it
+ * doesn't check that each BEGIN has a matching END and vice
+ * versa. That should be done in the parser, though it currently
+ * isn't.
+ */
+
+typedef struct tzparams {
+ /* Pointers to values in parse tree.
+ * TZOFFSETFROM is used to calculate the absolute time at which
+ * the transition to a given observance takes place.
+ * TZOFFSETTO is the timezone offset from UTC. Both are in HHmm
+ * format. */
+ char *offsetfrom, *offsetto;
+ const char *dtstart;
+ const char *rrule;
+
+ /* This is only used to make sure that timezone applies. And not
+ always, because if the timezone DTSTART is before the epoch, we
+ don't try to compare to it. */
+ time_t start_dt; /* in seconds since epoch */
+} tzparams;
+
+struct tzdesc {
+ char *tzid;
+
+ /* The following are translations of the pieces of RRULE and DTSTART
+ into seconds from beginning of year. */
+ tzparams standard_params;
+ tzparams daylight_params;
+
+ struct tzdesc *next;
+};
+
+/*
+ * Parse a datetime of the form YYYYMMDDThhmmss and a string
+ * representation of the timezone in units of [+-]hhmm and load the
+ * struct tws.
+ */
+static int
+parse_datetime (const char *datetime, const char *zone, int dst,
+ struct tws *tws) {
+ char utc_indicator;
+ int form_1 = 0;
+ int items_matched =
+ sscanf (datetime, "%4d%2d%2dT%2d%2d%2d%c",
+ &tws->tw_year, &tws->tw_mon, &tws->tw_mday,
+ &tws->tw_hour, &tws->tw_min, &tws->tw_sec,
+ &utc_indicator);
+ tws->tw_flags = TW_NULL;
+
+ if (items_matched == 7) {
+ /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
+ if (utc_indicator != 'Z') {
+ advise (NULL, "%s has invalid timezone indicator of 0x%x",
+ datetime, utc_indicator);
+ return NOTOK;
+ }
+ } else if (zone == NULL) {
+ form_1 = 1;
+ }
+
+ if (items_matched >= 6) {
+ int offset = atoi (zone ? zone : "0");
+
+ /* struct tws defines tw_mon over [0, 11]. */
+ --tws->tw_mon;
+
+ set_dotw (tws);
+ /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
+ dasctime() outputs the dotw before the date instead of after. */
+ tws->tw_flags &= ~TW_SDAY, tws->tw_flags |= TW_SEXP;
+
+ /* For the call to dmktime():
+ - don't need tw_yday
+ - tw_clock must be 0 on entry, and is set by dmktime()
+ - the only flag in tw_flags used is TW_DST
+ */
+ tws->tw_yday = tws->tw_clock = 0;
+ tws->tw_zone = 60 * (offset / 100) + offset % 100;
+ if (dst) {
+ tws->tw_zone -= 60; /* per dlocaltime() */
+ tws->tw_flags |= TW_DST;
+ }
+ /* dmktime() just sets tws->tw_clock. */
+ (void) dmktime (tws);
+
+ if (! form_1) {
+ /* Set TW_SZEXP so that dasctime outputs timezone, except
+ with local time (Form #1). */
+ tws->tw_flags |= TW_SZEXP;
+
+ /* Convert UTC time to time in local timezone. However,
+ don't try for years before 1970 because dlocatime()
+ doesn't handle them well. dlocaltime() will succeed if
+ tws->tw_clock is nonzero. */
+ if (tws->tw_year >= 1970 && tws->tw_clock > 0) {
+ const int was_dst = tws->tw_flags & TW_DST;
+
+ *tws = *dlocaltime (&tws->tw_clock);
+ if (was_dst && ! (tws->tw_flags & TW_DST)) {
+ /* dlocaltime() changed the DST flag from 1 to 0,
+ which means the time is in the hour (assumed to
+ be one hour) that is lost in the transition to
+ DST. So per RFC 5545 Sec. 3.3.5, "the
+ DATE-TIME value is interpreted using the UTC
+ offset before the gap in local times." In
+ other words, add an hour to it.
+ No adjustment is necessary for the transition
+ from DST to standard time, because dasctime()
+ shows the first occurrence of the time. */
+ tws->tw_clock += 3600;
+ *tws = *dlocaltime (&tws->tw_clock);
+ }
+ }
+ }
+
+ return OK;
+ } else {
+ return NOTOK;
+ }
+}
+
+tzdesc_t
+load_timezones (const contentline *clines) {
+ tzdesc_t timezones = NULL, timezone = NULL;
+ int in_vtimezone, in_standard, in_daylight;
+ tzparams *params = NULL;
+ const contentline *node;
+
+ /* Interpret each VTIMEZONE section. */
+ in_vtimezone = in_standard = in_daylight = 0;
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was "deleted". */
+ if (! node->name) { continue; }
+
+ if (in_daylight || in_standard) {
+ if (! strcasecmp ("END", node->name) &&
+ ((in_standard && ! strcasecmp ("STANDARD", node->value)) ||
+ (in_daylight && ! strcasecmp ("DAYLIGHT", node->value)))) {
+ struct tws tws;
+
+ if (in_standard) { in_standard = 0; }
+ else if (in_daylight) { in_daylight = 0; }
+ if (parse_datetime (params->dtstart, params->offsetfrom,
+ in_daylight ? 1 : 0,
+ &tws) == OK) {
+ if (tws.tw_year >= 1970) {
+ /* dmktime() falls apart for, e.g., the year 1601. */
+ params->start_dt = tws.tw_clock;
+ }
+ } else {
+ advise (NULL, "failed to parse start time %s for %s",
+ params->dtstart,
+ in_standard ? "standard" : "daylight");
+ return NULL;
+ }
+ params = NULL;
+ } else if (! strcasecmp ("DTSTART", node->name)) {
+ /* Save DTSTART for use after getting TZOFFSETFROM. */
+ params->dtstart = node->value;
+ } else if (! strcasecmp ("TZOFFSETFROM", node->name)) {
+ params->offsetfrom = node->value;
+ } else if (! strcasecmp ("TZOFFSETTO", node->name)) {
+ params->offsetto = node->value;
+ } else if (! strcasecmp ("RRULE", node->name)) {
+ params->rrule = node->value;
+ }
+ } else if (in_vtimezone) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+ in_vtimezone = 0;
+ } else if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("STANDARD", node->value)) {
+ in_standard = 1;
+ params = &timezone->standard_params;
+ } else if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("DAYLIGHT", node->value)) {
+ in_daylight = 1;
+ params = &timezone->daylight_params;
+ } else if (! strcasecmp ("TZID", node->name)) {
+ timezone->tzid = strdup (node->value);
+ }
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+
+ in_vtimezone = 1;
+ timezone = mh_xcalloc (1, sizeof (struct tzdesc));
+ if (timezones) {
+ tzdesc_t t;
+
+ for (t = timezones; t && t->next; t = t->next) { continue; }
+ /* The loop terminated at, not after, the last
+ timezones node. */
+ t->next = timezone;
+ } else {
+ timezones = timezone;
+ }
+ }
+ }
+ }
+
+ return timezones;
+}
+
+void
+free_timezones (tzdesc_t timezone) {
+ tzdesc_t next;
+
+ for ( ; timezone; timezone = next) {
+ free (timezone->tzid);
+ next = timezone->next;
+ free (timezone);
+ }
+}
+
+/*
+ * Convert time to local timezone, accounting for daylight saving time:
+ * - Detect which type of datetime the node contains:
+ * Form #1: DATE WITH LOCAL TIME
+ * Form #2: DATE WITH UTC TIME
+ * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
+ * - Convert value to local time in seconds since epoch.
+ * - If there's a DST in the timezone, convert its start and end
+ * date-times to local time in seconds, also. Then determine
+ * if the value is between them, and therefore DST. Otherwise, it's
+ * not.
+ * - Format the time value.
+ */
+
+/*
+ * Given a recurrence rule and year, calculate its time in seconds
+ * from 01 January UTC of the year.
+ */
+time_t
+rrule_clock (const char *rrule, const char *starttime, const char *zone,
+ unsigned int year) {
+ time_t clock = 0;
+
+ if (nmh_strcasestr (rrule, "FREQ=YEARLY;INTERVAL=1")) {
+ struct tws *tws;
+ const char *cp;
+ int wday = -1, month = -1;
+ int specific_day = 1; /* BYDAY integer (prefix) */
+ char buf[32];
+ int day;
+
+ if ((cp = nmh_strcasestr (rrule, "BYDAY="))) {
+ cp += 6;
+ /* BYDAY integers must be ASCII. */
+ if (*cp == '+') { ++cp; } /* +n specific day; don't support '-' */
+ else if (*cp == '-') { goto fail; }
+
+ if (isdigit ((unsigned char) *cp)) { specific_day = *cp++ - 0x30; }
+
+ if (! strncasecmp (cp, "SU", 2)) { wday = 0; }
+ else if (! strncasecmp (cp, "MO", 2)) { wday = 1; }
+ else if (! strncasecmp (cp, "TU", 2)) { wday = 2; }
+ else if (! strncasecmp (cp, "WE", 2)) { wday = 3; }
+ else if (! strncasecmp (cp, "TH", 2)) { wday = 4; }
+ else if (! strncasecmp (cp, "FR", 2)) { wday = 5; }
+ else if (! strncasecmp (cp, "SA", 2)) { wday = 6; }
+ }
+ if ((cp = nmh_strcasestr (rrule, "BYMONTH="))) {
+ month = atoi (cp + 8);
+ }
+
+ for (day = 1; day <= 7; ++day) {
+ /* E.g, 11-01-2014 02:00:00-0400 */
+ snprintf (buf, sizeof buf, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
+ month, day + 7 * (specific_day-1), year,
+ starttime, starttime + 2, starttime + 4,
+ zone ? zone : "0000");
+ if ((tws = dparsetime (buf))) {
+ if (! (tws->tw_flags & (TW_SEXP|TW_SIMP))) { set_dotw (tws); }
+
+ if (tws->tw_wday == wday) {
+ /* Found the day specified in the RRULE. */
+ break;
+ }
+ }
+ }
+
+ if (day <= 7) {
+ clock = tws->tw_clock;
+ }
+ }
+
+fail:
+ if (clock == 0) {
+ admonish (NULL,
+ "Unsupported RRULE format: %s, assume local timezone",
+ rrule);
+ }
+
+ return clock;
+}
+
+char *
+format_datetime (tzdesc_t timezones, const contentline *node) {
+ param_list *p;
+ char *dt_timezone = NULL;
+ int dst = 0;
+ struct tws tws[2]; /* [standard, daylight] */
+ tzdesc_t tz;
+ char *tp_std, *tp_dst, *tp_dt;
+
+ /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
+ for (p = node->params; p && p->param_name; p = p->next) {
+ if (! strcasecmp (p->param_name, "TZID") && p->values) {
+ dt_timezone = p->values->value;
+ break;
+ }
+ }
+
+ if (! dt_timezone) {
+ /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
+ Form #2: DATE WITH UTC TIME */
+ if (parse_datetime (node->value, NULL, 0, &tws[0]) == OK) {
+ return strdup (dasctime (&tws[0], 0));
+ } else {
+ advise (NULL, "unable to parse datetime %s", node->value);
+ return NULL;
+ }
+ }
+
+ /*
+ * must be
+ * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
+ */
+
+ /* Find the corresponding tzdesc. */
+ for (tz = timezones; dt_timezone && tz; tz = tz->next) {
+ /* Property parameter values are case insenstive (RFC 5545
+ Sec. 2) and time zone identifiers are property parameters
+ (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
+ different case in the same file for identifiers that are
+ supposed to be the same. */
+ if (tz->tzid && ! strcasecmp (dt_timezone, tz->tzid)) { break; }
+ }
+
+ if (! tz) {
+ advise (NULL, "did not find VTIMEZONE section for %s", dt_timezone);
+ return NULL;
+ }
+
+ /* Determine if it's Daylight Saving. */
+ tp_std = strchr (tz->standard_params.dtstart, 'T');
+ tp_dt = strchr (node->value, 'T');
+
+ if (tz->daylight_params.dtstart) {
+ tp_dst = strchr (tz->daylight_params.dtstart, 'T');
+ } else {
+ /* No DAYLIGHT section. */
+ tp_dst = NULL;
+ dst = 0;
+ }
+
+ if (tp_std && tp_dt) {
+ time_t transition[2] = { 0, 0 }; /* [standard, daylight] */
+ time_t dt[2]; /* [standard, daylight] */
+ unsigned int year;
+ char buf[5];
+
+ /* Datetime is form YYYYMMDDThhmmss. Extract year. */
+ memcpy (buf, node->value, sizeof buf - 1);
+ buf[sizeof buf - 1] = '\0';
+ year = atoi (buf);
+
+ if (tz->standard_params.rrule) {
+ /* +1 to skip the T before the time */
+ transition[0] =
+ rrule_clock (tz->standard_params.rrule, tp_std + 1,
+ tz->standard_params.offsetfrom, year);
+ }
+ if (tp_dst && tz->daylight_params.rrule) {
+ /* +1 to skip the T before the time */
+ transition[1] =
+ rrule_clock (tz->daylight_params.rrule, tp_dst + 1,
+ tz->daylight_params.offsetfrom, year);
+ }
+
+ if (transition[0] < transition[1]) {
+ advise (NULL, "format_datetime() requires that daylight "
+ "saving time transition precede standard time "
+ "transition");
+ return NULL;
+ }
+
+ if (parse_datetime (node->value, tz->standard_params.offsetto,
+ 0, &tws[0]) == OK) {
+ dt[0] = tws[0].tw_clock;
+ } else {
+ advise (NULL, "unable to parse datetime %s", node->value);
+ return NULL;
+ }
+
+ if (tp_dst) {
+ if (dt[0] < transition[1]) {
+ dst = 0;
+ } else {
+ if (parse_datetime (node->value,
+ tz->daylight_params.offsetto, 1,
+ &tws[1]) == OK) {
+ dt[1] = tws[1].tw_clock;
+ } else {
+ advise (NULL, "unable to parse datetime %s",
+ node->value);
+ return NULL;
+ }
+
+ dst = dt[1] > transition[0] ? 0 : 1;
+ }
+ }
+
+ if (dst) {
+ if (tz->daylight_params.start_dt > 0 &&
+ dt[dst] < tz->daylight_params.start_dt) {
+ advise (NULL, "date-time of %s is before VTIMEZONE start "
+ "of %s", node->value,
+ tz->daylight_params.dtstart);
+ return NULL;
+ }
+ } else {
+ if (tz->standard_params.start_dt > 0 &&
+ dt[dst] < tz->standard_params.start_dt) {
+ advise (NULL, "date-time of %s is before VTIMEZONE start "
+ "of %s", node->value,
+ tz->standard_params.dtstart);
+ return NULL;
+ }
+ }
+ } else {
+ if (! tp_std) {
+ advise (NULL, "unsupported date-time format: %s",
+ tz->standard_params.dtstart);
+ return NULL;
+ }
+ if (! tp_dt) {
+ advise (NULL, "unsupported date-time format: %s", node->value);
+ return NULL;
+ }
+ }
+
+ return strdup (dasctime (&tws[dst], 0));
+}
--- /dev/null
+/*
+ * icalendar.l -- icalendar (RFC 5545) scanner
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh. See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+/* See porting notes at end of this file. */
+
+%{
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "sbr/icalparse.h"
+
+static char *unfold (char *, size_t *);
+static void destroy_icallex ();
+%}
+
+/*
+ * These flex options aren't used:
+ * 8bit not needed
+ * case-insensitive not needed
+ * align not used because this isn't performance critical
+ */
+%option outfile="lex.yy.c" prefix="ical"
+%option perf-report warn
+%option never-interactive noinput noyywrap
+
+ /*
+ * From RFC 5545 § 3.1.
+ */
+name {iana-token}|{x-name}
+iana-token ({ALPHA}|{DIGIT}|-)+
+x-name X-({vendorid}-)?({ALPHA}|{DIGIT}|-)+
+vendorid ({ALPHA}|{DIGIT}){3,}
+param-name {iana-token}|{x-name}
+param-value {paramtext}|{quoted-string}
+paramtext {SAFE-CHAR}*
+value {VALUE-CHAR}*
+quoted-string {DQUOTE}{QSAFE-CHAR}*{DQUOTE}
+QSAFE-CHAR {WSP}|[\x21\x23-\x7E]|{NON-US-ASCII}
+SAFE-CHAR {WSP}|[\x21\x23-\x2B\x2D-\x39\x3C-\x7E]|{NON-US-ASCII}
+VALUE-CHAR {WSP}|[\x21-\x7E]|{NON-US-ASCII}
+ /* The following is a short-cut definition that admits more
+ that the UNICODE characters permitted by RFC 5545. */
+NON-US-ASCII [\x80-\xF8]{2,4}
+ /* The following excludes HTAB, unlike {CTL}. */
+CONTROL [\x00-\x08\x0A-\x1F\x7F]
+EQUAL =
+ /* Solaris lex requires that the , be escaped. */
+COMMA \,
+ /*
+ * From RFC 5545 § 2.1.
+ */
+COLON :
+SEMICOLON ;
+
+ /*
+ * From RFC 5545 § 3.3.11.
+ */
+text ({TSAFE-CHAR}|:|{DQUOTE}|{ESCAPED-CHAR})*
+ESCAPED-CHAR \\\\|\\;|\\,|\\N|\\n
+TSAFE-CHAR {WSP}|[\x21\x23-\x2B\x2D-\x39\x3C-\x5B\x5D-\x7E]|{NON-US-ASCII|
+
+ /*
+ * Core rules (definitions) from RFC 5234 Appendix B.1.
+ */
+ALPHA [\x41-\x5A\x61-\x7A]
+BIT [01]
+CHAR [\x01-\x7F]
+CR \x0D
+ /* Variance from RFC 5234: the {CR} is required in
+ CRLF, but it is optional below to support Unix
+ filesystem convention. */
+CRLF ({CR}?{LF})+
+CTL [\x00-\x1F\x7F]
+DIGIT [\x30-\x39]
+DQUOTE \x22
+HEXDIG {DIGIT}|[A-F]
+HTAB \x09
+LF \x0A
+LWSP ({WSP}|({CRLF}{WSP}))*
+OCTET [\x00-\xFF]
+SP \x20
+VCHAR [\x21-\x7E]
+WSP {SP}|{HTAB}
+
+/*
+ * Our definitions.
+ */
+fold {CRLF}{WSP}
+folded-name {name}({fold}+{iana-token})+
+folded-param-name {param-name}({fold}+{iana-token})+
+folded-quoted-string {DQUOTE}{QSAFE-CHAR}*{fold}+{QSAFE-CHAR}*{DQUOTE}
+folded-param-value {paramtext}({fold}{paramtext}*)+|{folded-quoted-string}
+folded-value {VALUE-CHAR}*({fold}{VALUE-CHAR}*)+
+
+%s s_name s_colon s_value s_semicolon s_param_name s_equal s_comma
+
+%%
+
+<INITIAL>
+{CRLF} {
+ /* Eat any leading newlines. */
+}
+
+<INITIAL>
+{folded-name} {
+ /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+ so copy it. */
+ size_t len = icalleng;
+ unfold (icaltext, &len);
+ icalleng = len;
+
+ icallval = strdup (icaltext);
+ /* yy_push_state (s_name); * s_name */
+ BEGIN (s_name); /* s_name */
+ return ICAL_NAME;
+}
+
+<INITIAL>
+{name} {
+ icallval = strdup (icaltext);
+ /* yy_push_state (s_name); * s_name */
+ BEGIN (s_name); /* s_name */
+ return ICAL_NAME;
+}
+
+<s_name>
+{COLON} {
+ /* Don't need to strdup a single character. */
+ icallval = icaltext;
+ /* yy_pop_state (); * INITIAL */
+ /* yy_push_state (s_colon); * s_colon */
+ BEGIN (s_colon); /* s_colon */
+ return ICAL_COLON;
+}
+
+<s_colon>
+{folded-value} {
+ /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+ so copy it. */
+ size_t len = icalleng;
+ unfold (icaltext, &len);
+ icalleng = len;
+
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * INITIAL */
+ /* yy_push_state (s_value); * s_value */
+ BEGIN (s_value); /* s_value */
+ return ICAL_VALUE;
+}
+
+<s_colon>
+{value} {
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * INITIAL */
+ /* yy_push_state (s_value); * s_value */
+ BEGIN (s_value); /* s_value */
+ return ICAL_VALUE;
+}
+
+<s_name>
+{SEMICOLON} {
+ /* Don't need to strdup a single character. */
+ icallval = icaltext;
+ /* yy_push_state (s_semicolon); * s_name, s_semicolon */
+ BEGIN (s_semicolon); /* s_name, s_semicolon */
+ return ICAL_SEMICOLON;
+}
+
+<s_semicolon>
+{folded-param-name} {
+ /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+ so copy it. */
+ size_t len = icalleng;
+ unfold (icaltext, &len);
+ icalleng = len;
+
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * s_name */
+ /* yy_push_state (s_param_name); * s_name, s_param_name */
+ BEGIN (s_param_name); /* s_name, s_param_name */
+ return ICAL_PARAM_NAME;
+}
+
+<s_semicolon>
+{param-name} {
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * s_name */
+ /* yy_push_state (s_param_name); * s_name, s_param_name */
+ BEGIN (s_param_name); /* s_name, s_param_name */
+ return ICAL_PARAM_NAME;
+}
+
+<s_param_name>
+{EQUAL} {
+ /* Don't need to strdup a single character. */
+ icallval = icaltext;
+ /* yy_pop_state (); * s_name */
+ /* yy_push_state (s_equal); * s_name, s_equal */
+ BEGIN (s_equal); /* s_name, s_equal */
+ return ICAL_EQUAL;
+}
+
+<s_equal,s_comma>
+{folded-param-value} {
+ /* flex 2.5.4 defines icalleng as an int instead of a size_t,
+ so copy it. */
+ size_t len = icalleng;
+ unfold (icaltext, &len);
+ icalleng = len;
+
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * s_name */
+ BEGIN (s_name); /* s_name */
+ return ICAL_PARAM_VALUE;
+}
+
+<s_equal,s_comma>
+{param-value} {
+ icallval = strdup (icaltext);
+ /* yy_pop_state (); * s_name */
+ BEGIN (s_name); /* s_name */
+ return ICAL_PARAM_VALUE;
+}
+
+<s_name>
+{COMMA} {
+ /* Don't need to strdup a single character. */
+ icallval = icaltext;
+ /* yy_push_state (s_comma); * s_name, s_comma */
+ BEGIN (s_comma); /* s_name, s_comma */
+ return ICAL_COMMA;
+}
+
+<s_value>
+{CRLF} {
+ /* Use start condition to ensure that all newlines are where expected. */
+ icallval = icaltext;
+ /* yy_pop_state (); * INITIAL */
+ BEGIN (INITIAL); /* INITIAL */
+ return ICAL_CRLF;
+}
+
+<s_colon>
+{CRLF} {
+ /* Null value. */
+ icallval = strdup ("");
+ /* yy_pop_state (); * INITIAL */
+ /* yy_push_state (s_value); * s_value */
+ BEGIN (s_value); /* s_value */
+ /* Push the newline back so it can be handled in the proper state. */
+ unput ('\n');
+ return ICAL_VALUE;
+}
+
+. {
+ /* By default, flex will just pass unmatched text. Catch it instead. */
+ advise (NULL, "unexpected input: |%s|\n", icaltext);
+}
+
+<<EOF>> {
+ destroy_icallex ();
+ yyterminate ();
+}
+
+%%
+
+static char *
+unfold (char *text, size_t *leng) {
+ /* It's legal to shorten text and modify leng (because we don't
+ use yymore()). */
+ char *cp;
+
+ /* First squash any CR-LF-WSP sequences. */
+ while ((cp = strstr (text, "\r\n ")) || (cp = strstr (text, "\r\n\t"))) {
+ /* Subtract any characters prior to fold sequence and 3 for
+ the fold sequence, and add 1 for the terminating null. */
+ (void) memmove (cp, cp + 3, *leng - (cp - text) - 3 + 1);
+ *leng -= 3;
+ }
+
+ /* Then squash any LF-WSP sequences. */
+ while ((cp = strstr (text, "\n ")) || (cp = strstr (text, "\n\t"))) {
+ /* Subtract any characters prior to fold sequence and 2 for
+ the fold sequence, and add 1 for the terminating null. */
+ (void) memmove (cp, cp + 2, *leng - (cp - text) - 2 + 1);
+ *leng -= 2;
+ }
+
+ return text;
+}
+
+
+/*
+ * To clean up memory, call the function provided by modern
+ * versions of flex. Older versions don't have it, and of
+ * course this won't do anything if the scanner was built
+ * with something other than flex.
+ */
+static void
+destroy_icallex () {
+#if defined FLEX_SCANNER && defined YY_FLEX_SUBMINOR_VERSION
+ /* Hack: rely on fact that the the YY_FLEX_SUBMINOR_VERSION
+ #define was added to flex (flex.skl v. 2.163) after
+ #yylex_destroy() was added. */
+ icallex_destroy ();
+#endif /* FLEX_SCANNER && YY_CURRENT_BUFFER_LVALUE */
+}
+
+/*
+ * See comment in h/icalendar.h about having to provide these
+ * because flex 2.5.4 doesn't.
+ */
+void
+icalset_inputfile (FILE *file) {
+ yyin = file;
+}
+
+void
+icalset_outputfile (FILE *file) {
+ yyout = file;
+}
+
+/*
+ * Porting notes
+ * -------------
+ * POSIX lex only supports an entry point name of yylex(). nmh
+ * programs can contain multiple scanners (see sbr/dtimep.l), so
+ * nmh requires the use of flex to build them.
+ * In addition, if there is a need to port this to Solaris lex:
+ * - Use the lex -e or -w option.
+ * - Comment out all of the %options.
+ * - Comment out the <<EOF>> rule.
+ * - The start condition and pattern must be on the same line.
+ * - Comments must be inside rules, not just before them.
+ * - Don't use start condition stack. In the code, above BEGIN's are
+ * used instead, and the contents of an imaginary start condition
+ * stack are shown after each. The stack operations are also shown
+ * in comments.
+ */
--- /dev/null
+/*
+ * 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 */
showproc: ${MH_LIBEXEC_DIR}/mhl
EOF
- for f in MailAliases components digestcomps distcomps forwcomps mhl.body \
- mhl.digest mhl.format mhl.forward mhl.headers mhl.reply \
- mhl.replywithoutbody mhn.defaults rcvdistcomps replcomps \
- replgroupcomps scan.MMDDYY scan.YYYYMMDD scan.curses scan.default \
+ for f in MailAliases components digestcomps distcomps forwcomps \
+ mhical.12hour mhical.24hour mhl.body mhl.digest mhl.format \
+ mhl.forward mhl.headers mhl.reply mhl.replywithoutbody \
+ mhn.defaults rcvdistcomps replcomps replgroupcomps \
+ scan.MMDDYY scan.YYYYMMDD scan.curses scan.default \
scan.highlighted scan.mailx scan.nomime scan.size scan.time \
scan.timely scan.unseen
do
--- /dev/null
+/*
+ * mhical.c -- operate on an iCalendar request
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "sbr/icalparse.h"
+#include <h/fmt_scan.h>
+#include "h/addrsbr.h"
+#include "h/mts.h"
+#include "h/utils.h"
+#include <time.h>
+
+typedef enum act {
+ ACT_NONE,
+ ACT_ACCEPT,
+ ACE_DECLINE,
+ ACT_TENTATIVE,
+ ACT_DELEGATE,
+ ACT_CANCEL
+} act;
+
+static void convert_to_reply (contentline *, act);
+static void convert_to_cancellation (contentline *);
+static void convert_common (contentline *, act);
+static void dump_unfolded (FILE *, contentline *);
+static void output (FILE *, contentline *, int);
+static void display (FILE *, contentline *, char *);
+static const char *identity (const contentline *);
+static char *format_params (char *, param_list *);
+static char *fold (char *, int);
+
+#define MHICAL_SWITCHES \
+ X("reply accept|decline|tentative", 0, REPLYSW) \
+ X("cancel", 0, CANCELSW) \
+ X("form formatfile", 0, FORMSW) \
+ X("format string", 5, FMTSW) \
+ X("infile", 0, INFILESW) \
+ X("outfile", 0, OUTFILESW) \
+ X("contenttype", 0, CONTENTTYPESW) \
+ X("nocontenttype", 0, NCONTENTTYPESW) \
+ X("unfold", 0, UNFOLDSW) \
+ X("debug", 0, DEBUGSW) \
+ X("version", 0, VERSIONSW) \
+ X("help", 0, HELPSW) \
+
+#define X(sw, minchars, id) id,
+DEFINE_SWITCH_ENUM(MHICAL);
+#undef X
+
+#define X(sw, minchars, id) { sw, minchars, id },
+DEFINE_SWITCH_ARRAY(MHICAL, switches);
+#undef X
+
+vevent vevents = { NULL, NULL, NULL};
+
+int
+main (int argc, char *argv[]) {
+ /* RFC 5322 § 3.3 date-time format, including the optional
+ day-of-week and not including the optional seconds. The
+ zone is required by the RFC but not always output by this
+ format, because RFC 5545 § 3.3.5 allows date-times not
+ bound to any time zone. */
+
+ act action = ACT_NONE;
+ char *infile = NULL, *outfile = NULL;
+ FILE *inputfile = NULL, *outputfile = NULL;
+ int contenttype = 0, unfold = 0;
+ vevent *v, *nextvevent;
+ char *form = "mhical.24hour", *format = NULL;
+ char **argp, **arguments, *cp;
+
+ icaldebug = 0; /* Global provided by bison (with name-prefix "ical"). */
+
+ if (nmh_init(argv[0], 1)) { return 1; }
+
+ arguments = getarguments (invo_name, argc, argv, 1);
+ argp = arguments;
+
+ /*
+ * Parse arguments
+ */
+ while ((cp = *argp++)) {
+ if (*cp == '-') {
+ switch (smatch (++cp, switches)) {
+ case AMBIGSW:
+ ambigsw (cp, switches);
+ done (1);
+ case UNKWNSW:
+ adios (NULL, "-%s unknown", cp);
+
+ case HELPSW: {
+ char buf[128];
+ snprintf (buf, sizeof buf, "%s [switches]", invo_name);
+ print_help (buf, switches, 1);
+ done (0);
+ }
+ case VERSIONSW:
+ print_version(invo_name);
+ done (0);
+ case DEBUGSW:
+ icaldebug = 1;
+ continue;
+
+ case REPLYSW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ if (! strcasecmp (cp, "accept")) {
+ action = ACT_ACCEPT;
+ } else if (! strcasecmp (cp, "decline")) {
+ action = ACE_DECLINE;
+ } else if (! strcasecmp (cp, "tentative")) {
+ action = ACT_TENTATIVE;
+ } else if (! strcasecmp (cp, "delegate")) {
+ action = ACT_DELEGATE;
+ } else {
+ adios (NULL, "Unknown action: %s", cp);
+ }
+ continue;
+
+ case CANCELSW:
+ action = ACT_CANCEL;
+ continue;
+
+ case FORMSW:
+ if (! (form = *argp++) || *form == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+ format = NULL;
+ continue;
+ case FMTSW:
+ if (! (format = *argp++) || *format == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+ form = NULL;
+ continue;
+
+ case INFILESW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ infile = *cp == '-' ? add (cp, NULL) : path (cp, TFILE);
+ continue;
+ case OUTFILESW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ outfile = *cp == '-' ? add (cp, NULL) : path (cp, TFILE);
+ continue;
+
+ case CONTENTTYPESW:
+ contenttype = 1;
+ continue;
+ case NCONTENTTYPESW:
+ contenttype = 0;
+ continue;
+
+ case UNFOLDSW:
+ unfold = 1;
+ continue;
+ }
+ }
+ }
+
+ free (arguments);
+
+ if (infile) {
+ if ((inputfile = fopen (infile, "r"))) {
+ icalset_inputfile (inputfile);
+ } else {
+ adios (infile, "error opening");
+ }
+ } else {
+ inputfile = stdin;
+ }
+
+ if (outfile) {
+ if ((outputfile = fopen (outfile, "w"))) {
+ icalset_outputfile (outputfile);
+ } else {
+ adios (outfile, "error opening");
+ }
+ } else {
+ outputfile = stdout;
+ }
+
+ vevents.last = &vevents;
+ /* vevents is accessed by parser as global. */
+ icalparse ();
+
+ for (v = &vevents; v; v = nextvevent) {
+ if (! unfold && v != &vevents && v->contentlines &&
+ v->contentlines->name &&
+ strcasecmp (v->contentlines->name, "END") &&
+ v->contentlines->value &&
+ strcasecmp (v->contentlines->value, "VCALENDAR")) {
+ /* Output blank line between vevents. Not before
+ first vevent and not after last. */
+ putc ('\n', outputfile);
+ }
+
+ if (action == ACT_NONE) {
+ if (unfold) {
+ dump_unfolded (outputfile, v->contentlines);
+ } else {
+ char *nfs = new_fs (form, format, NULL);
+
+ display (outputfile, v->contentlines, nfs);
+ free_fs ();
+ }
+ } else {
+ if (action == ACT_CANCEL) {
+ convert_to_cancellation (v->contentlines);
+ } else {
+ convert_to_reply (v->contentlines, action);
+ }
+ output (outputfile, v->contentlines, contenttype);
+ }
+
+ free_contentlines (v->contentlines);
+ nextvevent = v->next;
+ if (v != &vevents) {
+ free (v);
+ }
+ }
+
+ if (infile) {
+ if (fclose (inputfile) != 0) {
+ advise (infile, "error closing");
+ }
+ free (infile);
+ }
+ if (outfile) {
+ if (fclose (outputfile) != 0) {
+ advise (outfile, "error closing");
+ }
+ free (outfile);
+ }
+
+ return 0;
+}
+
+/*
+ * - Change METHOD from REQUEST to REPLY.
+ * - Change PRODID.
+ * - Remove all ATTENDEE lines for other users (based on ismymbox ()).
+ * - For the user's ATTENDEE line:
+ * - Remove ROLE and RSVP parameters.
+ * - Change PARTSTAT value to indicate reply action, e.g., ACCEPTED,
+ * DECLINED, or TENTATIVE.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Remove all DESCRIPTION lines.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_reply (contentline *clines, act action) {
+ char *partstat = NULL;
+ int found_my_attendee_line = 0;
+ contentline *node;
+
+ convert_common (clines, action);
+
+ switch (action) {
+ case ACT_ACCEPT:
+ partstat = "ACCEPTED";
+ break;
+ case ACE_DECLINE:
+ partstat = "DECLINED";
+ break;
+ case ACT_TENTATIVE:
+ partstat = "TENTATIVE";
+ break;
+ default:
+ ;
+ }
+
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ for (node = clines;
+ (node = find_contentline (node, "ATTENDEE", 0));
+ node = node->next) {
+ param_list *p;
+
+ ismymbox (NULL); /* need to prime ismymbox() */
+
+ /* According to RFC 5545 § 3.3.3, an email address in the
+ value must be a mailto URI. */
+ if (! strncasecmp (node->value, "mailto:", 7)) {
+ char *addr = node->value + 7;
+ struct mailname *mn;
+
+ /* Skip any leading whitespace. */
+ for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+ addr = getname (addr);
+ mn = getm (addr, NULL, 0, NULL, 0);
+
+ /* Need to flush getname after use. */
+ while (getname ("")) { continue; }
+
+ if (ismymbox (mn)) {
+ found_my_attendee_line = 1;
+ for (p = node->params; p && p->param_name; p = p->next) {
+ value_list *v;
+
+ for (v = p->values; v; v = v->next) {
+ if (! strcasecmp (p->param_name, "ROLE") ||
+ ! strcasecmp (p->param_name, "RSVP")) {
+ remove_value (v);
+ } else if (! strcasecmp (p->param_name, "PARTSTAT")) {
+ free (v->value);
+ v->value = strdup (partstat);
+ }
+ }
+ }
+ } else {
+ remove_contentline (node);
+ }
+
+ mnfree (mn);
+ }
+ }
+
+ if (! found_my_attendee_line) {
+ /* Generate and attach an ATTENDEE line for me. */
+ contentline *node;
+
+ /* Add it after the ORGANIZER line, or if none, BEGIN:VEVENT line. */
+ if ((node = find_contentline (clines, "ORGANIZER", 0)) ||
+ (node = find_contentline (clines, "BEGIN", "VEVENT"))) {
+ contentline *new_node = add_contentline (node, "ATTENDEE");
+
+ add_param_name (new_node, strdup ("PARTSTAT"));
+ add_param_value (new_node, strdup (partstat));
+ add_param_name (new_node, strdup ("CN"));
+ add_param_value (new_node, strdup (getfullname ()));
+ new_node->value = concat ("MAILTO:", getlocalmbox (), NULL);
+ }
+ }
+
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ for (node = clines;
+ (node = find_contentline (node, "DESCRIPTION", 0));
+ node = node->next) {
+ /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
+ remove_contentline (node);
+ }
+}
+
+/*
+ * - Change METHOD from REQUEST to CANCEL.
+ * - Change PRODID.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Change STATUS from CONFIRMED to CANCELLED.
+ * - Increment value of SEQUENCE.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_cancellation (contentline *clines) {
+ contentline *node;
+
+ convert_common (clines, ACT_CANCEL);
+
+ if ((node = find_contentline (clines, "STATUS", 0)) &&
+ ! strcasecmp (node->value, "CONFIRMED")) {
+ free (node->value);
+ node->value = strdup ("CANCELLED");
+ }
+
+ if ((node = find_contentline (clines, "SEQUENCE", 0))) {
+ int sequence = atoi (node->value);
+ char buf[32];
+
+ (void) snprintf (buf, sizeof buf, "%d", sequence + 1);
+ free (node->value);
+ node->value = strdup (buf);
+ }
+}
+
+static void
+convert_common (contentline *clines, act action) {
+ contentline *node;
+ int in_valarm;
+
+ if ((node = find_contentline (clines, "METHOD", 0))) {
+ free (node->value);
+ node->value = strdup (action == ACT_CANCEL ? "CANCEL" : "REPLY");
+ }
+
+ if ((node = find_contentline (clines, "PRODID", 0))) {
+ free (node->value);
+ node->value = strdup ("nmh mhical v0.1");
+ }
+
+ if ((node = find_contentline (clines, "VERSION", 0))) {
+ if (! node->value) {
+ admonish (NULL, "Version property is missing value, assume 2.0");
+ node->value = strdup ("2.0");
+ }
+
+ if (strcmp (node->value, "2.0")) {
+ admonish (NULL, "supports the Version 2.0 specified by RFC 5545 "
+ "but iCalendar object has Version %s", node->value);
+ node->value = strdup ("2.0");
+ }
+ }
+
+ if ((node = find_contentline (clines, "SUMMARY", 0))) {
+ char *insert = NULL;
+
+ switch (action) {
+ case ACT_ACCEPT:
+ insert = "Accepted: ";
+ break;
+ case ACE_DECLINE:
+ insert = "Declined: ";
+ break;
+ case ACT_TENTATIVE:
+ insert = "Tentative: ";
+ break;
+ case ACT_DELEGATE:
+ adios (NULL, "Delegate replies are not supported");
+ break;
+ case ACT_CANCEL:
+ insert = "Cancelled:";
+ break;
+ default:
+ ;
+ }
+
+ if (insert) {
+ const size_t len = strlen (insert) + strlen (node->value) + 1;
+ char *tmp = mh_xmalloc (len);
+
+ (void) strncpy (tmp, insert, len);
+ (void) strncat (tmp, node->value, len - strlen (insert) - 1);
+ free (node->value);
+ node->value = tmp;
+ } else {
+ /* Should never get here. */
+ adios (NULL, "Unknown action: %d", action);
+ }
+ }
+
+ if ((node = find_contentline (clines, "DTSTAMP", 0))) {
+ const time_t now = time (NULL);
+ struct tm now_tm;
+
+ if (gmtime_r (&now, &now_tm)) {
+ /* 17 would be sufficient given that RFC 5545 § 3.3.4
+ supports only a 4 digit year. */
+ char buf[32];
+
+ if (strftime (buf, sizeof buf, "%Y%m%dT%H%M%SZ", &now_tm)) {
+ free (node->value);
+ node->value = strdup (buf);
+ } else {
+ admonish (NULL, "strftime unable to format current time");
+ }
+ } else {
+ admonish (NULL, "gmtime_r failed on current time");
+ }
+ }
+
+ /* Excise X- lines and VALARM section(s). */
+ in_valarm = 0;
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (! node->name) { continue; }
+
+ if (in_valarm) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 0;
+ }
+ remove_contentline (node);
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 1;
+ remove_contentline (node);
+ } else if (! strncasecmp ("X-", node->name, 2)) {
+ remove_contentline (node);
+ }
+ }
+ }
+}
+
+/* Echo the input, but with unfolded lines. */
+static void
+dump_unfolded (FILE *file, contentline *clines) {
+ contentline *node;
+
+ for (node = clines; node; node = node->next) {
+ fputs (node->input_line, file);
+ }
+}
+
+static void
+output (FILE *file, contentline *clines, int contenttype) {
+ contentline *node;
+
+ if (contenttype) {
+ /* Generate a Content-Type header to pass the method parameter
+ to mhbuild. Per RFC 5545 Secs. 6 and 8.1, it must be
+ UTF-8. But we don't attempt to do any conversion of the
+ input. */
+ if ((node = find_contentline (clines, "METHOD", 0))) {
+ fprintf (file,
+ "Content-Type: text/calendar; method=\"%s\"; "
+ "charset=\"UTF-8\"\n\n",
+ node->value);
+ }
+ }
+
+ for (node = clines; node; node = node->next) {
+ if (node->name) {
+ char *line = NULL;
+ size_t len;
+
+ line = strdup (node->name);
+ line = format_params (line, node->params);
+
+ len = strlen (line);
+ line = mh_xrealloc (line, len + 2);
+ line[len] = ':';
+ line[len + 1] = '\0';
+
+ line = fold (add (node->value, line),
+ clines->cr_before_lf == CR_BEFORE_LF);
+
+ if (clines->cr_before_lf == LF_ONLY) {
+ fprintf (file, "%s\n", line);
+ } else {
+ fprintf (file, "%s\r\n", line);
+ }
+ free (line);
+ }
+ }
+}
+
+/*
+ * Display these fields of the iCalendar event:
+ * - method
+ * - organizer
+ * - summary
+ * - description, except for "\n\n" and in VALARM
+ * - location
+ * - dtstart in local timezone
+ * - dtend in local timezone
+ * - attendees (limited to number specified in initialization)
+ */
+static void
+display (FILE *file, contentline *clines, char *nfs) {
+ tzdesc_t timezones = load_timezones (clines);
+ int in_vtimezone;
+ int in_valarm;
+ contentline *node;
+ struct format *fmt;
+ int dat[5] = { 0, 0, 0, INT_MAX, 0 };
+ struct comp *c;
+ charstring_t buffer = charstring_create (BUFSIZ);
+ charstring_t attendees = charstring_create (BUFSIZ);
+ const unsigned int max_attendees = 20;
+ unsigned int num_attendees;
+
+ /* Don't call on the END:VCALENDAR line. */
+ if (clines && clines->next) {
+ (void) fmt_compile (nfs, &fmt, 1);
+ }
+
+ if ((c = fmt_findcomp ("method"))) {
+ if ((node = find_contentline (clines, "METHOD", 0)) && node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ if ((c = fmt_findcomp ("organizer"))) {
+ if ((node = find_contentline (clines, "ORGANIZER", 0)) &&
+ node->value) {
+ c->c_text = strdup (identity (node));
+ }
+ }
+
+ if ((c = fmt_findcomp ("summary"))) {
+ if ((node = find_contentline (clines, "SUMMARY", 0)) && node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ /* Only display DESCRIPTION lines that are outside VALARM section(s). */
+ in_valarm = 0;
+ if ((c = fmt_findcomp ("description"))) {
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (node->name && node->value && ! in_valarm &&
+ ! strcasecmp ("DESCRIPTION", node->name) &&
+ strcasecmp (node->value, "\\n\\n")) {
+ c->c_text = strdup (node->value);
+ } else if (in_valarm) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 0;
+ }
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 1;
+ }
+ }
+ }
+ }
+
+ if ((c = fmt_findcomp ("location"))) {
+ if ((node = find_contentline (clines, "LOCATION", 0)) &&
+ node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ if ((c = fmt_findcomp ("dtstart"))) {
+ /* Find DTSTART outsize of a VTIMEZONE section. */
+ in_vtimezone = 0;
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (! node->name) { continue; }
+
+ if (in_vtimezone) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+ in_vtimezone = 0;
+ }
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+ in_vtimezone = 1;
+ } else if (! strcasecmp ("DTSTART", node->name)) {
+ /* Got it: DTSTART outside of a VTIMEZONE section. */
+ char *datetime = format_datetime (timezones, node);
+ c->c_text = datetime ? datetime : node->value;
+ }
+ }
+ }
+ }
+
+ if ((c = fmt_findcomp ("dtend"))) {
+ if ((node = find_contentline (clines, "DTEND", 0)) && node->value) {
+ char *datetime = format_datetime (timezones, node);
+ c->c_text = datetime ? datetime : node->value;
+ }
+ }
+
+ if ((c = fmt_findcomp ("attendees"))) {
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ charstring_append_cstring (attendees, "Attendees: ");
+ for (node = clines, num_attendees = 0;
+ (node = find_contentline (node, "ATTENDEE", 0)) &&
+ num_attendees++ < max_attendees;
+ node = node->next) {
+ const char *id = identity (node);
+
+ if (num_attendees > 1) {
+ charstring_append_cstring (attendees, ", ");
+ }
+ charstring_append_cstring (attendees, id);
+ }
+
+ if (num_attendees >= max_attendees) {
+ unsigned int not_shown = 0;
+
+ for ( ;
+ (node = find_contentline (node, "ATTENDEE", 0));
+ node = node->next) {
+ ++not_shown;
+ }
+
+ if (not_shown > 0) {
+ char buf[32];
+
+ (void) snprintf (buf, sizeof buf, ", and %d more", not_shown);
+ charstring_append_cstring (attendees, buf);
+ }
+ }
+
+ if (num_attendees > 0) {
+ c->c_text = charstring_buffer_copy (attendees);
+ }
+ }
+
+ /* Don't call on the END:VCALENDAR line. */
+ if (clines->next) {
+ (void) fmt_scan (fmt, buffer, INT_MAX, dat, NULL);
+ fputs (charstring_buffer (buffer), file);
+ fmt_free (fmt, 1);
+ }
+
+ charstring_free (attendees);
+ charstring_free (buffer);
+ free_timezones (timezones);
+}
+
+static const char *
+identity (const contentline *node) {
+ /* According to RFC 5545 § 3.3.3, an email address in the value
+ must be a mailto URI. */
+ if (! strncasecmp (node->value, "mailto:", 7)) {
+ char *addr;
+ param_list *p;
+
+ for (p = node->params; p && p->param_name; p = p->next) {
+ value_list *v;
+
+ for (v = p->values; v; v = v->next) {
+ if (! strcasecmp (p->param_name, "CN")) {
+ return v->value;
+ }
+ }
+ }
+
+ /* Did not find a CN parameter, so output the address. */
+ addr = node->value + 7;
+
+ /* Skip any leading whitespace. */
+ for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+ return addr;
+ }
+
+ return "unknown";
+}
+
+static char *
+format_params (char *line, param_list *p) {
+ for ( ; p && p->param_name; p = p->next) {
+ value_list *v;
+ size_t num_values = 0;
+
+ for (v = p->values; v; v = v->next) {
+ if (v->value) { ++num_values; }
+ }
+
+ if (num_values) {
+ size_t len = strlen (line);
+
+ line = mh_xrealloc (line, len + 2);
+ line[len] = ';';
+ line[len + 1] = '\0';
+
+ line = add (p->param_name, line);
+
+ for (v = p->values; v; v = v->next) {
+ len = strlen (line);
+ line = mh_xrealloc (line, len + 2);
+ line[len] = v == p->values ? '=' : ',';
+ line[len + 1] = '\0';
+
+ line = add (v->value, line);
+ }
+ }
+ }
+
+ return line;
+}
+
+static char *
+fold (char *line, int uses_cr) {
+ size_t remaining = strlen (line);
+ size_t current_line_len = 0;
+ charstring_t folded_line = charstring_create (2 * remaining);
+ const char *cp = line;
+
+#ifdef MULTIBYTE_SUPPORT
+ if (mbtowc (NULL, NULL, 0)) {} /* reset shift state */
+#endif
+
+ while (*cp && remaining > 0) {
+#ifdef MULTIBYTE_SUPPORT
+ int char_len = mbtowc (NULL, cp, (size_t) MB_CUR_MAX < remaining
+ ? (size_t) MB_CUR_MAX
+ : remaining);
+ if (char_len == -1) { char_len = 1; }
+#else
+ const int char_len = 1;
+#endif
+
+ charstring_push_back_chars (folded_line, cp, char_len, 1);
+ remaining -= char_len > 0 ? char_len : 1;
+
+ /* remaining must be > 0 to pass the loop condition above, so
+ if it's not > 1, it is == 1. */
+ if (++current_line_len >= 75) {
+ if (remaining > 1 || (*(cp+1) != '\0' && *(cp+1) != '\r' &&
+ *(cp+1) != '\n')) {
+ /* fold */
+ if (uses_cr) { charstring_push_back (folded_line, '\r'); }
+ charstring_push_back (folded_line, '\n');
+ charstring_push_back (folded_line, ' ');
+ current_line_len = 0;
+ }
+ }
+
+ cp += char_len > 0 ? char_len : 1;
+ }
+
+ free (line);
+ line = charstring_buffer_copy (folded_line);
+ charstring_free (folded_line);
+
+ return line;
+}