+/*
+ * mhical.c -- operate on an iCalendar request
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.
+ * See the COPYRIGHT file in the root directory of the nmh
+ * distribution for complete copyright information.
+ */
+
+#include "h/mh.h"
+#include "h/icalendar.h"
+#include "sbr/icalparse.h"
+#include <h/fmt_scan.h>
+#include "h/addrsbr.h"
+#include "h/mts.h"
+#include "h/utils.h"
+#include <time.h>
+
+typedef enum act {
+ ACT_NONE,
+ ACT_ACCEPT,
+ ACE_DECLINE,
+ ACT_TENTATIVE,
+ ACT_DELEGATE,
+ ACT_CANCEL
+} act;
+
+static void convert_to_reply (contentline *, act);
+static void convert_to_cancellation (contentline *);
+static void convert_common (contentline *, act);
+static void dump_unfolded (FILE *, contentline *);
+static void output (FILE *, contentline *, int);
+static void display (FILE *, contentline *, char *);
+static const char *identity (const contentline *);
+static char *format_params (char *, param_list *);
+static char *fold (char *, int);
+
+#define MHICAL_SWITCHES \
+ X("reply accept|decline|tentative", 0, REPLYSW) \
+ X("cancel", 0, CANCELSW) \
+ X("form formatfile", 0, FORMSW) \
+ X("format string", 5, FMTSW) \
+ X("infile", 0, INFILESW) \
+ X("outfile", 0, OUTFILESW) \
+ X("contenttype", 0, CONTENTTYPESW) \
+ X("nocontenttype", 0, NCONTENTTYPESW) \
+ X("unfold", 0, UNFOLDSW) \
+ X("debug", 0, DEBUGSW) \
+ X("version", 0, VERSIONSW) \
+ X("help", 0, HELPSW) \
+
+#define X(sw, minchars, id) id,
+DEFINE_SWITCH_ENUM(MHICAL);
+#undef X
+
+#define X(sw, minchars, id) { sw, minchars, id },
+DEFINE_SWITCH_ARRAY(MHICAL, switches);
+#undef X
+
+vevent vevents = { NULL, NULL, NULL};
+
+int
+main (int argc, char *argv[]) {
+ /* RFC 5322 § 3.3 date-time format, including the optional
+ day-of-week and not including the optional seconds. The
+ zone is required by the RFC but not always output by this
+ format, because RFC 5545 § 3.3.5 allows date-times not
+ bound to any time zone. */
+
+ act action = ACT_NONE;
+ char *infile = NULL, *outfile = NULL;
+ FILE *inputfile = NULL, *outputfile = NULL;
+ int contenttype = 0, unfold = 0;
+ vevent *v, *nextvevent;
+ char *form = "mhical.24hour", *format = NULL;
+ char **argp, **arguments, *cp;
+
+ icaldebug = 0; /* Global provided by bison (with name-prefix "ical"). */
+
+ if (nmh_init(argv[0], 1)) { return 1; }
+
+ arguments = getarguments (invo_name, argc, argv, 1);
+ argp = arguments;
+
+ /*
+ * Parse arguments
+ */
+ while ((cp = *argp++)) {
+ if (*cp == '-') {
+ switch (smatch (++cp, switches)) {
+ case AMBIGSW:
+ ambigsw (cp, switches);
+ done (1);
+ case UNKWNSW:
+ adios (NULL, "-%s unknown", cp);
+
+ case HELPSW: {
+ char buf[128];
+ snprintf (buf, sizeof buf, "%s [switches]", invo_name);
+ print_help (buf, switches, 1);
+ done (0);
+ }
+ case VERSIONSW:
+ print_version(invo_name);
+ done (0);
+ case DEBUGSW:
+ icaldebug = 1;
+ continue;
+
+ case REPLYSW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ if (! strcasecmp (cp, "accept")) {
+ action = ACT_ACCEPT;
+ } else if (! strcasecmp (cp, "decline")) {
+ action = ACE_DECLINE;
+ } else if (! strcasecmp (cp, "tentative")) {
+ action = ACT_TENTATIVE;
+ } else if (! strcasecmp (cp, "delegate")) {
+ action = ACT_DELEGATE;
+ } else {
+ adios (NULL, "Unknown action: %s", cp);
+ }
+ continue;
+
+ case CANCELSW:
+ action = ACT_CANCEL;
+ continue;
+
+ case FORMSW:
+ if (! (form = *argp++) || *form == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+ format = NULL;
+ continue;
+ case FMTSW:
+ if (! (format = *argp++) || *format == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+ form = NULL;
+ continue;
+
+ case INFILESW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ infile = *cp == '-' ? add (cp, NULL) : path (cp, TFILE);
+ continue;
+ case OUTFILESW:
+ if (! (cp = *argp++) || (*cp == '-' && cp[1]))
+ adios (NULL, "missing argument to %s", argp[-2]);
+ outfile = *cp == '-' ? add (cp, NULL) : path (cp, TFILE);
+ continue;
+
+ case CONTENTTYPESW:
+ contenttype = 1;
+ continue;
+ case NCONTENTTYPESW:
+ contenttype = 0;
+ continue;
+
+ case UNFOLDSW:
+ unfold = 1;
+ continue;
+ }
+ }
+ }
+
+ free (arguments);
+
+ if (infile) {
+ if ((inputfile = fopen (infile, "r"))) {
+ icalset_inputfile (inputfile);
+ } else {
+ adios (infile, "error opening");
+ }
+ } else {
+ inputfile = stdin;
+ }
+
+ if (outfile) {
+ if ((outputfile = fopen (outfile, "w"))) {
+ icalset_outputfile (outputfile);
+ } else {
+ adios (outfile, "error opening");
+ }
+ } else {
+ outputfile = stdout;
+ }
+
+ vevents.last = &vevents;
+ /* vevents is accessed by parser as global. */
+ icalparse ();
+
+ for (v = &vevents; v; v = nextvevent) {
+ if (! unfold && v != &vevents && v->contentlines &&
+ v->contentlines->name &&
+ strcasecmp (v->contentlines->name, "END") &&
+ v->contentlines->value &&
+ strcasecmp (v->contentlines->value, "VCALENDAR")) {
+ /* Output blank line between vevents. Not before
+ first vevent and not after last. */
+ putc ('\n', outputfile);
+ }
+
+ if (action == ACT_NONE) {
+ if (unfold) {
+ dump_unfolded (outputfile, v->contentlines);
+ } else {
+ char *nfs = new_fs (form, format, NULL);
+
+ display (outputfile, v->contentlines, nfs);
+ free_fs ();
+ }
+ } else {
+ if (action == ACT_CANCEL) {
+ convert_to_cancellation (v->contentlines);
+ } else {
+ convert_to_reply (v->contentlines, action);
+ }
+ output (outputfile, v->contentlines, contenttype);
+ }
+
+ free_contentlines (v->contentlines);
+ nextvevent = v->next;
+ if (v != &vevents) {
+ free (v);
+ }
+ }
+
+ if (infile) {
+ if (fclose (inputfile) != 0) {
+ advise (infile, "error closing");
+ }
+ free (infile);
+ }
+ if (outfile) {
+ if (fclose (outputfile) != 0) {
+ advise (outfile, "error closing");
+ }
+ free (outfile);
+ }
+
+ return 0;
+}
+
+/*
+ * - Change METHOD from REQUEST to REPLY.
+ * - Change PRODID.
+ * - Remove all ATTENDEE lines for other users (based on ismymbox ()).
+ * - For the user's ATTENDEE line:
+ * - Remove ROLE and RSVP parameters.
+ * - Change PARTSTAT value to indicate reply action, e.g., ACCEPTED,
+ * DECLINED, or TENTATIVE.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Remove all DESCRIPTION lines.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_reply (contentline *clines, act action) {
+ char *partstat = NULL;
+ int found_my_attendee_line = 0;
+ contentline *node;
+
+ convert_common (clines, action);
+
+ switch (action) {
+ case ACT_ACCEPT:
+ partstat = "ACCEPTED";
+ break;
+ case ACE_DECLINE:
+ partstat = "DECLINED";
+ break;
+ case ACT_TENTATIVE:
+ partstat = "TENTATIVE";
+ break;
+ default:
+ ;
+ }
+
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ for (node = clines;
+ (node = find_contentline (node, "ATTENDEE", 0));
+ node = node->next) {
+ param_list *p;
+
+ ismymbox (NULL); /* need to prime ismymbox() */
+
+ /* According to RFC 5545 § 3.3.3, an email address in the
+ value must be a mailto URI. */
+ if (! strncasecmp (node->value, "mailto:", 7)) {
+ char *addr = node->value + 7;
+ struct mailname *mn;
+
+ /* Skip any leading whitespace. */
+ for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+ addr = getname (addr);
+ mn = getm (addr, NULL, 0, NULL, 0);
+
+ /* Need to flush getname after use. */
+ while (getname ("")) { continue; }
+
+ if (ismymbox (mn)) {
+ found_my_attendee_line = 1;
+ for (p = node->params; p && p->param_name; p = p->next) {
+ value_list *v;
+
+ for (v = p->values; v; v = v->next) {
+ if (! strcasecmp (p->param_name, "ROLE") ||
+ ! strcasecmp (p->param_name, "RSVP")) {
+ remove_value (v);
+ } else if (! strcasecmp (p->param_name, "PARTSTAT")) {
+ free (v->value);
+ v->value = strdup (partstat);
+ }
+ }
+ }
+ } else {
+ remove_contentline (node);
+ }
+
+ mnfree (mn);
+ }
+ }
+
+ if (! found_my_attendee_line) {
+ /* Generate and attach an ATTENDEE line for me. */
+ contentline *node;
+
+ /* Add it after the ORGANIZER line, or if none, BEGIN:VEVENT line. */
+ if ((node = find_contentline (clines, "ORGANIZER", 0)) ||
+ (node = find_contentline (clines, "BEGIN", "VEVENT"))) {
+ contentline *new_node = add_contentline (node, "ATTENDEE");
+
+ add_param_name (new_node, strdup ("PARTSTAT"));
+ add_param_value (new_node, strdup (partstat));
+ add_param_name (new_node, strdup ("CN"));
+ add_param_value (new_node, strdup (getfullname ()));
+ new_node->value = concat ("MAILTO:", getlocalmbox (), NULL);
+ }
+ }
+
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ for (node = clines;
+ (node = find_contentline (node, "DESCRIPTION", 0));
+ node = node->next) {
+ /* ACCEPT, at least, replies don't seem to have DESCRIPTIONS. */
+ remove_contentline (node);
+ }
+}
+
+/*
+ * - Change METHOD from REQUEST to CANCEL.
+ * - Change PRODID.
+ * - Insert action at beginning of SUMMARY value.
+ * - Remove all X- lines.
+ * - Update DTSTAMP with current timestamp.
+ * - Change STATUS from CONFIRMED to CANCELLED.
+ * - Increment value of SEQUENCE.
+ * - Excise VALARM sections.
+ */
+static void
+convert_to_cancellation (contentline *clines) {
+ contentline *node;
+
+ convert_common (clines, ACT_CANCEL);
+
+ if ((node = find_contentline (clines, "STATUS", 0)) &&
+ ! strcasecmp (node->value, "CONFIRMED")) {
+ free (node->value);
+ node->value = strdup ("CANCELLED");
+ }
+
+ if ((node = find_contentline (clines, "SEQUENCE", 0))) {
+ int sequence = atoi (node->value);
+ char buf[32];
+
+ (void) snprintf (buf, sizeof buf, "%d", sequence + 1);
+ free (node->value);
+ node->value = strdup (buf);
+ }
+}
+
+static void
+convert_common (contentline *clines, act action) {
+ contentline *node;
+ int in_valarm;
+
+ if ((node = find_contentline (clines, "METHOD", 0))) {
+ free (node->value);
+ node->value = strdup (action == ACT_CANCEL ? "CANCEL" : "REPLY");
+ }
+
+ if ((node = find_contentline (clines, "PRODID", 0))) {
+ free (node->value);
+ node->value = strdup ("nmh mhical v0.1");
+ }
+
+ if ((node = find_contentline (clines, "VERSION", 0))) {
+ if (! node->value) {
+ admonish (NULL, "Version property is missing value, assume 2.0");
+ node->value = strdup ("2.0");
+ }
+
+ if (strcmp (node->value, "2.0")) {
+ admonish (NULL, "supports the Version 2.0 specified by RFC 5545 "
+ "but iCalendar object has Version %s", node->value);
+ node->value = strdup ("2.0");
+ }
+ }
+
+ if ((node = find_contentline (clines, "SUMMARY", 0))) {
+ char *insert = NULL;
+
+ switch (action) {
+ case ACT_ACCEPT:
+ insert = "Accepted: ";
+ break;
+ case ACE_DECLINE:
+ insert = "Declined: ";
+ break;
+ case ACT_TENTATIVE:
+ insert = "Tentative: ";
+ break;
+ case ACT_DELEGATE:
+ adios (NULL, "Delegate replies are not supported");
+ break;
+ case ACT_CANCEL:
+ insert = "Cancelled:";
+ break;
+ default:
+ ;
+ }
+
+ if (insert) {
+ const size_t len = strlen (insert) + strlen (node->value) + 1;
+ char *tmp = mh_xmalloc (len);
+
+ (void) strncpy (tmp, insert, len);
+ (void) strncat (tmp, node->value, len - strlen (insert) - 1);
+ free (node->value);
+ node->value = tmp;
+ } else {
+ /* Should never get here. */
+ adios (NULL, "Unknown action: %d", action);
+ }
+ }
+
+ if ((node = find_contentline (clines, "DTSTAMP", 0))) {
+ const time_t now = time (NULL);
+ struct tm now_tm;
+
+ if (gmtime_r (&now, &now_tm)) {
+ /* 17 would be sufficient given that RFC 5545 § 3.3.4
+ supports only a 4 digit year. */
+ char buf[32];
+
+ if (strftime (buf, sizeof buf, "%Y%m%dT%H%M%SZ", &now_tm)) {
+ free (node->value);
+ node->value = strdup (buf);
+ } else {
+ admonish (NULL, "strftime unable to format current time");
+ }
+ } else {
+ admonish (NULL, "gmtime_r failed on current time");
+ }
+ }
+
+ /* Excise X- lines and VALARM section(s). */
+ in_valarm = 0;
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (! node->name) { continue; }
+
+ if (in_valarm) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 0;
+ }
+ remove_contentline (node);
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 1;
+ remove_contentline (node);
+ } else if (! strncasecmp ("X-", node->name, 2)) {
+ remove_contentline (node);
+ }
+ }
+ }
+}
+
+/* Echo the input, but with unfolded lines. */
+static void
+dump_unfolded (FILE *file, contentline *clines) {
+ contentline *node;
+
+ for (node = clines; node; node = node->next) {
+ fputs (node->input_line, file);
+ }
+}
+
+static void
+output (FILE *file, contentline *clines, int contenttype) {
+ contentline *node;
+
+ if (contenttype) {
+ /* Generate a Content-Type header to pass the method parameter
+ to mhbuild. Per RFC 5545 Secs. 6 and 8.1, it must be
+ UTF-8. But we don't attempt to do any conversion of the
+ input. */
+ if ((node = find_contentline (clines, "METHOD", 0))) {
+ fprintf (file,
+ "Content-Type: text/calendar; method=\"%s\"; "
+ "charset=\"UTF-8\"\n\n",
+ node->value);
+ }
+ }
+
+ for (node = clines; node; node = node->next) {
+ if (node->name) {
+ char *line = NULL;
+ size_t len;
+
+ line = strdup (node->name);
+ line = format_params (line, node->params);
+
+ len = strlen (line);
+ line = mh_xrealloc (line, len + 2);
+ line[len] = ':';
+ line[len + 1] = '\0';
+
+ line = fold (add (node->value, line),
+ clines->cr_before_lf == CR_BEFORE_LF);
+
+ if (clines->cr_before_lf == LF_ONLY) {
+ fprintf (file, "%s\n", line);
+ } else {
+ fprintf (file, "%s\r\n", line);
+ }
+ free (line);
+ }
+ }
+}
+
+/*
+ * Display these fields of the iCalendar event:
+ * - method
+ * - organizer
+ * - summary
+ * - description, except for "\n\n" and in VALARM
+ * - location
+ * - dtstart in local timezone
+ * - dtend in local timezone
+ * - attendees (limited to number specified in initialization)
+ */
+static void
+display (FILE *file, contentline *clines, char *nfs) {
+ tzdesc_t timezones = load_timezones (clines);
+ int in_vtimezone;
+ int in_valarm;
+ contentline *node;
+ struct format *fmt;
+ int dat[5] = { 0, 0, 0, INT_MAX, 0 };
+ struct comp *c;
+ charstring_t buffer = charstring_create (BUFSIZ);
+ charstring_t attendees = charstring_create (BUFSIZ);
+ const unsigned int max_attendees = 20;
+ unsigned int num_attendees;
+
+ /* Don't call on the END:VCALENDAR line. */
+ if (clines && clines->next) {
+ (void) fmt_compile (nfs, &fmt, 1);
+ }
+
+ if ((c = fmt_findcomp ("method"))) {
+ if ((node = find_contentline (clines, "METHOD", 0)) && node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ if ((c = fmt_findcomp ("organizer"))) {
+ if ((node = find_contentline (clines, "ORGANIZER", 0)) &&
+ node->value) {
+ c->c_text = strdup (identity (node));
+ }
+ }
+
+ if ((c = fmt_findcomp ("summary"))) {
+ if ((node = find_contentline (clines, "SUMMARY", 0)) && node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ /* Only display DESCRIPTION lines that are outside VALARM section(s). */
+ in_valarm = 0;
+ if ((c = fmt_findcomp ("description"))) {
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (node->name && node->value && ! in_valarm &&
+ ! strcasecmp ("DESCRIPTION", node->name) &&
+ strcasecmp (node->value, "\\n\\n")) {
+ c->c_text = strdup (node->value);
+ } else if (in_valarm) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 0;
+ }
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VALARM", node->value)) {
+ in_valarm = 1;
+ }
+ }
+ }
+ }
+
+ if ((c = fmt_findcomp ("location"))) {
+ if ((node = find_contentline (clines, "LOCATION", 0)) &&
+ node->value) {
+ c->c_text = strdup (node->value);
+ }
+ }
+
+ if ((c = fmt_findcomp ("dtstart"))) {
+ /* Find DTSTART outsize of a VTIMEZONE section. */
+ in_vtimezone = 0;
+ for (node = clines; node; node = node->next) {
+ /* node->name will be NULL if the line was deleted. */
+ if (! node->name) { continue; }
+
+ if (in_vtimezone) {
+ if (! strcasecmp ("END", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+ in_vtimezone = 0;
+ }
+ } else {
+ if (! strcasecmp ("BEGIN", node->name) &&
+ ! strcasecmp ("VTIMEZONE", node->value)) {
+ in_vtimezone = 1;
+ } else if (! strcasecmp ("DTSTART", node->name)) {
+ /* Got it: DTSTART outside of a VTIMEZONE section. */
+ char *datetime = format_datetime (timezones, node);
+ c->c_text = datetime ? datetime : node->value;
+ }
+ }
+ }
+ }
+
+ if ((c = fmt_findcomp ("dtend"))) {
+ if ((node = find_contentline (clines, "DTEND", 0)) && node->value) {
+ char *datetime = format_datetime (timezones, node);
+ c->c_text = datetime ? datetime : node->value;
+ }
+ }
+
+ if ((c = fmt_findcomp ("attendees"))) {
+ /* Call find_contentline () with node as argument to find multiple
+ matching contentlines. */
+ charstring_append_cstring (attendees, "Attendees: ");
+ for (node = clines, num_attendees = 0;
+ (node = find_contentline (node, "ATTENDEE", 0)) &&
+ num_attendees++ < max_attendees;
+ node = node->next) {
+ const char *id = identity (node);
+
+ if (num_attendees > 1) {
+ charstring_append_cstring (attendees, ", ");
+ }
+ charstring_append_cstring (attendees, id);
+ }
+
+ if (num_attendees >= max_attendees) {
+ unsigned int not_shown = 0;
+
+ for ( ;
+ (node = find_contentline (node, "ATTENDEE", 0));
+ node = node->next) {
+ ++not_shown;
+ }
+
+ if (not_shown > 0) {
+ char buf[32];
+
+ (void) snprintf (buf, sizeof buf, ", and %d more", not_shown);
+ charstring_append_cstring (attendees, buf);
+ }
+ }
+
+ if (num_attendees > 0) {
+ c->c_text = charstring_buffer_copy (attendees);
+ }
+ }
+
+ /* Don't call on the END:VCALENDAR line. */
+ if (clines->next) {
+ (void) fmt_scan (fmt, buffer, INT_MAX, dat, NULL);
+ fputs (charstring_buffer (buffer), file);
+ fmt_free (fmt, 1);
+ }
+
+ charstring_free (attendees);
+ charstring_free (buffer);
+ free_timezones (timezones);
+}
+
+static const char *
+identity (const contentline *node) {
+ /* According to RFC 5545 § 3.3.3, an email address in the value
+ must be a mailto URI. */
+ if (! strncasecmp (node->value, "mailto:", 7)) {
+ char *addr;
+ param_list *p;
+
+ for (p = node->params; p && p->param_name; p = p->next) {
+ value_list *v;
+
+ for (v = p->values; v; v = v->next) {
+ if (! strcasecmp (p->param_name, "CN")) {
+ return v->value;
+ }
+ }
+ }
+
+ /* Did not find a CN parameter, so output the address. */
+ addr = node->value + 7;
+
+ /* Skip any leading whitespace. */
+ for ( ; isspace ((unsigned char) *addr); ++addr) { continue; }
+
+ return addr;
+ }
+
+ return "unknown";
+}
+
+static char *
+format_params (char *line, param_list *p) {
+ for ( ; p && p->param_name; p = p->next) {
+ value_list *v;
+ size_t num_values = 0;
+
+ for (v = p->values; v; v = v->next) {
+ if (v->value) { ++num_values; }
+ }
+
+ if (num_values) {
+ size_t len = strlen (line);
+
+ line = mh_xrealloc (line, len + 2);
+ line[len] = ';';
+ line[len + 1] = '\0';
+
+ line = add (p->param_name, line);
+
+ for (v = p->values; v; v = v->next) {
+ len = strlen (line);
+ line = mh_xrealloc (line, len + 2);
+ line[len] = v == p->values ? '=' : ',';
+ line[len + 1] = '\0';
+
+ line = add (v->value, line);
+ }
+ }
+ }
+
+ return line;
+}
+
+static char *
+fold (char *line, int uses_cr) {
+ size_t remaining = strlen (line);
+ size_t current_line_len = 0;
+ charstring_t folded_line = charstring_create (2 * remaining);
+ const char *cp = line;
+
+#ifdef MULTIBYTE_SUPPORT
+ if (mbtowc (NULL, NULL, 0)) {} /* reset shift state */
+#endif
+
+ while (*cp && remaining > 0) {
+#ifdef MULTIBYTE_SUPPORT
+ int char_len = mbtowc (NULL, cp, (size_t) MB_CUR_MAX < remaining
+ ? (size_t) MB_CUR_MAX
+ : remaining);
+ if (char_len == -1) { char_len = 1; }
+#else
+ const int char_len = 1;
+#endif
+
+ charstring_push_back_chars (folded_line, cp, char_len, 1);
+ remaining -= char_len > 0 ? char_len : 1;
+
+ /* remaining must be > 0 to pass the loop condition above, so
+ if it's not > 1, it is == 1. */
+ if (++current_line_len >= 75) {
+ if (remaining > 1 || (*(cp+1) != '\0' && *(cp+1) != '\r' &&
+ *(cp+1) != '\n')) {
+ /* fold */
+ if (uses_cr) { charstring_push_back (folded_line, '\r'); }
+ charstring_push_back (folded_line, '\n');
+ charstring_push_back (folded_line, ' ');
+ current_line_len = 0;
+ }
+ }
+
+ cp += char_len > 0 ? char_len : 1;
+ }
+
+ free (line);
+ line = charstring_buffer_copy (folded_line);
+ charstring_free (folded_line);
+
+ return line;
+}