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