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