/* encode_rfc2047.c -- encode message headers using RFC 2047 encoding.
 *
 * This code is Copyright (c) 2002, 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/mhparse.h>
#include <h/addrsbr.h>
#include <h/utils.h>
#include "unquote.h"

/*
 * List of headers that contain addresses and as a result require special
 * handling
 */

static char *address_headers[] = {
    "To",
    "From",
    "cc",
    "Bcc",
    "Reply-To",
    "Sender",
    "Resent-To",
    "Resent-From",
    "Resent-cc",
    "Resent-Bcc",
    "Resent-Reply-To",
    "Resent-Sender",
    NULL,
};

/*
 * Macros we use for parsing headers
 */

#define is_fws(c) (c == '\t' || c == ' ' || c == '\n')

#define qphrasevalid(c) ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || \
			 (c >= 'a' && c <= 'z') || \
			 c == '!' || c == '*' || c == '+' || c == '-' || \
			 c == '/' || c == '=' || c == '_')
#define qpspecial(c) (c < ' ' || c == '=' || c == '?' || c == '_')

#define base64len(n) ((((n) + 2) / 3 ) * 4)	/* String len to base64 len */
#define strbase64(n) ((n) / 4 * 3)		/* Chars that fit in base64 */

#define ENCODELINELIMIT	76

static void unfold_header(char **, int);
static int field_encode_address(const char *, char **, int, const char *);
static int field_encode_quoted(const char *, char **, const char *, int,
			       int, int);
static int field_encode_base64(const char *, char **, const char *);
static int scanstring(const char *, int *, int *, int *);
static int utf8len(const char *);
static int pref_encoding(int, int, int);

/*
 * Encode a message header using RFC 2047 encoding.  We make the assumption
 * that all characters < 128 are ASCII and as a consequence don't need any
 * encoding.
 */

int
encode_rfc2047(const char *name, char **value, int encoding,
	       const char *charset)
{
    int i, asciicount = 0, eightbitcount = 0, qpspecialcount = 0;
    char *p;

    /*
     * First, check to see if we even need to encode the header
     */

    for (p = *value; *p != '\0'; p++) {
	if (isascii((unsigned char) *p)) {
	    asciicount++;
	    if (qpspecial((unsigned char) *p))
	    	qpspecialcount++;
	} else
	    eightbitcount++;
    }

    if (eightbitcount == 0)
    	return 0;

    /*
     * Some rules from RFC 2047:
     *
     * - Encoded words cannot be more than 75 characters long
     * - Multiple "long" encoded words must be on new lines.
     *
     * Also, we're not permitted to encode email addresses, so
     * we need to actually _parse_ email addresses and only encode
     * the right bits.  
     */

    /*
     * If charset was NULL, then get the value from the locale.  But
     * we reject it if it returns US-ASCII
     */

    if (charset == NULL)
    	charset = write_charset_8bit();

    if (strcasecmp(charset, "US-ASCII") == 0) {
    	inform("Cannot use US-ASCII with 8 bit characters in header");
	return 1;
    }

    /*
     * If we have an address header, then we need to parse the addresses
     * and only encode the names or comments.  Otherwise, handle it normally.
     */

    for (i = 0; address_headers[i]; i++) {
    	if (strcasecmp(name, address_headers[i]) == 0)
	    return field_encode_address(name, value, encoding, charset);
    }

    /*
     * On the encoding we choose, and the specifics of encoding:
     *
     * - If a specified encoding is passed in, we use that.
     * - Otherwise, pick which encoding is shorter.
     *
     * We don't quite handle continuation right here, but it should be
     * pretty close.
     */

    if (encoding == CE_UNKNOWN)
        encoding = pref_encoding(asciicount, qpspecialcount, eightbitcount);

    unfold_header(value, asciicount + eightbitcount);

    switch (encoding) {

    case CE_BASE64:
    	return field_encode_base64(name, value, charset);

    case CE_QUOTED:
	return field_encode_quoted(name, value, charset, asciicount,
				   eightbitcount + qpspecialcount, 0);

    default:
    	inform("Internal error: unknown RFC-2047 encoding type");
	return 1;
    }
}

