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