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