/*
 * Encode our specified header (or field) using quoted-printable
 */

static int
field_encode_quoted(const char *name, char **value, const char *charset,
		    int ascii, int encoded, int phraserules)
{
    int prefixlen = name ? strlen(name) + 2: 0, outlen = 0, column, newline = 1;
    int charsetlen = strlen(charset), utf8;
    char *output = NULL, *p, *q = NULL;

    /*
     * Right now we just encode the whole thing.  Maybe later on we'll
     * only encode things on a per-atom basis.
     */

    p = *value;

    column = prefixlen + 2;	/* Header name plus ": " */

    utf8 = strcasecmp(charset, "UTF-8") == 0;

    while (*p != '\0') {
    	/*
	 * Start a new line, if it's time
	 */
    	if (newline) {
	    /*
	     * If it's the start of the header, we don't need to pad it
	     *
	     * The length of the output string is ...
	     * =?charset?Q?...?=  so that's 7+strlen(charset) + 2 for \n NUL
	     *
	     * plus 1 for every ASCII character and 3 for every eight bit
	     * or special character (eight bit characters are written as =XX).
	     *
	     */

	    int tokenlen;

	    outlen += 9 + charsetlen + ascii + 3 * encoded;

	    /*
	     * If output is set, then we're continuing the header.  Otherwise
	     * do the initial allocation.
	     */

	    if (output) {
	        int curlen = q - output, i;
		outlen += prefixlen + 1;	/* Header plus \n ": " */
		output = mh_xrealloc(output, outlen);
		q = output + curlen;
		*q++ = '?';
		*q++ = '=';
		*q++ = '\n';
		for (i = 0; i < prefixlen; i++)
		    *q++ = ' ';
	    } else {
	    	/*
		 * A bit of a hack here; the header can contain multiple
		 * spaces (probably at least one) until we get to the
		 * actual text.  Copy until we get to a non-space.
		 */
	    	output = mh_xmalloc(outlen);
		q = output;
		while (is_fws(*p))
		    *q++ = *p++;
	    }

	    tokenlen = snprintf(q, outlen - (q - output), "=?%s?Q?", charset);
	    q += tokenlen;
	    column = prefixlen + tokenlen;
	    newline = 0;
	}

	/*
	 * Process each character, encoding if necessary
	 *
	 * Note that we have a different set of rules if we're processing
	 * RFC 5322 'phrase' (something you'd see in an address header).
	 */

	column++;

	if (*p == ' ') {
	    *q++ = '_';
	    ascii--;
	} else if (isascii((unsigned char) *p) &&
		   (phraserules ? qphrasevalid((unsigned char) *p) :
		   			!qpspecial((unsigned char) *p))) {
	    *q++ = *p;
	    ascii--;
	} else {
	    snprintf(q, outlen - (q - output), "=%02X", (unsigned char) *p);
	    q += 3;
	    column += 2;	/* column already incremented by 1 above */
	    encoded--;
	}

	p++;

	/*
	 * We're not allowed more than ENCODELINELIMIT characters per line,
	 * so reserve some room for the final ?=.
	 *
	 * If prefixlen == 0, we haven't been passed in a header name, so
	 * don't ever wrap the field (we're likely doing an address).
	 */

	if (prefixlen == 0)
	    continue;

	if (column >= ENCODELINELIMIT - 2) {
	    newline = 1;
	} else if (utf8) {
	    /*
	     * Okay, this is a bit weird, but to explain a bit more ...
	     *
	     * RFC 2047 prohibits the splitting of multibyte characters
	     * across encoded words.  Right now we only handle the case
	     * of UTF-8, the most common multibyte encoding.
	     *
	     * p is now pointing at the next input character.  If we're
	     * using UTF-8 _and_ we'd go over ENCODELINELIMIT given the
	     * length of the complete character, then trigger a newline
	     * now.  Note that we check the length * 3 since we have to
	     * allow for the encoded output.
	     */
	    if (column + (utf8len(p) * 3) > ENCODELINELIMIT - 2) {
		newline = 1;
	    }
	}
    }

    if (q == NULL) {
	/* This should never happen, but just in case.  Found by
	   clang static analyzer. */
	inform("null output encoding for %s, continuing...", *value);
	return 1;
    }
    *q++ = '?';
    *q++ = '=';

    if (prefixlen)
	*q++ = '\n';

    *q = '\0';

    free(*value);

    *value = output;

    return 0;
}

