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