#include <stdio.h>
#include <string.h>
#include <errno.h>

#include "imapfilter.h"
#include "session.h"
#include "buffer.h"


buffer nbuf;			/* Namespace buffer. */
buffer cbuf;			/* Conversion buffer. */


static const char base64[] =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";


const char *apply_conversion(const char *mbox);
const char *reverse_conversion(const char *mbox);


/*
 * Convert the names of personal mailboxes, using the namespace specified
 * by the mail server, from internal to mail server format.
 */
const char *
apply_namespace(const char *mbox, session *ssn)
{
	int n;
	char *c;
	const char *m;

	if (!strcasecmp(mbox, "INBOX"))
		return mbox;

	m = mbox;
	if (!ssn->utf8)
		m = apply_conversion(mbox);

	if ((ssn->ns.prefix == NULL && ssn->ns.delim == '\0') ||
	    (ssn->ns.prefix == NULL && ssn->ns.delim == '/'))
		return m;

	buffer_reset(&nbuf);

	n = snprintf(nbuf.data, nbuf.size + 1, "%s%s",
	    (ssn->ns.prefix ? ssn->ns.prefix : ""), m);
	if (n > (int)nbuf.size) {
		buffer_check(&nbuf, n);
		snprintf(nbuf.data, nbuf.size + 0, "%s%s",
		    (ssn->ns.prefix ? ssn->ns.prefix : ""), m);
	}
	for (c = nbuf.data; (c = strchr(c, '/')) != NULL; *(c++) = ssn->ns.delim);

	debug("namespace: '%s' -> '%s'\n", m, nbuf.data);

	return nbuf.data;
}


/*
 * Convert the names of personal mailboxes, using the namespace specified by
 * the mail server, from mail server format to internal format.
 */
const char *
reverse_namespace(const char *mbox, session *ssn)
{
	int n, o;
	char *c;

	if (!strcasecmp(mbox, "INBOX"))
		return mbox;

	if ((ssn->ns.prefix == NULL && ssn->ns.delim == '\0') ||
	    (ssn->ns.prefix == NULL && ssn->ns.delim == '/')) {
		if (!ssn->utf8)
			return reverse_conversion(mbox);
		return mbox;
	}

	buffer_reset(&nbuf);

	o = strlen(ssn->ns.prefix ? ssn->ns.prefix : "");
	if (strncasecmp(mbox, (ssn->ns.prefix ? ssn->ns.prefix : ""), o))
		o = 0;

	n = snprintf(nbuf.data, nbuf.size + 1, "%s", mbox + o);
	if (n > (int)nbuf.size) {
		buffer_check(&nbuf, n);
		snprintf(nbuf.data, nbuf.size + 1, "%s", mbox + o);
	}
	for (c = nbuf.data; (c = strchr(c, ssn->ns.delim)) != NULL; *(c++) = '/');

	debug("namespace: '%s' <- '%s'\n", mbox, nbuf.data);

	return reverse_conversion(nbuf.data);
}


/*
 * Convert a mailbox name to the modified UTF-7 encoding, according to RFC 3501
 * Section 5.1.3.
 */