/*
 * Encode our specified header (or field) using base64.
 *
 * This is a little easier since every character gets encoded, we can
 * calculate the line wrap up front.
 */

static int
field_encode_base64(const char *name, char **value, const char *charset)
{
    int prefixlen = name ? strlen(name) + 2 : 0, charsetlen = strlen(charset);
    int outlen = 0, numencode, curlen;
    char *output = NULL, *p = *value, *q = NULL, *linestart = NULL;

    /*
     * Skip over any leading white space.
     */

    while (*p == ' ' || *p == '\t')
    	p++;

    /*
     * If we had a zero-length prefix, then just encode the whole field
     * as-is, without line wrapping.  Note that in addition to the encoding
     *
     * The added length we need is =? + charset + ?B? ... ?=
     *
     * That's 7 + strlen(charset) + 2 (for \n NUL).
     */

    while (prefixlen && ((base64len(strlen(p)) + 7 + charsetlen +
    			  prefixlen) > ENCODELINELIMIT)) {

	/*
	 * Our very first time, don't pad the line in the front
	 *
	 * Note ENCODELINELIMIT is + 2 because of \n \0
	 */


	if (! output) {
	    outlen += ENCODELINELIMIT + 2;
	    output = q = mh_xmalloc(outlen);
	    linestart = q - prefixlen;	/* Yes, this is intentional */
	} else {
	    int curstart = linestart - output;
	    curlen = q - output;

	    outlen += ENCODELINELIMIT + 2;
	    output = mh_xrealloc(output, outlen);
	    q = output + curlen;
	    linestart = output + curstart;
	}

	/*
	 * We should have enough space now, so prepend the encoding markers
	 * and character set information.  The leading space is intentional.
	 */

	q += snprintf(q, outlen - (q - output), " =?%s?B?", charset);

	/*
         * Find out how much room we have left on the line and see how
         * many characters we can stuff in.  The start of our line
         * is marked by "linestart", so use that to figure out how
         * many characters are left out of ENCODELINELIMIT.  Reserve
         * 2 characters for the end markers and calculate how many
         * characters we can fit into that space given the base64
         * encoding expansion.
	 */

	numencode = strbase64(ENCODELINELIMIT - (q - linestart) - 2);

	if (numencode <= 0) {
	    inform("Internal error: tried to encode %d characters "
	    	   "in base64", numencode);
	    return 1;
	}

	/*
	 * RFC 2047 prohibits spanning multibyte characters across tokens.
	 * Right now we only check for UTF-8.
	 *
	 * So note the key here ... we want to make sure the character BEYOND
	 * our last character is not a continuation byte.  If it's the start
	 * of a new multibyte character or a single-byte character, that's ok.
	 */

	if (strcasecmp(charset, "UTF-8") == 0) {
	    /*
	     * p points to the start of our current buffer, so p + numencode
	     * is one past the last character to encode
	     */

	    while (numencode > 0 && ((*(p + numencode) & 0xc0) == 0x80))
	    	numencode--;

	    if (numencode == 0) {
	    	inform("Internal error: could not find start of "
		       "UTF-8 character when base64 encoding header");
		return 1;
	    }
	}

	if (writeBase64raw((unsigned char *) p, numencode,
			   (unsigned char *) q) != OK) {
	    inform("Internal error: base64 encoding of header failed");
	    return 1;
	}

	p += numencode;
	q += base64len(numencode);

	/*
	 * This will point us at the beginning of the new line (trust me).
	 */

	linestart = q + 3;

	/*
	 * What's going on here?  Well, we know we're continuing to the next
	 * line, so we want to add continuation padding.  We also add the
	 * trailing marker for the RFC 2047 token at this time as well.
	 * This uses a trick of snprintf(); we tell it to print a zero-length
	 * string, but pad it out to prefixlen - 1 characters; that ends
	 * up always printing out the requested number of spaces.  We use
	 * prefixlen - 1 because we always add a space on the starting
	 * token marker; this makes things work out correctly for the first
	 * line, which should have a space between the ':' and the start
	 * of the token.
	 *
	 * It's okay if you don't follow all of that.
	 */

	q += snprintf(q, outlen - (q - output), "?=\n%*s", prefixlen - 1, "");
    }

    /*
     * We're here if there is either no prefix, or we can fit it in less
     * than ENCODELINELIMIT characters.  Encode the whole thing.
     */

    outlen += prefixlen + 9 + charsetlen + base64len(strlen(p));
    curlen = q - output;

    output = mh_xrealloc(output, outlen);
    q = output + curlen;

    q += snprintf(q, outlen - (q - output), "%s=?%s?B?",
    		  prefixlen ? " " : "", charset);

    if (writeBase64raw((unsigned char *) p, strlen(p),
    		       (unsigned char *) q) != OK) {
	inform("Internal error: base64 encoding of header failed");
	return 1;
    }

    strcat(q, "?=");

    if (prefixlen)
    	strcat(q, "\n");

    free(*value);

    *value = output;

    return 0;
}

