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