const char *
apply_conversion(const char *mbox)
{
	unsigned char *c, *out;
	unsigned char ucp[4], ucplast, ucptemp;
	int padding, shift;

	buffer_check(&nbuf, strlen(mbox));
	buffer_reset(&nbuf);
	xstrncpy(nbuf.data, mbox, nbuf.size);
	nbuf.len = strlen(nbuf.data);
	buffer_check(&cbuf, nbuf.len * 4);
	buffer_reset(&cbuf);

	c = (unsigned char *)nbuf.data;
	out = (unsigned char *)cbuf.data;

	memset((void *)ucp, 0, sizeof(ucp));
	ucplast = ucptemp = 0;
	padding = shift = 0;

	while (*c != '\0' || shift > 0) {
		if (shift > 0 && *c <= 0x7F) {
			/* Last character so do Base64 padding. */
			if (padding == 2) {
				*out++ = base64[ucplast << 2 & 0x3C];
			} else if (padding == 4) {
				*out++ = base64[ucplast << 4 & 0x30];
			}
			*out++ = '-';
			padding = 0;
			shift = 0;
			continue;
		}

		/* Escape shift character for modified UTF-7. */
		if (*c == '&') {
			*out++ = '&';
			*out++ = '-';
			c++;
			continue;

		/* Copy all ASCII printable characters. */
		} else if ((*c >= 0x20 && *c <= 0x7e)) {
			*out++ = *c;
			c++;
			continue;
		}

		/* Non-ASCII UTF-8 characters follow. */
		if (shift == 0)
			*out++ = '&';
		/* Convert UTF-8 characters to Unicode code point. */
		if ((*c & 0xE0) == 0xC0) {
			shift = 2;
			ucp[0] = 0x07 & *c >> 2;
			ucp[1] = (*c << 6 & 0xC0) | (*(c + 1) & 0x3F);
			c += 2;
		} else if ((*c & 0xF0) == 0xE0) {
			shift = 3;
			ucp[0] = (*c << 4 & 0xF0) | (*(c + 1) >> 2 & 0x0F);
			ucp[1] = (*(c + 1) << 6 & 0xC0) | (*(c + 2) & 0x3F);
			c += 3;
		} else if ((*c & 0xF8) == 0xF0) {
			shift = 4;
			ucptemp = ((*c << 2 & 0x1C) | (*(c + 1) >> 4 & 0x03)) -
			    0x01;
			ucp[0] = (ucptemp >> 2 & 0x03) | 0xD8;
			ucp[1] = (ucptemp << 6 & 0xC0) |
			    (*(c + 1) << 2 & 0x3C) | (*(c + 2) >> 4 & 0x03);
			ucp[2] = (*(c + 2) >> 2 & 0x03) | 0xDC;
			ucp[3] = (*(c + 2) << 6 & 0xC0) | (*(c + 3) & 0x3F);
			c += 4;
		}

		/* Convert Unicode characters to UTF-7. */
		if (padding == 0) {
			*out++ = base64[ucp[0] >> 2 & 0x3F];
			*out++ = base64[(ucp[0] << 4 & 0x30) |
			    (ucp[1] >> 4 & 0x0F)];
			if (shift == 4) {
				ucplast = ucp[3];
				*out++ = base64[(ucp[1] << 2 & 0x3C) |
				    (ucp[2] >> 6 & 0x03)];
				*out++ = base64[ucp[2] & 0x3F];
				*out++ = base64[ucp[3] >> 2 & 0x3F];
				padding = 4;
			} else {
				ucplast = ucp[1];
				padding = 2;
			}
		} else if (padding == 2) {
			*out++ = base64[(ucplast << 2 & 0x3C) |
			    (ucp[0] >> 6 & 0x03)];
			*out++ = base64[ucp[0] & 0x3F];
			*out++ = base64[ucp[1] >> 2 & 0x3F];
			if (shift == 4) {
				*out++ = base64[(ucp[1] << 4 & 0x30) |
				    (ucp[2] >> 4 & 0x0F)];
				*out++ = base64[(ucp[2] << 2 & 0x3C) |
				    (ucp[3] >> 6 & 0x03)];
				*out++ = base64[ucp[3] & 0x3F];
				padding = 0;
			} else {
				ucplast = ucp[1];
				padding = 4;
			}
		} else if (padding == 4) {
			*out++ = base64[(ucplast << 4 & 0x30) |
			    (ucp[0] >> 4 & 0x0F)];
			*out++ = base64[(ucp[0] << 2 & 0x3C) |
			    (ucp[1] >> 6 & 0x03)];
			if (shift == 4) {
				ucplast = ucp[3];
				*out++ = base64[ucp[1] & 0x3F];
				*out++ = base64[ucp[2] >> 2 & 0x3F];
				*out++ = base64[(ucp[2] << 4 & 0x30) |
				    (ucp[3] >> 4 & 0x0F)];
				padding = 2;
			} else {
				*out++ = base64[ucp[1] & 0x3F];
				padding = 0;
			}
		}
	}
	*out = '\0';

	debug("conversion: '%s' -> '%s'\n", nbuf.data, cbuf.data);

	return cbuf.data;
}

/*
 * Convert a mailbox name from the modified UTF-7 encoding, according to RFC
 * 3501 Section 5.1.3.
 */