/*
 * Calculate the length of a UTF-8 character.
 *
 * If it's not a UTF-8 character (or we're in the middle of a multibyte
 * character) then simply return 0.
 */

static int
utf8len(const char *p)
{
    int len = 1;

    if (*p == '\0')
    	return 0;

    if (isascii((unsigned char) *p) || (((unsigned char) *p) & 0xc0) == 0x80)
    	return 0;

    p++;
    while ((((unsigned char) *p++) & 0xc0) == 0x80)
    	len++;

    return len;
}

/*
 * "Unfold" a header, making it a single line (without continuation)
 *
 * We cheat a bit here; we never make the string longer, so using the
 * original length here is fine.
 */

static void
unfold_header(char **value, int len)
{
    char *str = mh_xmalloc(len + 1);
    char *p = str, *q = *value;

    while (*q != '\0') {
    	if (*q == '\n') {
	    /*
	     * When we get a newline, skip to the next non-whitespace
	     * character and add a space to replace all of the whitespace
	     *
	     * This has the side effect of stripping off the final newline
	     * for the header; we put it back in the encoding routine.
	     */
	    while (is_fws(*q))
	    	q++;
	    if (*q == '\0')
	    	break;

	    *p++ = ' ';
	} else {
	    *p++ = *q++;
	}
    }

    *p = '\0';

    free(*value);
    *value = str;
}

/*
 * Decode a header containing addresses.  This means we have to parse
 * each address and only encode the display-name or comment field.
 */

