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