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