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