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