]>
diplodocus.org Git - nmh/blob - uip/mhical.c
2 * mhical.c -- operate on an iCalendar request
4 * This code is Copyright (c) 2014, by the authors of nmh.
5 * See the COPYRIGHT file in the root directory of the nmh
6 * distribution for complete copyright information.
10 #include "h/icalendar.h"
11 #include "sbr/icalparse.h"
12 #include <h/fmt_scan.h>
13 #include "h/addrsbr.h"
27 static void convert_to_reply (contentline
*, act
);
28 static void convert_to_cancellation (contentline
*);
29 static void convert_common (contentline
*, act
);
30 static void dump_unfolded (FILE *, contentline
*);
31 static void output (FILE *, contentline
*, int);
32 static void display (FILE *, contentline
*, char *);
33 static const char *identity (const contentline
*);
34 static char *format_params (char *, param_list
*);
35 static char *fold (char *, int);
37 #define MHICAL_SWITCHES \
38 X("reply accept|decline|tentative", 0, REPLYSW) \
39 X("cancel", 0, CANCELSW) \
40 X("form formatfile", 0, FORMSW) \
41 X("format string", 5, FMTSW) \
42 X("infile", 0, INFILESW) \
43 X("outfile", 0, OUTFILESW) \
44 X("contenttype", 0, CONTENTTYPESW) \
45 X("nocontenttype", 0, NCONTENTTYPESW) \
46 X("unfold", 0, UNFOLDSW) \
47 X("debug", 0, DEBUGSW) \
48 X("version", 0, VERSIONSW) \
49 X("help", 0, HELPSW) \
51 #define X(sw, minchars, id) id,
52 DEFINE_SWITCH_ENUM(MHICAL
);
55 #define X(sw, minchars, id) { sw, minchars, id },
56 DEFINE_SWITCH_ARRAY(MHICAL
, switches
);
59 vevent vevents
= { NULL
, NULL
, NULL
};
62 main (int argc
, char *argv
[]) {
63 /* RFC 5322 § 3.3 date-time format, including the optional
64 day-of-week and not including the optional seconds. The
65 zone is required by the RFC but not always output by this
66 format, because RFC 5545 § 3.3.5 allows date-times not
67 bound to any time zone. */
69 act action
= ACT_NONE
;
70 char *infile
= NULL
, *outfile
= NULL
;
71 FILE *inputfile
= NULL
, *outputfile
= NULL
;
72 int contenttype
= 0, unfold
= 0;
73 vevent
*v
, *nextvevent
;
74 char *form
= "mhical.24hour", *format
= NULL
;
75 char **argp
, **arguments
, *cp
;
77 icaldebug
= 0; /* Global provided by bison (with name-prefix "ical"). */
79 if (nmh_init(argv
[0], 1)) { return 1; }
81 arguments
= getarguments (invo_name
, argc
, argv
, 1);
87 while ((cp
= *argp
++)) {
89 switch (smatch (++cp
, switches
)) {
91 ambigsw (cp
, switches
);
94 adios (NULL
, "-%s unknown", cp
);
98 snprintf (buf
, sizeof buf
, "%s [switches]", invo_name
);
99 print_help (buf
, switches
, 1);
103 print_version(invo_name
);
110 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
111 adios (NULL
, "missing argument to %s", argp
[-2]);
112 if (! strcasecmp (cp
, "accept")) {
114 } else if (! strcasecmp (cp
, "decline")) {
115 action
= ACE_DECLINE
;
116 } else if (! strcasecmp (cp
, "tentative")) {
117 action
= ACT_TENTATIVE
;
118 } else if (! strcasecmp (cp
, "delegate")) {
119 action
= ACT_DELEGATE
;
121 adios (NULL
, "Unknown action: %s", cp
);
130 if (! (form
= *argp
++) || *form
== '-')
131 adios (NULL
, "missing argument to %s", argp
[-2]);
135 if (! (format
= *argp
++) || *format
== '-')
136 adios (NULL
, "missing argument to %s", argp
[-2]);
141 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
142 adios (NULL
, "missing argument to %s", argp
[-2]);
143 infile
= *cp
== '-' ? add (cp
, NULL
) : path (cp
, TFILE
);
146 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
147 adios (NULL
, "missing argument to %s", argp
[-2]);
148 outfile
= *cp
== '-' ? add (cp
, NULL
) : path (cp
, TFILE
);
168 if ((inputfile
= fopen (infile
, "r"))) {
169 icalset_inputfile (inputfile
);
171 adios (infile
, "error opening");
178 if ((outputfile
= fopen (outfile
, "w"))) {
179 icalset_outputfile (outputfile
);
181 adios (outfile
, "error opening");
187 vevents
.last
= &vevents
;
188 /* vevents is accessed by parser as global. */
191 for (v
= &vevents
; v
; v
= nextvevent
) {
192 if (! unfold
&& v
!= &vevents
&& v
->contentlines
&&
193 v
->contentlines
->name
&&
194 strcasecmp (v
->contentlines
->name
, "END") &&
195 v
->contentlines
->value
&&
196 strcasecmp (v
->contentlines
->value
, "VCALENDAR")) {
197 /* Output blank line between vevents. Not before
198 first vevent and not after last. */
199 putc ('\n', outputfile
);
202 if (action
== ACT_NONE
) {
204 dump_unfolded (outputfile
, v
->contentlines
);
206 char *nfs
= new_fs (form
, format
, NULL
);
208 display (outputfile
, v
->contentlines
, nfs
);
212 if (action
== ACT_CANCEL
) {
213 convert_to_cancellation (v
->contentlines
);
215 convert_to_reply (v
->contentlines
, action
);
217 output (outputfile
, v
->contentlines
, contenttype
);
220 free_contentlines (v
->contentlines
);
221 nextvevent
= v
->next
;
228 if (fclose (inputfile
) != 0) {
229 advise (infile
, "error closing");
234 if (fclose (outputfile
) != 0) {
235 advise (outfile
, "error closing");
244 * - Change METHOD from REQUEST to REPLY.
246 * - Remove all ATTENDEE lines for other users (based on ismymbox ()).
247 * - For the user's ATTENDEE line:
248 * - Remove ROLE and RSVP parameters.
249 * - Change PARTSTAT value to indicate reply action, e.g., ACCEPTED,
250 * DECLINED, or TENTATIVE.
251 * - Insert action at beginning of SUMMARY value.
252 * - Remove all X- lines.
253 * - Update DTSTAMP with current timestamp.
254 * - Remove all DESCRIPTION lines.
255 * - Excise VALARM sections.
258 convert_to_reply (contentline
*clines
, act action
) {
259 char *partstat
= NULL
;
260 int found_my_attendee_line
= 0;
263 convert_common (clines
, action
);
267 partstat
= "ACCEPTED";
270 partstat
= "DECLINED";
273 partstat
= "TENTATIVE";
279 /* Call find_contentline () with node as argument to find multiple
280 matching contentlines. */
282 (node
= find_contentline (node
, "ATTENDEE", 0));
286 ismymbox (NULL
); /* need to prime ismymbox() */
288 /* According to RFC 5545 § 3.3.3, an email address in the
289 value must be a mailto URI. */
290 if (! strncasecmp (node
->value
, "mailto:", 7)) {
291 char *addr
= node
->value
+ 7;
294 /* Skip any leading whitespace. */
295 for ( ; isspace ((unsigned char) *addr
); ++addr
) { continue; }
297 addr
= getname (addr
);
298 mn
= getm (addr
, NULL
, 0, NULL
, 0);
300 /* Need to flush getname after use. */
301 while (getname ("")) { continue; }
304 found_my_attendee_line
= 1;
305 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
308 for (v
= p
->values
; v
; v
= v
->next
) {
309 if (! strcasecmp (p
->param_name
, "ROLE") ||
310 ! strcasecmp (p
->param_name
, "RSVP")) {
312 } else if (! strcasecmp (p
->param_name
, "PARTSTAT")) {
314 v
->value
= strdup (partstat
);
319 remove_contentline (node
);
326 if (! found_my_attendee_line
) {
327 /* Generate and attach an ATTENDEE line for me. */
330 /* Add it after the ORGANIZER line, or if none, BEGIN:VEVENT line. */
331 if ((node
= find_contentline (clines
, "ORGANIZER", 0)) ||
332 (node
= find_contentline (clines
, "BEGIN", "VEVENT"))) {
333 contentline
*new_node
= add_contentline (node
, "ATTENDEE");
335 add_param_name (new_node
, strdup ("PARTSTAT"));
336 add_param_value (new_node
, strdup (partstat
));
337 add_param_name (new_node
, strdup ("CN"));
338 add_param_value (new_node
, strdup (getfullname ()));
339 new_node
->value
= concat ("MAILTO:", getlocalmbox (), NULL
);
343 /* Call find_contentline () with node as argument to find multiple
344 matching contentlines. */
346 (node
= find_contentline (node
, "DESCRIPTION", 0));
348 /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
349 remove_contentline (node
);
354 * - Change METHOD from REQUEST to CANCEL.
356 * - Insert action at beginning of SUMMARY value.
357 * - Remove all X- lines.
358 * - Update DTSTAMP with current timestamp.
359 * - Change STATUS from CONFIRMED to CANCELLED.
360 * - Increment value of SEQUENCE.
361 * - Excise VALARM sections.
364 convert_to_cancellation (contentline
*clines
) {
367 convert_common (clines
, ACT_CANCEL
);
369 if ((node
= find_contentline (clines
, "STATUS", 0)) &&
370 ! strcasecmp (node
->value
, "CONFIRMED")) {
372 node
->value
= strdup ("CANCELLED");
375 if ((node
= find_contentline (clines
, "SEQUENCE", 0))) {
376 int sequence
= atoi (node
->value
);
379 (void) snprintf (buf
, sizeof buf
, "%d", sequence
+ 1);
381 node
->value
= strdup (buf
);
386 convert_common (contentline
*clines
, act action
) {
390 if ((node
= find_contentline (clines
, "METHOD", 0))) {
392 node
->value
= strdup (action
== ACT_CANCEL
? "CANCEL" : "REPLY");
395 if ((node
= find_contentline (clines
, "PRODID", 0))) {
397 node
->value
= strdup ("nmh mhical v0.1");
400 if ((node
= find_contentline (clines
, "VERSION", 0))) {
402 admonish (NULL
, "Version property is missing value, assume 2.0");
403 node
->value
= strdup ("2.0");
406 if (strcmp (node
->value
, "2.0")) {
407 admonish (NULL
, "supports the Version 2.0 specified by RFC 5545 "
408 "but iCalendar object has Version %s", node
->value
);
409 node
->value
= strdup ("2.0");
413 if ((node
= find_contentline (clines
, "SUMMARY", 0))) {
418 insert
= "Accepted: ";
421 insert
= "Declined: ";
424 insert
= "Tentative: ";
427 adios (NULL
, "Delegate replies are not supported");
430 insert
= "Cancelled:";
437 const size_t len
= strlen (insert
) + strlen (node
->value
) + 1;
438 char *tmp
= mh_xmalloc (len
);
440 (void) strncpy (tmp
, insert
, len
);
441 (void) strncat (tmp
, node
->value
, len
- strlen (insert
) - 1);
445 /* Should never get here. */
446 adios (NULL
, "Unknown action: %d", action
);
450 if ((node
= find_contentline (clines
, "DTSTAMP", 0))) {
451 const time_t now
= time (NULL
);
454 if (gmtime_r (&now
, &now_tm
)) {
455 /* 17 would be sufficient given that RFC 5545 § 3.3.4
456 supports only a 4 digit year. */
459 if (strftime (buf
, sizeof buf
, "%Y%m%dT%H%M%SZ", &now_tm
)) {
461 node
->value
= strdup (buf
);
463 admonish (NULL
, "strftime unable to format current time");
466 admonish (NULL
, "gmtime_r failed on current time");
470 /* Excise X- lines and VALARM section(s). */
472 for (node
= clines
; node
; node
= node
->next
) {
473 /* node->name will be NULL if the line was deleted. */
474 if (! node
->name
) { continue; }
477 if (! strcasecmp ("END", node
->name
) &&
478 ! strcasecmp ("VALARM", node
->value
)) {
481 remove_contentline (node
);
483 if (! strcasecmp ("BEGIN", node
->name
) &&
484 ! strcasecmp ("VALARM", node
->value
)) {
486 remove_contentline (node
);
487 } else if (! strncasecmp ("X-", node
->name
, 2)) {
488 remove_contentline (node
);
494 /* Echo the input, but with unfolded lines. */
496 dump_unfolded (FILE *file
, contentline
*clines
) {
499 for (node
= clines
; node
; node
= node
->next
) {
500 fputs (node
->input_line
, file
);
505 output (FILE *file
, contentline
*clines
, int contenttype
) {
509 /* Generate a Content-Type header to pass the method parameter
510 to mhbuild. Per RFC 5545 Secs. 6 and 8.1, it must be
511 UTF-8. But we don't attempt to do any conversion of the
513 if ((node
= find_contentline (clines
, "METHOD", 0))) {
515 "Content-Type: text/calendar; method=\"%s\"; "
516 "charset=\"UTF-8\"\n\n",
521 for (node
= clines
; node
; node
= node
->next
) {
526 line
= strdup (node
->name
);
527 line
= format_params (line
, node
->params
);
530 line
= mh_xrealloc (line
, len
+ 2);
532 line
[len
+ 1] = '\0';
534 line
= fold (add (node
->value
, line
),
535 clines
->cr_before_lf
== CR_BEFORE_LF
);
537 if (clines
->cr_before_lf
== LF_ONLY
) {
538 fprintf (file
, "%s\n", line
);
540 fprintf (file
, "%s\r\n", line
);
548 * Display these fields of the iCalendar event:
552 * - description, except for "\n\n" and in VALARM
554 * - dtstart in local timezone
555 * - dtend in local timezone
556 * - attendees (limited to number specified in initialization)
559 display (FILE *file
, contentline
*clines
, char *nfs
) {
560 tzdesc_t timezones
= load_timezones (clines
);
565 int dat
[5] = { 0, 0, 0, INT_MAX
, 0 };
567 charstring_t buffer
= charstring_create (BUFSIZ
);
568 charstring_t attendees
= charstring_create (BUFSIZ
);
569 const unsigned int max_attendees
= 20;
570 unsigned int num_attendees
;
572 /* Don't call on the END:VCALENDAR line. */
573 if (clines
&& clines
->next
) {
574 (void) fmt_compile (nfs
, &fmt
, 1);
577 if ((c
= fmt_findcomp ("method"))) {
578 if ((node
= find_contentline (clines
, "METHOD", 0)) && node
->value
) {
579 c
->c_text
= strdup (node
->value
);
583 if ((c
= fmt_findcomp ("organizer"))) {
584 if ((node
= find_contentline (clines
, "ORGANIZER", 0)) &&
586 c
->c_text
= strdup (identity (node
));
590 if ((c
= fmt_findcomp ("summary"))) {
591 if ((node
= find_contentline (clines
, "SUMMARY", 0)) && node
->value
) {
592 c
->c_text
= strdup (node
->value
);
596 /* Only display DESCRIPTION lines that are outside VALARM section(s). */
598 if ((c
= fmt_findcomp ("description"))) {
599 for (node
= clines
; node
; node
= node
->next
) {
600 /* node->name will be NULL if the line was deleted. */
601 if (node
->name
&& node
->value
&& ! in_valarm
&&
602 ! strcasecmp ("DESCRIPTION", node
->name
) &&
603 strcasecmp (node
->value
, "\\n\\n")) {
604 c
->c_text
= strdup (node
->value
);
605 } else if (in_valarm
) {
606 if (! strcasecmp ("END", node
->name
) &&
607 ! strcasecmp ("VALARM", node
->value
)) {
611 if (! strcasecmp ("BEGIN", node
->name
) &&
612 ! strcasecmp ("VALARM", node
->value
)) {
619 if ((c
= fmt_findcomp ("location"))) {
620 if ((node
= find_contentline (clines
, "LOCATION", 0)) &&
622 c
->c_text
= strdup (node
->value
);
626 if ((c
= fmt_findcomp ("dtstart"))) {
627 /* Find DTSTART outsize of a VTIMEZONE section. */
629 for (node
= clines
; node
; node
= node
->next
) {
630 /* node->name will be NULL if the line was deleted. */
631 if (! node
->name
) { continue; }
634 if (! strcasecmp ("END", node
->name
) &&
635 ! strcasecmp ("VTIMEZONE", node
->value
)) {
639 if (! strcasecmp ("BEGIN", node
->name
) &&
640 ! strcasecmp ("VTIMEZONE", node
->value
)) {
642 } else if (! strcasecmp ("DTSTART", node
->name
)) {
643 /* Got it: DTSTART outside of a VTIMEZONE section. */
644 char *datetime
= format_datetime (timezones
, node
);
645 c
->c_text
= datetime
? datetime
: strdup(node
->value
);
651 if ((c
= fmt_findcomp ("dtend"))) {
652 if ((node
= find_contentline (clines
, "DTEND", 0)) && node
->value
) {
653 char *datetime
= format_datetime (timezones
, node
);
654 c
->c_text
= datetime
? datetime
: strdup(node
->value
);
655 } else if ((node
= find_contentline (clines
, "DTSTART", 0)) &&
657 /* There is no DTEND. If there's a DTSTART, use it. If it
658 doesn't have a time, assume that the event is for the
659 entire day and append 23:59:59 to it so that it signifies
660 the end of the day. And assume local timezone. */
661 if (strchr(node
->value
, 'T')) {
662 char * datetime
= format_datetime (timezones
, node
);
663 c
->c_text
= datetime
? datetime
: strdup(node
->value
);
666 contentline node_copy
;
668 memcpy(&node_copy
, node
, sizeof node_copy
);
669 node_copy
.value
= concat(node_copy
.value
, "T235959", NULL
);
670 datetime
= format_datetime (timezones
, &node_copy
);
671 c
->c_text
= datetime
? datetime
: strdup(node_copy
.value
);
672 free(node_copy
.value
);
677 if ((c
= fmt_findcomp ("attendees"))) {
678 /* Call find_contentline () with node as argument to find multiple
679 matching contentlines. */
680 charstring_append_cstring (attendees
, "Attendees: ");
681 for (node
= clines
, num_attendees
= 0;
682 (node
= find_contentline (node
, "ATTENDEE", 0)) &&
683 num_attendees
++ < max_attendees
;
685 const char *id
= identity (node
);
687 if (num_attendees
> 1) {
688 charstring_append_cstring (attendees
, ", ");
690 charstring_append_cstring (attendees
, id
);
693 if (num_attendees
>= max_attendees
) {
694 unsigned int not_shown
= 0;
697 (node
= find_contentline (node
, "ATTENDEE", 0));
705 (void) snprintf (buf
, sizeof buf
, ", and %d more", not_shown
);
706 charstring_append_cstring (attendees
, buf
);
710 if (num_attendees
> 0) {
711 c
->c_text
= charstring_buffer_copy (attendees
);
715 /* Don't call on the END:VCALENDAR line. */
717 (void) fmt_scan (fmt
, buffer
, INT_MAX
, dat
, NULL
);
718 fputs (charstring_buffer (buffer
), file
);
722 charstring_free (attendees
);
723 charstring_free (buffer
);
724 free_timezones (timezones
);
728 identity (const contentline
*node
) {
729 /* According to RFC 5545 § 3.3.3, an email address in the value
730 must be a mailto URI. */
731 if (! strncasecmp (node
->value
, "mailto:", 7)) {
735 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
738 for (v
= p
->values
; v
; v
= v
->next
) {
739 if (! strcasecmp (p
->param_name
, "CN")) {
745 /* Did not find a CN parameter, so output the address. */
746 addr
= node
->value
+ 7;
748 /* Skip any leading whitespace. */
749 for ( ; isspace ((unsigned char) *addr
); ++addr
) { continue; }
758 format_params (char *line
, param_list
*p
) {
759 for ( ; p
&& p
->param_name
; p
= p
->next
) {
761 size_t num_values
= 0;
763 for (v
= p
->values
; v
; v
= v
->next
) {
764 if (v
->value
) { ++num_values
; }
768 size_t len
= strlen (line
);
770 line
= mh_xrealloc (line
, len
+ 2);
772 line
[len
+ 1] = '\0';
774 line
= add (p
->param_name
, line
);
776 for (v
= p
->values
; v
; v
= v
->next
) {
778 line
= mh_xrealloc (line
, len
+ 2);
779 line
[len
] = v
== p
->values
? '=' : ',';
780 line
[len
+ 1] = '\0';
782 line
= add (v
->value
, line
);
791 fold (char *line
, int uses_cr
) {
792 size_t remaining
= strlen (line
);
793 size_t current_line_len
= 0;
794 charstring_t folded_line
= charstring_create (2 * remaining
);
795 const char *cp
= line
;
797 #ifdef MULTIBYTE_SUPPORT
798 if (mbtowc (NULL
, NULL
, 0)) {} /* reset shift state */
801 while (*cp
&& remaining
> 0) {
802 #ifdef MULTIBYTE_SUPPORT
803 int char_len
= mbtowc (NULL
, cp
, (size_t) MB_CUR_MAX
< remaining
804 ? (size_t) MB_CUR_MAX
806 if (char_len
== -1) { char_len
= 1; }
808 const int char_len
= 1;
811 charstring_push_back_chars (folded_line
, cp
, char_len
, 1);
812 remaining
-= char_len
> 0 ? char_len
: 1;
814 /* remaining must be > 0 to pass the loop condition above, so
815 if it's not > 1, it is == 1. */
816 if (++current_line_len
>= 75) {
817 if (remaining
> 1 || (*(cp
+1) != '\0' && *(cp
+1) != '\r' &&
820 if (uses_cr
) { charstring_push_back (folded_line
, '\r'); }
821 charstring_push_back (folded_line
, '\n');
822 charstring_push_back (folded_line
, ' ');
823 current_line_len
= 0;
827 cp
+= char_len
> 0 ? char_len
: 1;
831 line
= charstring_buffer_copy (folded_line
);
832 charstring_free (folded_line
);