static int
field_encode_address(const char *name, char **value, int encoding,
		     const char *charset)
{
    int prefixlen = strlen(name) + 2, column = prefixlen, groupflag;
    int asciichars, specialchars, eightbitchars, reformat = 0, errflag = 0;
    size_t len;
    char *mp, *cp = NULL, *output = NULL;
    char *tmpbuf = NULL;
    size_t tmpbufsize = 0;
    struct mailname *mn;
    char errbuf[BUFSIZ];

    /*
     * Because these are addresses, we need to handle them individually.
     *
     * Break them down and process them one by one.  This means we have to
     * rewrite the whole header, but that's unavoidable.
     */

    /*
     * The output headers always have to start with a space first; this
     * is just the way the API works right now.
     */

    output = add(" ", output);

    for (groupflag = 0; (mp = getname(*value)); ) {
    	if ((mn = getm(mp, NULL, 0, errbuf, sizeof(errbuf))) == NULL) {
	    inform("%s: %s", errbuf, mp);
	    errflag++;
	    continue;
	}

	reformat = 0;

	/*
	 * We only care if the phrase (m_pers) or any trailing comment
	 * (m_note) have 8-bit characters.  If doing q-p, we also need
	 * to encode anything marked as qspecial().  Unquote it first
	 * so the specialchars count is right.
	 */

	if (! mn->m_pers)
	    goto check_note;

	if ((len = strlen(mn->m_pers)) + 1 > tmpbufsize) {
	    tmpbuf = mh_xrealloc(tmpbuf, tmpbufsize = len + 1);
	}

	unquote_string(mn->m_pers, tmpbuf);

	if (scanstring(tmpbuf, &asciichars, &eightbitchars,
		       &specialchars)) {
	    /*
	     * If we have 8-bit characters, encode it.
	     */

	    if (encoding == CE_UNKNOWN)
	    	encoding = pref_encoding(asciichars, specialchars,
					 eightbitchars);

	    /*
	     * This is okay, because the output of unquote_string will be either
	     * equal or shorter than the original.
	     */

	    strcpy(mn->m_pers, tmpbuf);

	    switch (encoding) {

	    case CE_BASE64:
	    	if (field_encode_base64(NULL, &mn->m_pers, charset)) {
		    errflag++;
		    goto out;
		}
		break;

	    case CE_QUOTED:
	    	if (field_encode_quoted(NULL, &mn->m_pers, charset, asciichars,
					eightbitchars + specialchars, 1)) {
		    errflag++;
		    goto out;
		}
		break;

	    default:
		inform("Internal error: unknown RFC-2047 encoding type");
		errflag++;
		goto out;
	    }

	    reformat++;
	}

	check_note:

	/*
	 * The "note" field is generally a comment at the end of the address,
	 * at least as how it's implemented here.  Notes are always surrounded
	 * by parenthesis (since they're comments).  Strip them out and
	 * then put them back when we format the final field, but they do
	 * not get encoded.
	 */

	if (! mn->m_note)
	    goto do_reformat;

	if ((len = strlen(mn->m_note)) + 1 > tmpbufsize) {
	    tmpbuf = mh_xrealloc(tmpbuf, tmpbufsize = len + 1);
	}

	if (mn->m_note[0] != '(' || mn->m_note[len - 1] != ')') {
	    inform("Internal error: Invalid note field \"%s\"",
	    	   mn->m_note);
	    errflag++;
	    goto out;
	}

	strncpy(tmpbuf, mn->m_note + 1, len - 1);
	tmpbuf[len - 2] = '\0';

	if (scanstring(tmpbuf, &asciichars, &eightbitchars,
		       &specialchars)) {
	    /*
	     * If we have 8-bit characters, encode it.
	     */

	    if (encoding == CE_UNKNOWN)
	    	encoding = pref_encoding(asciichars, specialchars,
					 eightbitchars);

	    switch (encoding) {

	    case CE_BASE64:
	    	if (field_encode_base64(NULL, &tmpbuf, charset)) {
		    errflag++;
		    goto out;
		}
		break;

	    case CE_QUOTED:
	    	if (field_encode_quoted(NULL, &tmpbuf, charset, asciichars,
					eightbitchars + specialchars, 1)) {
		    errflag++;
		    goto out;
		}
		break;

	    default:
		inform("Internal error: unknown RFC-2047 encoding type");
		errflag++;
		goto out;
	    }

	    reformat++;

	    /*
	     * Make sure the size of tmpbuf is correct (it always gets
	     * reallocated in the above functions).
	     */

	    tmpbufsize = strlen(tmpbuf) + 1;

	    /*
	     * Put the note field back surrounded by parenthesis.
	     */

	    mn->m_note = mh_xrealloc(mn->m_note, tmpbufsize + 2);

	    snprintf(mn->m_note, tmpbufsize + 2, "(%s)", tmpbuf);
	}

do_reformat:

	/*
	 * So, some explanation is in order.
	 *
	 * We know we need to rewrite at least one address in the header,
	 * otherwise we wouldn't be here.  If we had to reformat this
	 * particular address, then run it through adrformat().  Otherwise
	 * we can use m_text directly.
	 */

	/*
	 * If we were in a group but are no longer, make sure we add a
	 * semicolon (which needs to be FIRST, as it needs to be at the end
	 * of the last address).
	 */

	if (groupflag && ! mn->m_ingrp) {
	    output = add(";", output);
	    column++;
	}

	groupflag = mn->m_ingrp;

	if (mn->m_gname) {
	    cp = mh_xstrdup(mn->m_gname);
	}

	if (reformat) {
	    cp = add(adrformat(mn), cp);
	} else {
	    cp = add(mn->m_text, cp);
	}

	len = strlen(cp);

	/*
	 * If we're not at the beginning of the line, add a command and
	 * either a space or a newline.
	 */

	if (column != prefixlen) {
	    if (len + column + 2 > OUTPUTLINELEN) {

	    	if ((size_t) (prefixlen + 3) < tmpbufsize)
		    tmpbuf = mh_xrealloc(tmpbuf, tmpbufsize = prefixlen + 3);

		snprintf(tmpbuf, tmpbufsize, ",\n%*s", column = prefixlen, "");
		output = add(tmpbuf, output);
	    } else {
	    	output = add(", ", output);
		column += 2;
	    }
	}

	/*
	 * Finally add the address
	 */

	output = add(cp, output);
	column += len;
	free(cp);
	cp = NULL;
    }

    /*
     * Just in case we're at the end of a list
     */

    if (groupflag) {
	output = add(";", output);
    }

    output = add("\n", output);

    free(*value);
    *value = output;
    output = NULL;

out:
    mh_xfree(tmpbuf);
    mh_xfree(output);

    return errflag > 0;
}

