1 /* mhical.c -- operate on an iCalendar request
2  *
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.
6  */
7 
8 #include "h/mh.h"
9 #include "h/icalendar.h"
10 #include "sbr/icalparse.h"
11 #include <h/fmt_scan.h>
12 #include "h/addrsbr.h"
13 #include "h/mts.h"
14 #include "h/utils.h"
15 #include <time.h>
16 
17 typedef enum act {
18     ACT_NONE,
19     ACT_ACCEPT,
20     ACE_DECLINE,
21     ACT_TENTATIVE,
22     ACT_DELEGATE,
23     ACT_CANCEL
24 } act;
25 
26 static void convert_to_reply (contentline *, act);
27 static void convert_to_cancellation (contentline *);
28 static void convert_common (contentline *, act);
29 static void dump_unfolded (FILE *, contentline *);
30 static void output (FILE *, contentline *, int);
31 static void display (FILE *, contentline *, char *);
32 static const char *identity (const contentline *);
33 static char *format_params (char *, param_list *);
34 static char *fold (char *, int);
35 
36 #define MHICAL_SWITCHES \
37     X("reply accept|decline|tentative", 0, REPLYSW) \
38     X("cancel", 0, CANCELSW) \
39     X("form formatfile", 0, FORMSW) \
40     X("format string", 5, FMTSW) \
41     X("infile", 0, INFILESW) \
42     X("outfile", 0, OUTFILESW) \
43     X("contenttype", 0, CONTENTTYPESW) \
44     X("nocontenttype", 0, NCONTENTTYPESW) \
45     X("unfold", 0, UNFOLDSW) \
46     X("debug", 0, DEBUGSW) \
47     X("version", 0, VERSIONSW) \
48     X("help", 0, HELPSW) \
49 
50 #define X(sw, minchars, id) id,
51 DEFINE_SWITCH_ENUM(MHICAL);
52 #undef X
53 
54 #define X(sw, minchars, id) { sw, minchars, id },
55 DEFINE_SWITCH_ARRAY(MHICAL, switches);
56 #undef X
57 
58 vevent vevents = { NULL, NULL, NULL};
59 int parser_status = 0;
60 
61 int
main(int argc,char * argv[])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. */
68 
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;
76 
77     icaldebug = 0;  /* Global provided by bison (with name-prefix "ical"). */
78 
79     if (nmh_init(argv[0], 2)) { return 1; }
80 
81     arguments = getarguments (invo_name, argc, argv, 1);
82     argp = arguments;
83 
84     /*
85      * Parse arguments
86      */
87     while ((cp = *argp++)) {
88         if (*cp == '-') {
89             switch (smatch (++cp, switches)) {
90             case AMBIGSW:
91                 ambigsw (cp, switches);
92                 done (1);
93             case UNKWNSW:
94                 adios (NULL, "-%s unknown", cp);
95 
96             case HELPSW: {
97                 char buf[128];
98                 snprintf (buf, sizeof buf, "%s [switches]", invo_name);
99                 print_help (buf, switches, 1);
100                 done (0);
101             }
102             case VERSIONSW:
103                 print_version(invo_name);
104                 done (0);
105             case DEBUGSW:
106                 icaldebug = 1;
107                 continue;
108 
109             case REPLYSW:
110                 if (! (cp = *argp++) || (*cp == '-' && cp[1]))
111                     adios (NULL, "missing argument to %s", argp[-2]);
112                 if (! strcasecmp (cp, "accept")) {
113                     action = ACT_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;
120                 } else {
121                     adios (NULL, "Unknown action: %s", cp);
122                 }
123                 continue;
124 
125             case CANCELSW:
126                 action = ACT_CANCEL;
127                 continue;
128 
129             case FORMSW:
130                 if (! (form = *argp++) || *form == '-')
131                     adios (NULL, "missing argument to %s", argp[-2]);
132                 format = NULL;
133                 continue;
134             case FMTSW:
135                 if (! (format = *argp++) || *format == '-')
136                     adios (NULL, "missing argument to %s", argp[-2]);
137                 form = NULL;
138                 continue;
139 
140             case INFILESW:
141                 if (! (cp = *argp++) || (*cp == '-' && cp[1]))
142                     adios (NULL, "missing argument to %s", argp[-2]);
143                 infile = *cp == '-'  ?  mh_xstrdup(cp)  :  path (cp, TFILE);
144                 continue;
145             case OUTFILESW:
146                 if (! (cp = *argp++) || (*cp == '-' && cp[1]))
147                     adios (NULL, "missing argument to %s", argp[-2]);
148                 outfile = *cp == '-'  ?  mh_xstrdup(cp)  :  path (cp, TFILE);
149                 continue;
150 
151             case CONTENTTYPESW:
152                 contenttype = 1;
153                 continue;
154             case NCONTENTTYPESW:
155                 contenttype = 0;
156                 continue;
157 
158             case UNFOLDSW:
159                 unfold = 1;
160                 continue;
161             }
162         }
163     }
164 
165     free (arguments);
166 
167     if (infile) {
168         if ((inputfile = fopen (infile, "r"))) {
169             icalset_inputfile (inputfile);
170         } else {
171             adios (infile, "error opening");
172         }
173     } else {
174         inputfile = stdin;
175     }
176 
177     if (outfile) {
178         if ((outputfile = fopen (outfile, "w"))) {
179             icalset_outputfile (outputfile);
180         } else {
181             adios (outfile, "error opening");
182         }
183     } else {
184         outputfile = stdout;
185     }
186 
187     vevents.last = &vevents;
188     /* vevents is accessed by parser as global. */
189     icalparse ();
190 
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);
200         }
201 
202         if (action == ACT_NONE) {
203             if (unfold) {
204                 dump_unfolded (outputfile, v->contentlines);
205             } else {
206                 char *nfs = new_fs (form, format, NULL);
207 
208                 display (outputfile, v->contentlines, nfs);
209                 free_fs ();
210             }
211         } else {
212             if (action == ACT_CANCEL) {
213                 convert_to_cancellation (v->contentlines);
214             } else {
215                 convert_to_reply (v->contentlines, action);
216             }
217             output (outputfile, v->contentlines, contenttype);
218         }
219 
220         free_contentlines (v->contentlines);
221         nextvevent = v->next;
222         if (v != &vevents) {
223             free (v);
224         }
225     }
226 
227     if (infile) {
228         if (fclose (inputfile) != 0) {
229             advise (infile, "error closing");
230         }
231         free (infile);
232     }
233     if (outfile) {
234         if (fclose (outputfile) != 0) {
235             advise (outfile, "error closing");
236         }
237         free (outfile);
238     }
239 
240     return parser_status;
241 }
242 
243 /*
244  * - Change METHOD from REQUEST to REPLY.
245  * - Change PRODID.
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.
256  */
257 static void
convert_to_reply(contentline * clines,act action)258 convert_to_reply (contentline *clines, act action) {
259     char *partstat = NULL;
260     int found_my_attendee_line = 0;
261     contentline *node;
262 
263     convert_common (clines, action);
264 
265     switch (action) {
266     case ACT_ACCEPT:
267         partstat = "ACCEPTED";
268         break;
269     case ACE_DECLINE:
270         partstat = "DECLINED";
271         break;
272     case ACT_TENTATIVE:
273         partstat = "TENTATIVE";
274         break;
275     default:
276         ;
277     }
278 
279     /* Call find_contentline () with node as argument to find multiple
280        matching contentlines. */
281     for (node = clines;
282          (node = find_contentline (node, "ATTENDEE", 0));
283          node = node->next) {
284         param_list *p;
285 
286         ismymbox (NULL); /* need to prime ismymbox() */
287 
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;
292             struct mailname *mn;
293 
294             /* Skip any leading whitespace. */
295             for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
296 
297             addr = getname (addr);
298             mn = getm (addr, NULL, 0, NULL, 0);
299 
300             /* Need to flush getname after use. */
301             while (getname ("")) { continue; }
302 
303             if (ismymbox (mn)) {
304                 found_my_attendee_line = 1;
305                 for (p = node->params; p && p->param_name; p = p->next) {
306                     value_list *v;
307 
308                     for (v = p->values; v; v = v->next) {
309                         if (! strcasecmp (p->param_name, "ROLE")  ||
310                             ! strcasecmp (p->param_name, "RSVP")) {
311                             remove_value (v);
312                         } else if (! strcasecmp (p->param_name, "PARTSTAT")) {
313                             free (v->value);
314                             v->value = strdup (partstat);
315                         }
316                     }
317                 }
318             } else {
319                 remove_contentline (node);
320             }
321 
322             mnfree (mn);
323         }
324     }
325 
326     if (! found_my_attendee_line) {
327         /* Generate and attach an ATTENDEE line for me. */
328         contentline *node;
329 
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");
334 
335             add_param_name (new_node, mh_xstrdup ("PARTSTAT"));
336             add_param_value (new_node, mh_xstrdup (partstat));
337             add_param_name (new_node, mh_xstrdup ("CN"));
338             add_param_value (new_node, mh_xstrdup (getfullname ()));
339             new_node->value = concat ("MAILTO:", getlocalmbox (), NULL);
340         }
341     }
342 
343     /* Call find_contentline () with node as argument to find multiple
344        matching contentlines. */
345     for (node = clines;
346          (node = find_contentline (node, "DESCRIPTION", 0));
347          node = node->next) {
348         /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
349         remove_contentline (node);
350     }
351 }
352 
353 /*
354  * - Change METHOD from REQUEST to CANCEL.
355  * - Change PRODID.
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.
362  */
363 static void
convert_to_cancellation(contentline * clines)364 convert_to_cancellation (contentline *clines) {
365     contentline *node;
366 
367     convert_common (clines, ACT_CANCEL);
368 
369     if ((node = find_contentline (clines, "STATUS", 0))  &&
370         ! strcasecmp (node->value, "CONFIRMED")) {
371         free (node->value);
372         node->value = mh_xstrdup ("CANCELLED");
373     }
374 
375     if ((node = find_contentline (clines, "SEQUENCE", 0))) {
376         int sequence = atoi (node->value);
377         char buf[32];
378 
379         (void) snprintf (buf, sizeof buf, "%d", sequence + 1);
380         free (node->value);
381         node->value = mh_xstrdup (buf);
382     }
383 }
384 
385 static void
convert_common(contentline * clines,act action)386 convert_common (contentline *clines, act action) {
387     contentline *node;
388     int in_valarm;
389 
390     if ((node = find_contentline (clines, "METHOD", 0))) {
391         free (node->value);
392         node->value = mh_xstrdup (action == ACT_CANCEL  ?  "CANCEL"  :  "REPLY");
393     }
394 
395     if ((node = find_contentline (clines, "PRODID", 0))) {
396         free (node->value);
397         node->value = mh_xstrdup ("nmh mhical v0.1");
398     }
399 
400     if ((node = find_contentline (clines, "VERSION", 0))) {
401         if (! node->value) {
402             inform("Version property is missing value, assume 2.0, continuing...");
403             node->value = mh_xstrdup ("2.0");
404         }
405 
406         if (strcmp (node->value, "2.0")) {
407             inform("supports the Version 2.0 specified by RFC 5545 "
408 		"but iCalendar object has Version %s, continuing...",
409 		node->value);
410             node->value = mh_xstrdup ("2.0");
411         }
412     }
413 
414     if ((node = find_contentline (clines, "SUMMARY", 0))) {
415         char *insert = NULL;
416 
417         switch (action) {
418         case ACT_ACCEPT:
419             insert = "Accepted: ";
420             break;
421         case ACE_DECLINE:
422             insert = "Declined: ";
423             break;
424         case ACT_TENTATIVE:
425             insert = "Tentative: ";
426             break;
427         case ACT_DELEGATE:
428             adios (NULL, "Delegate replies are not supported");
429             break;
430         case ACT_CANCEL:
431             insert = "Cancelled:";
432             break;
433         default:
434             ;
435         }
436 
437         if (insert) {
438             const size_t len = strlen (insert) + strlen (node->value) + 1;
439             char *tmp = mh_xmalloc (len);
440 
441             (void) strncpy (tmp, insert, len);
442             (void) strncat (tmp, node->value, len - strlen (insert) - 1);
443             free (node->value);
444             node->value = tmp;
445         } else {
446             /* Should never get here. */
447             adios (NULL, "Unknown action: %d", action);
448         }
449     }
450 
451     if ((node = find_contentline (clines, "DTSTAMP", 0))) {
452         const time_t now = time (NULL);
453         struct tm now_tm;
454 
455         if (gmtime_r (&now, &now_tm)) {
456             /* 17 would be sufficient given that RFC 5545 § 3.3.4
457                supports only a 4 digit year. */
458             char buf[32];
459 
460             if (strftime (buf, sizeof buf, "%Y%m%dT%H%M%SZ", &now_tm)) {
461                 free (node->value);
462                 node->value = mh_xstrdup (buf);
463             } else {
464                 inform("strftime unable to format current time, continuing...");
465             }
466         } else {
467             inform("gmtime_r failed on current time, continuing...");
468         }
469     }
470 
471     /* Excise X- lines and VALARM section(s). */
472     in_valarm = 0;
473     for (node = clines; node; node = node->next) {
474         /* node->name will be NULL if the line was deleted. */
475         if (! node->name) { continue; }
476 
477         if (in_valarm) {
478             if (! strcasecmp ("END", node->name)  &&
479                 ! strcasecmp ("VALARM", node->value)) {
480                 in_valarm = 0;
481             }
482             remove_contentline (node);
483         } else {
484             if (! strcasecmp ("BEGIN", node->name)  &&
485                 ! strcasecmp ("VALARM", node->value)) {
486                 in_valarm = 1;
487                 remove_contentline (node);
488             } else if (! strncasecmp ("X-", node->name, 2)) {
489                 remove_contentline (node);
490             }
491         }
492     }
493 }
494 
495 /* Echo the input, but with unfolded lines. */
496 static void
dump_unfolded(FILE * file,contentline * clines)497 dump_unfolded (FILE *file, contentline *clines) {
498     contentline *node;
499 
500     for (node = clines; node; node = node->next) {
501         fputs (node->input_line, file);
502     }
503 }
504 
505 static void
output(FILE * file,contentline * clines,int contenttype)506 output (FILE *file, contentline *clines, int contenttype) {
507     contentline *node;
508 
509     if (contenttype) {
510         /* Generate a Content-Type header to pass the method parameter
511            to mhbuild.  Per RFC 5545 Secs. 6 and 8.1, it must be
512            UTF-8.  But we don't attempt to do any conversion of the
513            input. */
514         if ((node = find_contentline (clines, "METHOD", 0))) {
515             fprintf (file,
516                      "Content-Type: text/calendar; method=\"%s\"; "
517                      "charset=\"UTF-8\"\n\n",
518                      node->value);
519         }
520     }
521 
522     for (node = clines; node; node = node->next) {
523         if (node->name) {
524             char *line = NULL;
525             size_t len;
526 
527             line = mh_xstrdup (node->name);
528             line = format_params (line, node->params);
529 
530             len = strlen (line);
531             line = mh_xrealloc (line, len + 2);
532             line[len] = ':';
533             line[len + 1] = '\0';
534 
535             line = fold (add (node->value, line),
536                          clines->cr_before_lf == CR_BEFORE_LF);
537 
538             if (clines->cr_before_lf == LF_ONLY) {
539                 fprintf (file, "%s\n", line);
540             } else {
541                 fprintf (file, "%s\r\n", line);
542             }
543             free (line);
544         }
545     }
546 }
547 
548 /*
549  * Display these fields of the iCalendar event:
550  *   - method
551  *   - organizer
552  *   - summary
553  *   - description, except for "\n\n" and in VALARM
554  *   - location
555  *   - dtstart in local timezone
556  *   - dtend in local timezone
557  *   - attendees (limited to number specified in initialization)
558  */
559 static void
display(FILE * file,contentline * clines,char * nfs)560 display (FILE *file, contentline *clines, char *nfs) {
561     tzdesc_t timezones = load_timezones (clines);
562     int in_vtimezone;
563     int in_valarm;
564     contentline *node;
565     struct format *fmt;
566     int dat[5] = { 0, 0, 0, INT_MAX, 0 };
567     struct comp *c;
568     charstring_t buffer = charstring_create (BUFSIZ);
569     charstring_t attendees = charstring_create (BUFSIZ);
570     const unsigned int max_attendees = 20;
571     unsigned int num_attendees;
572 
573     /* Don't call on the END:VCALENDAR line. */
574     if (clines  &&  clines->next) {
575       (void) fmt_compile (nfs, &fmt, 1);
576     }
577 
578     if ((c = fmt_findcomp ("method"))) {
579         if ((node = find_contentline (clines, "METHOD", 0))  &&  node->value) {
580             c->c_text = mh_xstrdup (node->value);
581         }
582     }
583 
584     if ((c = fmt_findcomp ("organizer"))) {
585         if ((node = find_contentline (clines, "ORGANIZER", 0))  &&
586             node->value) {
587             c->c_text = mh_xstrdup (identity (node));
588         }
589     }
590 
591     if ((c = fmt_findcomp ("summary"))) {
592         if ((node = find_contentline (clines, "SUMMARY", 0))  &&  node->value) {
593             c->c_text = mh_xstrdup (node->value);
594         }
595     }
596 
597     /* Only display DESCRIPTION lines that are outside VALARM section(s). */
598     in_valarm = 0;
599     if ((c = fmt_findcomp ("description"))) {
600         for (node = clines; node; node = node->next) {
601             /* node->name will be NULL if the line was deleted. */
602             if (node->name  &&  node->value  &&  ! in_valarm  &&
603                 ! strcasecmp ("DESCRIPTION", node->name)  &&
604                 strcasecmp (node->value, "\\n\\n")) {
605                 c->c_text = mh_xstrdup (node->value);
606             } else if (in_valarm) {
607                 if (! strcasecmp ("END", node->name)  &&
608                     ! strcasecmp ("VALARM", node->value)) {
609                     in_valarm = 0;
610                 }
611             } else {
612                 if (! strcasecmp ("BEGIN", node->name)  &&
613                     ! strcasecmp ("VALARM", node->value)) {
614                     in_valarm = 1;
615                 }
616             }
617         }
618     }
619 
620     if ((c = fmt_findcomp ("location"))) {
621         if ((node = find_contentline (clines, "LOCATION", 0))  &&
622             node->value) {
623             c->c_text = mh_xstrdup (node->value);
624         }
625     }
626 
627     if ((c = fmt_findcomp ("dtstart"))) {
628         /* Find DTSTART outsize of a VTIMEZONE section. */
629         in_vtimezone = 0;
630         for (node = clines; node; node = node->next) {
631             /* node->name will be NULL if the line was deleted. */
632             if (! node->name) { continue; }
633 
634             if (in_vtimezone) {
635                 if (! strcasecmp ("END", node->name)  &&
636                     ! strcasecmp ("VTIMEZONE", node->value)) {
637                     in_vtimezone = 0;
638                 }
639             } else {
640                 if (! strcasecmp ("BEGIN", node->name)  &&
641                     ! strcasecmp ("VTIMEZONE", node->value)) {
642                     in_vtimezone = 1;
643                 } else if (! strcasecmp ("DTSTART", node->name)) {
644                     /* Got it:  DTSTART outside of a VTIMEZONE section. */
645                     char *datetime = format_datetime (timezones, node);
646                     c->c_text = datetime ? datetime : mh_xstrdup(node->value);
647                 }
648             }
649         }
650     }
651 
652     if ((c = fmt_findcomp ("dtend"))) {
653         if ((node = find_contentline (clines, "DTEND", 0))  &&  node->value) {
654             char *datetime = format_datetime (timezones, node);
655             c->c_text = datetime ? datetime : strdup(node->value);
656         } else if ((node = find_contentline (clines, "DTSTART", 0))  &&
657                    node->value) {
658             /* There is no DTEND.  If there's a DTSTART, use it.  If it
659                doesn't have a time, assume that the event is for the
660                entire day and append 23:59:59 to it so that it signifies
661                the end of the day.  And assume local timezone. */
662             if (strchr(node->value, 'T')) {
663                 char * datetime = format_datetime (timezones, node);
664                 c->c_text = datetime ? datetime : strdup(node->value);
665             } else {
666                 char *datetime;
667                 contentline node_copy;
668 
669                 node_copy = *node;
670                 node_copy.value = concat(node_copy.value, "T235959", NULL);
671                 datetime = format_datetime (timezones, &node_copy);
672                 c->c_text = datetime ? datetime : strdup(node_copy.value);
673                 free(node_copy.value);
674             }
675         }
676     }
677 
678     if ((c = fmt_findcomp ("attendees"))) {
679         /* Call find_contentline () with node as argument to find multiple
680            matching contentlines. */
681         charstring_append_cstring (attendees, "Attendees: ");
682         for (node = clines, num_attendees = 0;
683              (node = find_contentline (node, "ATTENDEE", 0))  &&
684                  num_attendees++ < max_attendees;
685              node = node->next) {
686             const char *id = identity (node);
687 
688             if (num_attendees > 1) {
689                 charstring_append_cstring (attendees, ", ");
690             }
691             charstring_append_cstring (attendees, id);
692         }
693 
694         if (num_attendees >= max_attendees) {
695             unsigned int not_shown = 0;
696 
697             for ( ;
698                   (node = find_contentline (node, "ATTENDEE", 0));
699                   node = node->next) {
700                 ++not_shown;
701             }
702 
703             if (not_shown > 0) {
704                 char buf[32];
705 
706                 (void) snprintf (buf, sizeof buf, ", and %d more", not_shown);
707                 charstring_append_cstring (attendees, buf);
708             }
709         }
710 
711         if (num_attendees > 0) {
712             c->c_text = charstring_buffer_copy (attendees);
713         }
714     }
715 
716     /* Don't call on the END:VCALENDAR line. */
717     if (clines  &&  clines->next) {
718       (void) fmt_scan (fmt, buffer, INT_MAX, dat, NULL);
719       fputs (charstring_buffer (buffer), file);
720       fmt_free (fmt, 1);
721     }
722 
723     charstring_free (attendees);
724     charstring_free (buffer);
725     free_timezones (timezones);
726 }
727 
728 static const char *
identity(const contentline * node)729 identity (const contentline *node) {
730     /* According to RFC 5545 § 3.3.3, an email address in the value
731        must be a mailto URI. */
732     if (! strncasecmp (node->value, "mailto:", 7)) {
733         char *addr;
734         param_list *p;
735 
736         for (p = node->params; p && p->param_name; p = p->next) {
737             value_list *v;
738 
739             for (v = p->values; v; v = v->next) {
740                 if (! strcasecmp (p->param_name, "CN")) {
741                     return v->value;
742                 }
743             }
744         }
745 
746         /* Did not find a CN parameter, so output the address. */
747         addr = node->value + 7;
748 
749         /* Skip any leading whitespace. */
750         for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
751 
752         return addr;
753     }
754 
755     return "unknown";
756 }
757 
758 static char *
format_params(char * line,param_list * p)759 format_params (char *line, param_list *p) {
760     for ( ; p && p->param_name; p = p->next) {
761         value_list *v;
762         size_t num_values = 0;
763 
764         for (v = p->values; v; v = v->next) {
765             if (v->value) { ++num_values; }
766         }
767 
768         if (num_values) {
769             size_t len = strlen (line);
770 
771             line = mh_xrealloc (line, len + 2);
772             line[len] = ';';
773             line[len + 1] = '\0';
774 
775             line = add (p->param_name, line);
776 
777             for (v = p->values; v; v = v->next) {
778                 len = strlen (line);
779                 line = mh_xrealloc (line, len + 2);
780                 line[len] = v == p->values ? '=' : ',';
781                 line[len + 1] = '\0';
782 
783                 line = add (v->value, line);
784             }
785         }
786     }
787 
788     return line;
789 }
790 
791 static char *
fold(char * line,int uses_cr)792 fold (char *line, int uses_cr) {
793     size_t remaining = strlen (line);
794     size_t current_line_len = 0;
795     charstring_t folded_line = charstring_create (2 * remaining);
796     const char *cp = line;
797 
798 #ifdef MULTIBYTE_SUPPORT
799     if (mbtowc (NULL, NULL, 0)) {} /* reset shift state */
800 #endif
801 
802     while (*cp  &&  remaining > 0) {
803 #ifdef MULTIBYTE_SUPPORT
804         int char_len = mbtowc (NULL, cp, (size_t) MB_CUR_MAX < remaining
805                                          ? (size_t) MB_CUR_MAX
806                                          : remaining);
807         if (char_len == -1) { char_len = 1; }
808 #else
809         const int char_len = 1;
810 #endif
811 
812         charstring_push_back_chars (folded_line, cp, char_len, 1);
813         remaining -= max(char_len, 1);
814 
815         /* remaining must be > 0 to pass the loop condition above, so
816            if it's not > 1, it is == 1. */
817         if (++current_line_len >= 75) {
818             if (remaining > 1  ||  (*(cp+1) != '\0'  &&  *(cp+1) != '\r'  &&
819                                     *(cp+1) != '\n')) {
820                 /* fold */
821                 if (uses_cr) { charstring_push_back (folded_line, '\r'); }
822                 charstring_push_back (folded_line, '\n');
823                 charstring_push_back (folded_line, ' ');
824                 current_line_len = 0;
825             }
826         }
827 
828         cp += max(char_len, 1);
829     }
830 
831     free (line);
832     line = charstring_buffer_copy (folded_line);
833     charstring_free (folded_line);
834 
835     return line;
836 }
837