const char *
reverse_conversion(const char *mbox)
{
	unsigned char *c, *out;
	unsigned char ucp[4], ucptemp;
	unsigned char b64[6], b64last;
	int padding;
	
	buffer_check(&cbuf, strlen(mbox));
	buffer_reset(&cbuf);
	xstrncpy(cbuf.data, mbox, cbuf.size);
	cbuf.len = strlen(cbuf.data);
	buffer_check(&nbuf, cbuf.len);
	buffer_reset(&nbuf);

	c = (unsigned char *)cbuf.data;
	out = (unsigned char *)nbuf.data;

	memset((void *)ucp, 0, sizeof(ucp));
	memset((void *)b64, 0, sizeof(b64));
	ucptemp = b64last = 0;
	padding = 0;

	while (*c != '\0') {
		/* Copy all ASCII printable characters. */
		if (*c >= 0x20 && *c <= 0x7e) {
			if (*c != '&') {
				*out++ = *c++;
				continue;
			} else {
				c++;
			}
		}

		/* Write shift character for modified UTF-7. */
		if (*c == '-') {
			*out++ = '&';
			c++;
			continue;
		}

		/* UTF-7 characters follow. */
		padding = 0;
		do {
			/* Read Base64 characters. */
			b64[0] = strchr(base64, *c) - base64;
			b64[1] = strchr(base64, *(c + 1)) - base64;
			if (padding == 0 || padding == 2) {
				b64[2] = strchr(base64, *(c + 2)) - base64;
				c += 3;
			} else {
				c += 2;
			}
			/* Convert from Base64 to Unicode code point. */
			if (padding == 0) {
				ucp[0] = (b64[0] << 2 & 0xFC) |
				    (b64[1] >> 4 & 0x03);
				ucp[1] = (b64[1] << 4 & 0xF0) |
				    (b64[2] >> 2 & 0x0F);
				b64last = b64[2];
				padding = 2;
			} else if (padding == 2) {
				ucp[0] = (b64last << 6 & 0xC0) |
				    (b64[0] & 0x3F);
				ucp[1] = (b64[1] << 2 & 0xFC) |
				    (b64[2] >> 4 & 0x03);
				b64last = b64[2];
				padding = 4;
			} else if (padding == 4) {
				ucp[0] = (b64last << 4 & 0xF0) |
				    (b64[0] >> 2 & 0x0F);
				ucp[1] = (b64[0] << 6 & 0xC0) |
				    (b64[1] & 0x3F);
				padding = 0;
			}

			/* Convert from Unicode to UTF-8. */
			if (ucp[0] <= 0x07) {
				*out++ = 0xC0 | (ucp[0] << 2 & 0x1C) |
				    (ucp[1] >> 6 & 0x03);
				*out++ = 0x80 | (ucp[1] & 0x3F);
			} else if ((ucp[0] >= 0x08 && ucp[0] <= 0xD7) ||
			    ucp[0] >= 0xE0) {
				*out++ = 0xE0 | (ucp[0] >> 4 & 0x0F);
				*out++ = 0x80 | (ucp[0] << 2 & 0x1C) |
				    (ucp[1] >> 6 & 0x03);
				*out++ = 0x80 | (ucp[1] & 0x3F);
			} else if (ucp[0] >= 0xD8 && ucp[0] <= 0xDF) {
				b64[3] = strchr(base64, *c) - base64;
				b64[4] = strchr(base64, *(c + 1)) - base64;
				if (padding == 0 || padding == 2) {
					b64[5] = strchr(base64, *(c + 2)) -
					    base64;
					c += 3;
				} else {
					c += 2;
				}

				if (padding == 0) {
					ucp[2] = (b64[3] << 2 & 0xFC) |
					    (b64[4] >> 4 & 0x03);
					ucp[3] = (b64[4] << 4 & 0xF0) |
					    (b64[5] >> 2 & 0x0F);
					b64last = b64[5];
					padding = 2;
				} else if (padding == 2) {
					ucp[2] = (b64last << 6 & 0xC0) |
					    (b64[3] & 0x3F);
					ucp[3] = (b64[4] << 2 & 0xFC) |
					    (b64[5] >> 4 & 0x03);
					b64last = b64[5];
					padding = 4;
				} else if (padding == 4) {
					ucp[2] = (b64last << 4 & 0xF0) |
					    (b64[3] >> 2 & 0x0F);
					ucp[3] = (b64[3] << 6 & 0xC0) |
					    (b64[4] & 0x3F);
					padding = 0;
				}

				ucp[0] &= 0xFF - 0xDF;
				ucptemp = ((ucp[0] << 2 & 0x0C) |
				    (ucp[1] >> 6 & 0x03)) + 0x1;
				ucp[2] &= 0xFF - 0xDC;

				*out++ = 0xF0 | (ucptemp >> 2 & 0x03);
				*out++ = 0x80 | (ucptemp << 4 & 0x30) |
				    (ucp[1] >> 2 & 0x0F);
				*out++ = 0x80 | (ucp[1] << 4 & 0x30) |
				    (ucp[2] << 2 & 0x0C) |
				    (ucp[3] >> 6 & 0x03);
				*out++ = 0x80 | (ucp[3] & 0x3F);
			}
			if (*c == '-') {
				c++;
				break;
			}
		} while (*c != '-');
	}
	*out = '\0';

	debug("conversion: '%s' <- '%s'\n", nbuf.data, cbuf.data);

	return nbuf.data;
}
