]>
diplodocus.org Git - nmh/blob - uip/mhical.c
9618b2b2437bc4b6acc4d9be7c27184fa707e994
1 /* mhical.c -- operate on an iCalendar request
3 * This code is Copyright (c) 2014, by the authors of nmh.
4 * See the COPYRIGHT file in the root directory of the nmh
5 * distribution for complete copyright information.
9 #include "sbr/getarguments.h"
10 #include "sbr/concat.h"
11 #include "sbr/smatch.h"
12 #include "sbr/ambigsw.h"
14 #include "sbr/print_version.h"
15 #include "sbr/print_help.h"
16 #include "sbr/error.h"
17 #include "h/icalendar.h"
18 #include "sbr/datetime.h"
19 #include "sbr/icalparse.h"
20 #include "h/fmt_scan.h"
21 #include "h/addrsbr.h"
36 static void convert_to_reply (contentline
*, act
);
37 static void convert_to_cancellation (contentline
*);
38 static void convert_common (contentline
*, act
);
39 static void dump_unfolded (FILE *, contentline
*);
40 static void output (FILE *, contentline
*, int);
41 static void display (FILE *, contentline
*, char *);
42 static const char *identity (const contentline
*) PURE
;
43 static char *format_params (char *, param_list
*);
44 static char *fold (char *, int);
46 #define MHICAL_SWITCHES \
47 X("reply accept|decline|tentative", 0, REPLYSW) \
48 X("cancel", 0, CANCELSW) \
49 X("form formatfile", 0, FORMSW) \
50 X("format string", 5, FMTSW) \
51 X("infile", 0, INFILESW) \
52 X("outfile", 0, OUTFILESW) \
53 X("contenttype", 0, CONTENTTYPESW) \
54 X("nocontenttype", 0, NCONTENTTYPESW) \
55 X("unfold", 0, UNFOLDSW) \
56 X("debug", 0, DEBUGSW) \
57 X("version", 0, VERSIONSW) \
58 X("help", 0, HELPSW) \
60 #define X(sw, minchars, id) id,
61 DEFINE_SWITCH_ENUM(MHICAL
);
64 #define X(sw, minchars, id) { sw, minchars, id },
65 DEFINE_SWITCH_ARRAY(MHICAL
, switches
);
68 vevent vevents
= { NULL
, NULL
, NULL
};
69 int parser_status
= 0;
72 main (int argc
, char *argv
[])
74 /* RFC 5322 § 3.3 date-time format, including the optional
75 day-of-week and not including the optional seconds. The
76 zone is required by the RFC but not always output by this
77 format, because RFC 5545 § 3.3.5 allows date-times not
78 bound to any time zone. */
80 act action
= ACT_NONE
;
81 char *infile
= NULL
, *outfile
= NULL
;
82 FILE *inputfile
= NULL
, *outputfile
= NULL
;
83 bool contenttype
= false;
85 vevent
*v
, *nextvevent
;
86 char *form
= "mhical.24hour", *format
= NULL
;
87 char **argp
, **arguments
, *cp
;
89 icaldebug
= 0; /* Global provided by bison (with name-prefix "ical"). */
91 if (nmh_init(argv
[0], true, false)) { return 1; }
93 arguments
= getarguments (invo_name
, argc
, argv
, 1);
99 while ((cp
= *argp
++)) {
101 switch (smatch (++cp
, switches
)) {
103 ambigsw (cp
, switches
);
106 die("-%s unknown", cp
);
110 snprintf (buf
, sizeof buf
, "%s [switches]", invo_name
);
111 print_help (buf
, switches
, 1);
115 print_version(invo_name
);
122 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
123 die("missing argument to %s", argp
[-2]);
124 if (! strcasecmp (cp
, "accept")) {
126 } else if (! strcasecmp (cp
, "decline")) {
127 action
= ACE_DECLINE
;
128 } else if (! strcasecmp (cp
, "tentative")) {
129 action
= ACT_TENTATIVE
;
130 } else if (! strcasecmp (cp
, "delegate")) {
131 action
= ACT_DELEGATE
;
133 die("Unknown action: %s", cp
);
142 if (! (form
= *argp
++) || *form
== '-')
143 die("missing argument to %s", argp
[-2]);
147 if (! (format
= *argp
++) || *format
== '-')
148 die("missing argument to %s", argp
[-2]);
153 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
154 die("missing argument to %s", argp
[-2]);
155 infile
= *cp
== '-' ? mh_xstrdup(cp
) : path (cp
, TFILE
);
158 if (! (cp
= *argp
++) || (*cp
== '-' && cp
[1]))
159 die("missing argument to %s", argp
[-2]);
160 outfile
= *cp
== '-' ? mh_xstrdup(cp
) : path (cp
, TFILE
);
180 if ((inputfile
= fopen (infile
, "r"))) {
181 icalset_inputfile (inputfile
);
183 adios (infile
, "error opening");
190 if ((outputfile
= fopen (outfile
, "w"))) {
191 icalset_outputfile (outputfile
);
193 adios (outfile
, "error opening");
199 vevents
.last
= &vevents
;
200 /* vevents is accessed by parser as global. */
203 for (v
= &vevents
; v
; v
= nextvevent
) {
204 if (! unfold
&& v
!= &vevents
&& v
->contentlines
&&
205 v
->contentlines
->name
&&
206 strcasecmp (v
->contentlines
->name
, "END") &&
207 v
->contentlines
->value
&&
208 strcasecmp (v
->contentlines
->value
, "VCALENDAR")) {
209 /* Output blank line between vevents. Not before
210 first vevent and not after last. */
211 putc ('\n', outputfile
);
214 if (action
== ACT_NONE
) {
216 dump_unfolded (outputfile
, v
->contentlines
);
218 char *nfs
= new_fs (form
, format
, NULL
);
220 display (outputfile
, v
->contentlines
, nfs
);
224 if (action
== ACT_CANCEL
) {
225 convert_to_cancellation (v
->contentlines
);
227 convert_to_reply (v
->contentlines
, action
);
229 output (outputfile
, v
->contentlines
, contenttype
);
232 free_contentlines (v
->contentlines
);
233 nextvevent
= v
->next
;
240 if (fclose (inputfile
) != 0) {
241 advise (infile
, "error closing");
246 if (fclose (outputfile
) != 0) {
247 advise (outfile
, "error closing");
252 return parser_status
;
256 * - Change METHOD from REQUEST to REPLY.
258 * - Remove all ATTENDEE lines for other users (based on ismymbox ()).
259 * - For the user's ATTENDEE line:
260 * - Remove ROLE and RSVP parameters.
261 * - Change PARTSTAT value to indicate reply action, e.g., ACCEPTED,
262 * DECLINED, or TENTATIVE.
263 * - Insert action at beginning of SUMMARY value.
264 * - Remove all X- lines.
265 * - Update DTSTAMP with current timestamp.
266 * - Remove all DESCRIPTION lines.
267 * - Excise VALARM sections.
270 convert_to_reply (contentline
*clines
, act action
)
272 char *partstat
= NULL
;
273 bool found_my_attendee_line
= false;
276 convert_common (clines
, action
);
280 partstat
= "ACCEPTED";
283 partstat
= "DECLINED";
286 partstat
= "TENTATIVE";
292 /* Call find_contentline () with node as argument to find multiple
293 matching contentlines. */
295 (node
= find_contentline (node
, "ATTENDEE", 0));
299 ismymbox (NULL
); /* need to prime ismymbox() */
301 /* According to RFC 5545 § 3.3.3, an email address in the
302 value must be a mailto URI. */
303 if (! strncasecmp (node
->value
, "mailto:", 7)) {
304 char *addr
= node
->value
+ 7;
307 /* Skip any leading whitespace. */
308 for ( ; isspace ((unsigned char) *addr
); ++addr
) { continue; }
310 addr
= getname (addr
);
311 mn
= getm (addr
, NULL
, 0, NULL
, 0);
313 /* Need to flush getname after use. */
314 while (getname ("")) { continue; }
317 found_my_attendee_line
= true;
318 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
321 for (v
= p
->values
; v
; v
= v
->next
) {
322 if (! strcasecmp (p
->param_name
, "ROLE") ||
323 ! strcasecmp (p
->param_name
, "RSVP")) {
325 } else if (! strcasecmp (p
->param_name
, "PARTSTAT")) {
327 v
->value
= strdup (partstat
);
332 remove_contentline (node
);
339 if (! found_my_attendee_line
) {
340 /* Generate and attach an ATTENDEE line for me. */
343 /* Add it after the ORGANIZER line, or if none, BEGIN:VEVENT line. */
344 if ((node
= find_contentline (clines
, "ORGANIZER", 0)) ||
345 (node
= find_contentline (clines
, "BEGIN", "VEVENT"))) {
346 contentline
*new_node
= add_contentline (node
, "ATTENDEE");
348 add_param_name (new_node
, mh_xstrdup ("PARTSTAT"));
349 add_param_value (new_node
, mh_xstrdup (partstat
));
350 add_param_name (new_node
, mh_xstrdup ("CN"));
351 add_param_value (new_node
, mh_xstrdup (getfullname ()));
352 new_node
->value
= concat ("MAILTO:", getlocalmbox (), NULL
);
356 /* Call find_contentline () with node as argument to find multiple
357 matching contentlines. */
359 (node
= find_contentline (node
, "DESCRIPTION", 0));
361 /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
362 remove_contentline (node
);
367 * - Change METHOD from REQUEST to CANCEL.
369 * - Insert action at beginning of SUMMARY value.
370 * - Remove all X- lines.
371 * - Update DTSTAMP with current timestamp.
372 * - Change STATUS from CONFIRMED to CANCELLED.
373 * - Increment value of SEQUENCE.
374 * - Excise VALARM sections.
377 convert_to_cancellation (contentline
*clines
)
381 convert_common (clines
, ACT_CANCEL
);
383 if ((node
= find_contentline (clines
, "STATUS", 0)) &&
384 ! strcasecmp (node
->value
, "CONFIRMED")) {
386 node
->value
= mh_xstrdup ("CANCELLED");
389 if ((node
= find_contentline (clines
, "SEQUENCE", 0))) {
390 int sequence
= atoi (node
->value
);
393 (void) snprintf (buf
, sizeof buf
, "%d", sequence
+ 1);
395 node
->value
= mh_xstrdup (buf
);
400 convert_common (contentline
*clines
, act action
)
405 if ((node
= find_contentline (clines
, "METHOD", 0))) {
407 node
->value
= mh_xstrdup (action
== ACT_CANCEL
? "CANCEL" : "REPLY");
410 if ((node
= find_contentline (clines
, "PRODID", 0))) {
412 node
->value
= mh_xstrdup ("nmh mhical v0.1");
415 if ((node
= find_contentline (clines
, "VERSION", 0))) {
417 inform("Version property is missing value, assume 2.0, continuing...");
418 node
->value
= mh_xstrdup ("2.0");
421 if (strcmp (node
->value
, "2.0")) {
422 inform("supports the Version 2.0 specified by RFC 5545 "
423 "but iCalendar object has Version %s, continuing...",
425 node
->value
= mh_xstrdup ("2.0");
429 if ((node
= find_contentline (clines
, "SUMMARY", 0))) {
434 insert
= "Accepted: ";
437 insert
= "Declined: ";
440 insert
= "Tentative: ";
443 die("Delegate replies are not supported");
446 insert
= "Cancelled:";
453 const size_t len
= strlen (insert
) + strlen (node
->value
) + 1;
454 char *tmp
= mh_xmalloc (len
);
456 (void) strncpy (tmp
, insert
, len
);
457 (void) strncat (tmp
, node
->value
, len
- strlen (insert
) - 1);
461 /* Should never get here. */
462 die("Unknown action: %d", action
);
466 if ((node
= find_contentline (clines
, "DTSTAMP", 0))) {
467 const time_t now
= time (NULL
);
470 if (gmtime_r (&now
, &now_tm
)) {
471 /* 17 would be sufficient given that RFC 5545 § 3.3.4
472 supports only a 4 digit year. */
475 if (strftime (buf
, sizeof buf
, "%Y%m%dT%H%M%SZ", &now_tm
)) {
477 node
->value
= mh_xstrdup (buf
);
479 inform("strftime unable to format current time, continuing...");
482 inform("gmtime_r failed on current time, continuing...");
486 /* Excise X- lines and VALARM section(s). */
488 for (node
= clines
; node
; node
= node
->next
) {
489 /* node->name will be NULL if the line was deleted. */
490 if (! node
->name
) { continue; }
493 if (! strcasecmp ("END", node
->name
) &&
494 ! strcasecmp ("VALARM", node
->value
)) {
497 remove_contentline (node
);
499 if (! strcasecmp ("BEGIN", node
->name
) &&
500 ! strcasecmp ("VALARM", node
->value
)) {
502 remove_contentline (node
);
503 } else if (! strncasecmp ("X-", node
->name
, 2)) {
504 remove_contentline (node
);
510 /* Echo the input, but with unfolded lines. */
512 dump_unfolded (FILE *file
, contentline
*clines
)
516 for (node
= clines
; node
; node
= node
->next
) {
517 fputs (node
->input_line
, file
);
522 output (FILE *file
, contentline
*clines
, int contenttype
)
527 /* Generate a Content-Type header to pass the method parameter
528 to mhbuild. Per RFC 5545 Secs. 6 and 8.1, it must be
529 UTF-8. But we don't attempt to do any conversion of the
531 if ((node
= find_contentline (clines
, "METHOD", 0))) {
533 "Content-Type: text/calendar; method=\"%s\"; "
534 "charset=\"UTF-8\"\n\n",
539 for (node
= clines
; node
; node
= node
->next
) {
544 line
= mh_xstrdup (node
->name
);
545 line
= format_params (line
, node
->params
);
548 line
= mh_xrealloc (line
, len
+ 2);
550 line
[len
+ 1] = '\0';
552 line
= fold (add (node
->value
, line
),
553 clines
->cr_before_lf
== CR_BEFORE_LF
);
556 if (clines
->cr_before_lf
!= LF_ONLY
)
565 * Display these fields of the iCalendar event:
569 * - description, except for "\n\n" and in VALARM
571 * - dtstart in local timezone
572 * - dtend in local timezone
573 * - attendees (limited to number specified in initialization)
576 display (FILE *file
, contentline
*clines
, char *nfs
)
578 tzdesc_t timezones
= load_timezones (clines
);
583 int dat
[5] = { 0, 0, 0, INT_MAX
, 0 };
585 charstring_t buffer
= charstring_create (BUFSIZ
);
586 charstring_t attendees
= charstring_create (BUFSIZ
);
587 const unsigned int max_attendees
= 20;
588 unsigned int num_attendees
;
590 /* Don't call on the END:VCALENDAR line. */
591 if (clines
&& clines
->next
) {
592 (void) fmt_compile (nfs
, &fmt
, 1);
595 if ((c
= fmt_findcomp ("method"))) {
596 if ((node
= find_contentline (clines
, "METHOD", 0)) && node
->value
) {
597 c
->c_text
= mh_xstrdup (node
->value
);
601 if ((c
= fmt_findcomp ("organizer"))) {
602 if ((node
= find_contentline (clines
, "ORGANIZER", 0)) &&
604 c
->c_text
= mh_xstrdup (identity (node
));
608 if ((c
= fmt_findcomp ("summary"))) {
609 if ((node
= find_contentline (clines
, "SUMMARY", 0)) && node
->value
) {
610 c
->c_text
= mh_xstrdup (node
->value
);
614 /* Only display DESCRIPTION lines that are outside VALARM section(s). */
616 if ((c
= fmt_findcomp ("description"))) {
617 for (node
= clines
; node
; node
= node
->next
) {
618 /* node->name will be NULL if the line was deleted. */
619 if (node
->name
&& node
->value
&& ! in_valarm
&&
620 ! strcasecmp ("DESCRIPTION", node
->name
) &&
621 strcasecmp (node
->value
, "\\n\\n")) {
622 c
->c_text
= mh_xstrdup (node
->value
);
623 } else if (in_valarm
) {
624 if (! strcasecmp ("END", node
->name
) &&
625 ! strcasecmp ("VALARM", node
->value
)) {
629 if (! strcasecmp ("BEGIN", node
->name
) &&
630 ! strcasecmp ("VALARM", node
->value
)) {
637 if ((c
= fmt_findcomp ("location"))) {
638 if ((node
= find_contentline (clines
, "LOCATION", 0)) &&
640 c
->c_text
= mh_xstrdup (node
->value
);
644 if ((c
= fmt_findcomp ("dtstart"))) {
645 /* Find DTSTART outsize of a VTIMEZONE section. */
646 in_vtimezone
= false;
647 for (node
= clines
; node
; node
= node
->next
) {
648 /* node->name will be NULL if the line was deleted. */
649 if (! node
->name
) { continue; }
652 if (! strcasecmp ("END", node
->name
) &&
653 ! strcasecmp ("VTIMEZONE", node
->value
)) {
654 in_vtimezone
= false;
657 if (! strcasecmp ("BEGIN", node
->name
) &&
658 ! strcasecmp ("VTIMEZONE", node
->value
)) {
660 } else if (! strcasecmp ("DTSTART", node
->name
)) {
661 /* Got it: DTSTART outside of a VTIMEZONE section. */
662 char *datetime
= format_datetime (timezones
, node
);
663 c
->c_text
= datetime
? datetime
: mh_xstrdup(node
->value
);
669 if ((c
= fmt_findcomp ("dtend"))) {
670 if ((node
= find_contentline (clines
, "DTEND", 0)) && node
->value
) {
671 char *datetime
= format_datetime (timezones
, node
);
672 c
->c_text
= datetime
? datetime
: strdup(node
->value
);
673 } else if ((node
= find_contentline (clines
, "DTSTART", 0)) &&
675 /* There is no DTEND. If there's a DTSTART, use it. If it
676 doesn't have a time, assume that the event is for the
677 entire day and append 23:59:59 to it so that it signifies
678 the end of the day. And assume local timezone. */
679 if (strchr(node
->value
, 'T')) {
680 char * datetime
= format_datetime (timezones
, node
);
681 c
->c_text
= datetime
? datetime
: strdup(node
->value
);
684 contentline node_copy
;
687 node_copy
.value
= concat(node_copy
.value
, "T235959", NULL
);
688 datetime
= format_datetime (timezones
, &node_copy
);
689 c
->c_text
= datetime
? datetime
: strdup(node_copy
.value
);
690 free(node_copy
.value
);
695 if ((c
= fmt_findcomp ("attendees"))) {
696 /* Call find_contentline () with node as argument to find multiple
697 matching contentlines. */
698 charstring_append_cstring (attendees
, "Attendees: ");
699 for (node
= clines
, num_attendees
= 0;
700 (node
= find_contentline (node
, "ATTENDEE", 0)) &&
701 num_attendees
++ < max_attendees
;
703 const char *id
= identity (node
);
705 if (num_attendees
> 1) {
706 charstring_append_cstring (attendees
, ", ");
708 charstring_append_cstring (attendees
, id
);
711 if (num_attendees
>= max_attendees
) {
712 unsigned int not_shown
= 0;
715 (node
= find_contentline (node
, "ATTENDEE", 0));
723 (void) snprintf (buf
, sizeof buf
, ", and %d more", not_shown
);
724 charstring_append_cstring (attendees
, buf
);
728 if (num_attendees
> 0) {
729 c
->c_text
= charstring_buffer_copy (attendees
);
733 /* Don't call on the END:VCALENDAR line. */
734 if (clines
&& clines
->next
) {
735 (void) fmt_scan (fmt
, buffer
, INT_MAX
, dat
, NULL
);
736 fputs (charstring_buffer (buffer
), file
);
740 charstring_free (attendees
);
741 charstring_free (buffer
);
742 free_timezones (timezones
);
746 identity (const contentline
*node
)
748 /* According to RFC 5545 § 3.3.3, an email address in the value
749 must be a mailto URI. */
750 if (! strncasecmp (node
->value
, "mailto:", 7)) {
754 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
757 for (v
= p
->values
; v
; v
= v
->next
) {
758 if (! strcasecmp (p
->param_name
, "CN")) {
764 /* Did not find a CN parameter, so output the address. */
765 addr
= node
->value
+ 7;
767 /* Skip any leading whitespace. */
768 for ( ; isspace ((unsigned char) *addr
); ++addr
) { continue; }
777 format_params (char *line
, param_list
*p
)
779 for ( ; p
&& p
->param_name
; p
= p
->next
) {
781 size_t num_values
= 0;
783 for (v
= p
->values
; v
; v
= v
->next
) {
784 if (v
->value
) { ++num_values
; }
788 size_t len
= strlen (line
);
790 line
= mh_xrealloc (line
, len
+ 2);
792 line
[len
+ 1] = '\0';
794 line
= add (p
->param_name
, line
);
796 for (v
= p
->values
; v
; v
= v
->next
) {
798 line
= mh_xrealloc (line
, len
+ 2);
799 line
[len
] = v
== p
->values
? '=' : ',';
800 line
[len
+ 1] = '\0';
802 line
= add (v
->value
, line
);
811 fold (char *line
, int uses_cr
)
813 size_t remaining
= strlen (line
);
814 size_t current_line_len
= 0;
815 charstring_t folded_line
= charstring_create (2 * remaining
);
816 const char *cp
= line
;
818 #ifdef MULTIBYTE_SUPPORT
819 if (mbtowc (NULL
, NULL
, 0)) {} /* reset shift state */
822 while (*cp
&& remaining
> 0) {
823 #ifdef MULTIBYTE_SUPPORT
824 int char_len
= mbtowc (NULL
, cp
, (size_t) MB_CUR_MAX
< remaining
825 ? (size_t) MB_CUR_MAX
827 if (char_len
== -1) { char_len
= 1; }
829 const int char_len
= 1;
832 charstring_push_back_chars (folded_line
, cp
, char_len
, 1);
833 remaining
-= max(char_len
, 1);
835 /* remaining must be > 0 to pass the loop condition above, so
836 if it's not > 1, it is == 1. */
837 if (++current_line_len
>= 75) {
838 if (remaining
> 1 || (*(cp
+1) != '\0' && *(cp
+1) != '\r' &&
841 if (uses_cr
) { charstring_push_back (folded_line
, '\r'); }
842 charstring_push_back (folded_line
, '\n');
843 charstring_push_back (folded_line
, ' ');
844 current_line_len
= 0;
848 cp
+= max(char_len
, 1);
852 line
= charstring_buffer_copy (folded_line
);
853 charstring_free (folded_line
);