/*
 * Scan a string, check for characters that need to be encoded
 */

static int
scanstring(const char *string, int *asciilen, int *eightbitchars,
	   int *specialchars)
{
    *asciilen = 0;
    *eightbitchars = 0;
    *specialchars = 0;

    for (; *string != '\0'; string++) {
    	if ((isascii((unsigned char) *string))) {
	    (*asciilen)++;
	    /*
	     * So, a space is not a valid phrase character, but we're counting
	     * an exception here, because in q-p a space can be directly
	     * encoded as an underscore.
	     */
	    if (!qphrasevalid((unsigned char) *string) && *string != ' ')
	    	(*specialchars)++;
	} else {
	    (*eightbitchars)++;
	}
    }

    return *eightbitchars > 0;
}

/*
 * This function is to be used to decide which encoding algorithm we should
 * use if one is not given.  Basically, we pick whichever one is the shorter
 * of the two.
 *
 * Arguments are:
 *
 * ascii	- Number of ASCII characters in to-be-encoded string.
 * specials	- Number of ASCII characters in to-be-encoded string that
 *		  still require encoding under quoted-printable.  Note that
 *		  these are included in the "ascii" total.
 * eightbit	- Eight-bit characters in the to-be-encoded string.
 *
 * Returns one of CE_BASE64 or CE_QUOTED.
 */

static int
pref_encoding(int ascii, int specials, int eightbits)
{
    /*
     * The length of the q-p encoding is:
     *
     * ascii - specials + (specials + eightbits) * 3.
     *
     * The length of the base64 encoding is:
     *
     * base64len(ascii + eightbits)	(See macro for details)
     */

    return base64len(ascii + eightbits) < (ascii - specials +
    			(specials + eightbits) * 3) ? CE_BASE64 : CE_QUOTED;
}
