/*
 * Locking here is not quite right.
 * Calling qlock(&z->lk) can block the proc,
 * and when it comes back, boxes and msgs might have been freed
 * (if the refresh proc was holding the lock and in the middle of a
 * redial).  I've tried to be careful about not assuming boxes continue
 * to exist across imap commands, but maybe this isn't really tenable.
 * Maybe instead we should ref count the boxes and messages.
 */

#include "a.h"
#include <libsec.h>

struct Imap
{
	int		connected;
	int		autoreconnect;
	int		ticks;	/* until boom! */
	char*	server;
	char*	root;
	int		mode;
	int		fd;
	Biobuf	b;
	Ioproc*	io;
	QLock	lk;
	QLock	rlk;
	Rendez	r;

	Box*		inbox;
	Box*		box;
	Box*		nextbox;

	/* SEARCH results */
	uint		*uid;
	uint		nuid;
};

static struct {
	char *name;
	int flag;
} flagstab[] = 
{
	"Junk",	FlagJunk,
	"NonJunk",	FlagNonJunk,
	"\\Answered",	FlagReplied,
	"\\Flagged",	FlagFlagged,
	"\\Deleted",	FlagDeleted,
	"\\Draft",		FlagDraft,
	"\\Recent",	FlagRecent,
	"\\Seen",		FlagSeen,
	"\\NoInferiors",	FlagNoInferiors,
	"\\NoSelect",	FlagNoSelect,
	"\\Marked",	FlagMarked,
	"\\UnMarked",	FlagUnMarked
};

int			chattyimap;

static char	*tag = "#";

static void	checkbox(Imap*, Box*);
static char*	copyaddrs(Sx*);
static void	freeup(UserPasswd*);
static int		getbox(Imap*, Box*);
static int		getboxes(Imap*);
static char*	gsub(char*, char*, char*);
static int		imapcmd(Imap*, Box*, char*, ...);
static Sx*		imapcmdsx(Imap*, Box*, char*, ...);
static Sx*		imapcmdsx0(Imap*, char*, ...);
static Sx*		imapvcmdsx(Imap*, Box*, char*, va_list);
static Sx*		imapvcmdsx0(Imap*, char*, va_list);
static int		imapdial(char*, int);
static int		imaplogin(Imap*);
static int		imapquote(Fmt*);
static int		imapreconnect(Imap*);
static void	imaprefreshthread(void*);
static void	imaptimerproc(void*);
static Sx*		imapwaitsx(Imap*);
static int		isatom(Sx *v, char *name);
static int		islist(Sx *v);
static int		isnil(Sx *v);
static int		isnumber(Sx *sx);
static int		isstring(Sx *sx);
static int		ioimapdial(Ioproc*, char*, int);
static char*	nstring(Sx*);
static void	unexpected(Imap*, Sx*);
static Sx*		zBrdsx(Imap*);

/*
 * Imap connection maintenance and login.
 */

Imap*
imapconnect(char *server, int mode, char *root)
{
	Imap *z;

	fmtinstall('H', encodefmt);
	fmtinstall('Z', imapquote);

	z = emalloc(sizeof *z);
	z->server = estrdup(server);
	z->mode = mode;
	if(root)
		if(root[0] != 0 && root[strlen(root)-1] != '/')
			z->root = smprint("%s/", root);
		else
			z->root = root;
	else
		z->root = "";
	z->fd = -1;
	z->autoreconnect = 0;
	z->io = ioproc();
	
	qlock(&z->lk);
	if(imapreconnect(z) < 0){
		free(z);
		return nil;
	}
	
	z->r.l = &z->rlk;
	z->autoreconnect = 1;
	qunlock(&z->lk);
	
	proccreate(imaptimerproc, z, STACK);
	mailthread(imaprefreshthread, z);
	
	return z;
}

void
imaphangup(Imap *z, int ticks)
{
	z->ticks = ticks;
	if(ticks == 0){
		close(z->fd);
		z->fd = -1;
	}
}

static int
imapreconnect(Imap *z)
{
	Sx *sx;

	z->autoreconnect = 0;
	z->box = nil;
	z->inbox = nil;

	if(z->fd >= 0){
		close(z->fd);
		z->fd = -1;
	}

	if(chattyimap)
		fprint(2, "dial %s...\n", z->server);
	if((z->fd = ioimapdial(z->io, z->server, z->mode)) < 0)
		return -1;
	z->connected = 1;
	Binit(&z->b, z->fd, OREAD);
	if((sx = zBrdsx(z)) == nil){
		werrstr("no greeting");
		goto err;
	}
	if(chattyimap)
		fprint(2, "<I %#$\n", sx);
	if(sx->nsx >= 2 && isatom(sx->sx[0], "*") && isatom(sx->sx[1], "PREAUTH")){
		freesx(sx);
		goto preauth;
	}
	if(!oksx(sx)){
		werrstr("bad greeting - %#$", sx);
		goto err;
	}
	freesx(sx);
	sx = nil;
	if(imaplogin(z) < 0)
		goto err;
preauth:
	if(getboxes(z) < 0 || getbox(z, z->inbox) < 0)
		goto err;
	z->autoreconnect = 1;
	return 0;

err:
	if(z->fd >= 0){
		close(z->fd);
		z->fd = -1;
	}
	if(sx)
		freesx(sx);
	z->autoreconnect = 1;
	z->connected = 0;
	return -1;
}

static int
imaplogin(Imap *z)
{
	Sx *sx;
	UserPasswd *up;

	if((up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", z->server)) == nil){
		werrstr("getuserpasswd - %r");
		return -1;
	}

	sx = imapcmdsx(z, nil, "LOGIN %Z %Z", up->user, up->passwd);
	freeup(up);
	if(sx == nil)
		return -1;
	if(!oksx(sx)){
		freesx(sx);
		werrstr("login rejected - %#$", sx);
		return -1;
	}
	return 0;
}

static int
getboxes(Imap *z)
{
	int i;
	Box **r, **w, **e;
	
	for(i=0; i<nboxes; i++){
		boxes[i]->mark = 1;
		boxes[i]->exists = 0;
		boxes[i]->maxseen = 0;
	}
	if(imapcmd(z, nil, "LIST %Z *", z->root) < 0)
		return -1;
	if(z->root != nil && imapcmd(z, nil, "LIST %Z INBOX", "") < 0)
		return -1;
	if(z->nextbox && z->nextbox->mark)
		z->nextbox = nil;
	for(r=boxes, w=boxes, e=boxes+nboxes; r<e; r++){
		if((*r)->mark)
{fprint(2, "*** free box %s %s\n", (*r)->name, (*r)->imapname);
			boxfree(*r);
}
		else
			*w++ = *r;
	}
	nboxes = w - boxes;
	return 0;
}

static int
getbox(Imap *z, Box *b)
{
	int i;
	Msg **r, **w, **e;
	
	if(b == nil)
		return 0;
	
	for(i=0; i<b->nmsg; i++)
		b->msg[i]->imapid = 0;
	if(imapcmd(z, b, "UID FETCH 1:* FLAGS") < 0)
		return -1;
	for(r=b->msg, w=b->msg, e=b->msg+b->nmsg; r<e; r++){
		if((*r)->imapid == 0)
			msgfree(*r);
		else{
			(*r)->ix = w-b->msg;
			*w++ = *r;
		}
	}
	b->nmsg = w - b->msg;
	b->imapinit = 1;
	checkbox(z, b);
	return 0;
}

static void
freeup(UserPasswd *up)
{
	memset(up->user, 0, strlen(up->user));
	memset(up->passwd, 0, strlen(up->passwd));
	free(up);
}

static void
imaptimerproc(void *v)
{
	Imap *z;
	
	z = v;
	for(;;){
		sleep(60*1000);
		qlock(z->r.l);
		rwakeup(&z->r);
		qunlock(z->r.l);
	}
}

static void
checkbox(Imap *z, Box *b)
{
	if(imapcmd(z, b, "NOOP") >= 0){
		if(!b->imapinit)
			getbox(z, b);
		if(!b->imapinit)
			return;
		if(b==z->box && b->exists > b->maxseen){
			imapcmd(z, b, "UID FETCH %d:* FULL",
				b->uidnext);
		}
	}
}

static void
imaprefreshthread(void *v)
{
	Imap *z;
	
	z = v;
	for(;;){
		qlock(z->r.l);
		rsleep(&z->r);
		qunlock(z->r.l);
		
		qlock(&z->lk);
		if(z->inbox)
			checkbox(z, z->inbox);
		qunlock(&z->lk);
	}
}

/*
 * Run a single command and return the Sx.  Does NOT redial.
 */
static Sx*
imapvcmdsx0(Imap *z, char *fmt, va_list arg)
{
	char *s;
	Fmt f;
	int prefix, len;
	Sx *sx;
	
	if(canqlock(&z->lk))
		abort();

	if(z->fd < 0 || !z->connected)
		return nil;

	prefix = strlen(tag)+1;
	fmtstrinit(&f);
	fmtprint(&f, "%s ", tag);
	fmtvprint(&f, fmt, arg);
	fmtprint(&f, "\r\n");
	s = fmtstrflush(&f);
	len = strlen(s);
	s[len-2] = 0;
	if(chattyimap)
		fprint(2, "I> %s\n", s);
	s[len-2] = '\r';
	if(iowrite(z->io, z->fd, s, len) < 0){
		z->connected = 0;
		free(s);
		return nil;
	}
	sx = imapwaitsx(z);
	free(s);
	return sx;
}

static Sx*
imapcmdsx0(Imap *z, char *fmt, ...)
{
	va_list arg;
	Sx *sx;
	
	va_start(arg, fmt);
	sx = imapvcmdsx0(z, fmt, arg);
	va_end(arg);
	return sx;
}

/*
 * Run a single command on box b.  Does redial.
 */
static Sx*
imapvcmdsx(Imap *z, Box *b, char *fmt, va_list arg)
{
	int tries;
	Sx *sx;
	
	tries = 0;
	z->nextbox = b;

	if(z->fd < 0 || !z->connected){
reconnect:
		if(!z->autoreconnect)
			return nil;
		if(imapreconnect(z) < 0)
			return nil;
		if(b && z->nextbox == nil)	/* box disappeared on reconnect */
			return nil;
	}

	if(b && b != z->box){
		if(z->box)
			z->box->imapinit = 0;
		z->box = b;
		if((sx=imapcmdsx0(z, "SELECT %Z", b->imapname)) == nil){
			z->box = nil;
			if(tries++ == 0 && (z->fd < 0 || !z->connected))
				goto reconnect;
			return nil;
		}
		freesx(sx);
	}

	if((sx=imapvcmdsx0(z, fmt, arg)) == nil){
		if(tries++ == 0 && (z->fd < 0 || !z->connected))
			goto reconnect;
		return nil;
	}
	return sx;
}

static int
imapcmd(Imap *z, Box *b, char *fmt, ...)
{
	Sx *sx;
	va_list arg;

	va_start(arg, fmt);
	sx = imapvcmdsx(z, b, fmt, arg);
	va_end(arg);
	if(sx == nil)
		return -1;
	if(sx->nsx < 2 || !isatom(sx->sx[1], "OK")){
		werrstr("%$", sx);
		freesx(sx);
		return -1;
	}
	freesx(sx);
	return 0;
}

static Sx*
imapcmdsx(Imap *z, Box *b, char *fmt, ...)
{
	Sx *sx;
	va_list arg;

	va_start(arg, fmt);
	sx = imapvcmdsx(z, b, fmt, arg);
	va_end(arg);
	return sx;
}

static Sx*
imapwaitsx(Imap *z)
{
	Sx *sx;
	
	while((sx = zBrdsx(z)) != nil){
		if(chattyimap)
			fprint(2, "<| %#$\n", sx);
		if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && cistrcmp(sx->sx[0]->data, tag) == 0)
			return sx;
		if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && strcmp(sx->sx[0]->data, "*") == 0)
			unexpected(z, sx);
		if(sx->type == SxList && sx->nsx == 0){
			freesx(sx);
			break;
		}
		freesx(sx);
	}
	z->connected = 0;
	return nil;
}

/*
 * Imap interface to mail file system.
 */

static void
_bodyname(char *buf, char *ebuf, Part *p, char *extra)
{
	if(buf >= ebuf){
		fprint(2, "***** BUFFER TOO SMALL\n");
		return;
	}
	*buf = 0;
	if(p->parent){
		_bodyname(buf, ebuf, p->parent, "");
		buf += strlen(buf);
		seprint(buf, ebuf, ".%d", p->pix+1);
	}
	buf += strlen(buf);
	seprint(buf, ebuf, "%s", extra);
}

static char*
bodyname(Part *p, char *extra)
{
	static char buf[256];
	memset(buf, 0, sizeof buf);	/* can't see why this is necessary, but it is */
	_bodyname(buf, buf+sizeof buf, p, extra);
	return buf+1;	/* buf[0] == '.' */
}

static void
fetch1(Imap *z, Part *p, char *s)
{
	qlock(&z->lk);
	imapcmd(z, p->msg->box, "UID FETCH %d BODY[%s]", 
		p->msg->imapuid, bodyname(p, s));
	qunlock(&z->lk);
}

void
imapfetchrawheader(Imap *z, Part *p)
{
	fetch1(z, p, ".HEADER");
}

void
imapfetchrawmime(Imap *z, Part *p)
{
	fetch1(z, p, ".MIME");
}

void
imapfetchrawbody(Imap *z, Part *p)
{
	fetch1(z, p, ".TEXT");
}

void
imapfetchraw(Imap *z, Part *p)
{
	fetch1(z, p, "");
}

static int
imaplistcmd(Imap *z, Box *box, char *before, Msg **m, uint nm, char *after)
{
	int i, r;
	char *cmd;
	Fmt fmt;
	
	if(nm == 0)
		return 0;

	fmtstrinit(&fmt);
	fmtprint(&fmt, "%s ", before);
	for(i=0; i<nm; i++){
		if(i > 0)
			fmtrune(&fmt, ',');
		fmtprint(&fmt, "%ud", m[i]->imapuid);
	}
	fmtprint(&fmt, " %s", after);
	cmd = fmtstrflush(&fmt);
	
	r = 0;
	if(imapcmd(z, box, "%s", cmd) < 0)
		 r = -1;
	free(cmd);
	return r;
}

int
imapcopylist(Imap *z, char *nbox, Msg **m, uint nm)
{
	int rv;
	char *name, *p;
	
	if(nm == 0)
		return 0;

	qlock(&z->lk);
	if(strcmp(nbox, "mbox") == 0)
		name = estrdup("INBOX");
	else{
		p = esmprint("%s%s", z->root, nbox);
		name = esmprint("%Z", p);
		free(p);
	}
	rv = imaplistcmd(z, m[0]->box, "UID COPY", m, nm, name);
	free(name);
	qunlock(&z->lk);
	return rv;
}

int
imapremovelist(Imap *z, Msg **m, uint nm)
{
	int rv;
	
	if(nm == 0)
		return 0;

	qlock(&z->lk);
	rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, "+FLAGS.SILENT (\\Deleted)");
	/* careful - box might be gone; use z->box instead */
	if(rv == 0 && z->box)
		rv = imapcmd(z, z->box, "EXPUNGE");
	qunlock(&z->lk);
	return rv;
}

int
imapflaglist(Imap *z, int op, int flag, Msg **m, uint nm)
{
	char *mod, *s, *sep;
	int i, rv;
	Fmt fmt;
	
	if(op > 0)
		mod = "+";
	else if(op == 0)
		mod = "";
	else
		mod = "-";

	fmtstrinit(&fmt);
	fmtprint(&fmt, "%sFLAGS (", mod);
	sep = "";
	for(i=0; i<nelem(flagstab); i++){
		if(flagstab[i].flag & flag){
			fmtprint(&fmt, "%s%s", sep, flagstab[i].name);
			sep = " ";
		}
	}
	fmtprint(&fmt, ")");
	s = fmtstrflush(&fmt);
	
	qlock(&z->lk);
	rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, s);
	qunlock(&z->lk);
	free(s);
	return rv;
}

int
imapsearchbox(Imap *z, Box *b, char *search, Msg ***mm)
{
	uint *uid;
	int i, nuid;
	Msg **m;
	int nm;

	qlock(&z->lk);
	if(imapcmd(z, b, "UID SEARCH CHARSET UTF-8 TEXT %Z", search) < 0){
		qunlock(&z->lk);
		return -1;
	}

	uid = z->uid;
	nuid = z->nuid;
	z->uid = nil;
	z->nuid = 0;
	qunlock(&z->lk);

	m = emalloc(nuid*sizeof m[0]);
	nm = 0;
	for(i=0; i<nuid; i++)
		if((m[nm] = msgbyimapuid(b, uid[i], 0)) != nil)
			nm++;
	*mm = m;
	free(uid);
	return nm;
}

void
imapcheckbox(Imap *z, Box *b)
{
	if(b == nil)
		return;
	qlock(&z->lk);
	checkbox(z, b);
	qunlock(&z->lk);
}

/*
 * Imap utility routines
 */
static long
_ioimapdial(va_list *arg)
{
	char *server;
	int mode;
	
	server = va_arg(*arg, char*);
	mode = va_arg(*arg, int);
	return imapdial(server, mode);
}
static int
ioimapdial(Ioproc *io, char *server, int mode)
{
	return iocall(io, _ioimapdial, server, mode);
}

static long
_ioBrdsx(va_list *arg)
{
	Biobuf *b;
	Sx **sx;
	
	b = va_arg(*arg, Biobuf*);
	sx = va_arg(*arg, Sx**);
	*sx = Brdsx(b);
	if((*sx) && (*sx)->type == SxList && (*sx)->nsx == 0){
		freesx(*sx);
		*sx = nil;
	}
	return 0;
}
static Sx*
ioBrdsx(Ioproc *io, Biobuf *b)
{
	Sx *sx;

	iocall(io, _ioBrdsx, b, &sx);
	return sx;
}

static Sx*
zBrdsx(Imap *z)
{
	if(z->ticks && --z->ticks==0){
		close(z->fd);
		z->fd = -1;
		return nil;
	}
	return ioBrdsx(z->io, &z->b);
}

static int
imapdial(char *server, int mode)
{
	int p[2];
	int fd[3];
	char *tmp;
	
	switch(mode){
	default:
	case Unencrypted:
		return dial(netmkaddr(server, "tcp", "143"), nil, nil, nil);
		
	case Starttls:
		werrstr("starttls not supported");
		return -1;

	case Tls:
		if(pipe(p) < 0)
			return -1;
		fd[0] = dup(p[0], -1);
		fd[1] = dup(p[0], -1);
		fd[2] = dup(2, -1);
		tmp = esmprint("%s:993", server);
		if(threadspawnl(fd, "/usr/sbin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0
		    && threadspawnl(fd, "/usr/bin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0){
			free(tmp);
			close(p[0]);
			close(p[1]);
			close(fd[0]);
			close(fd[1]);
			close(fd[2]);
			return -1;
		}
		free(tmp);
		close(p[0]);
		return p[1];
	
	case Cmd:
		if(pipe(p) < 0)
			return -1;
		fd[0] = dup(p[0], -1);
		fd[1] = dup(p[0], -1);
		fd[2] = dup(2, -1);
		if(threadspawnl(fd, "/usr/local/plan9/bin/rc", "rc", "-c", server, nil) < 0){
			close(p[0]);
			close(p[1]);
			close(fd[0]);
			close(fd[1]);
			close(fd[2]);
			return -1;
		}
		close(p[0]);
		return p[1];
	}
}

enum
{
	Qok = 0,
	Qquote,
	Qbackslash
};

static int
needtoquote(Rune r)
{
	if(r >= Runeself)
		return Qquote;
	if(r <= ' ')
		return Qquote;
	if(r=='\\' || r=='"')
		return Qbackslash;
	return Qok;
}

static int
imapquote(Fmt *f)
{
	char *s, *t;
	int w, quotes;
	Rune r;

	s = va_arg(f->args, char*);
	if(s == nil || *s == '\0')
		return fmtstrcpy(f, "\"\"");

	quotes = 0;
	if(f->flags&FmtSharp)
		quotes = 1;
	for(t=s; *t; t+=w){
		w = chartorune(&r, t);
		quotes |= needtoquote(r);
	}
	if(quotes == 0)
		return fmtstrcpy(f, s);

	fmtrune(f, '"');
	for(t=s; *t; t+=w){
		w = chartorune(&r, t);
		if(needtoquote(r) == Qbackslash)
			fmtrune(f, '\\');
		fmtrune(f, r);
	}
	return fmtrune(f, '"');
}

static int
fmttype(char c)
{
	switch(c){
	case 'A':
		return SxAtom;
	case 'L':
		return SxList;
	case 'N':
		return SxNumber;
	case 'S':
		return SxString;
	default:
		return -1;
	}
}

/*
 * Check S expression against format string.
 */
static int
sxmatch(Sx *sx, char *fmt)
{
	int i;
	
	for(i=0; fmt[i]; i++){
		if(fmt[i] == '*')
			fmt--;	/* like i-- but better */
		if(i == sx->nsx && fmt[i+1] == '*')
			return 1;
		if(i >= sx->nsx)
			return 0;
		if(sx->sx[i] == nil)
			return 0;
		if(sx->sx[i]->type == SxAtom && strcmp(sx->sx[i]->data, "NIL") == 0){
			if(fmt[i] == 'L'){
				free(sx->sx[i]->data);
				sx->sx[i]->data = nil;
				sx->sx[i]->type = SxList;
				sx->sx[i]->sx = nil;
				sx->sx[i]->nsx = 0;
			}
			else if(fmt[i] == 'S'){
				free(sx->sx[i]->data);
				sx->sx[i]->data = nil;
				sx->sx[i]->type = SxString;
			}
		}
		if(sx->sx[i]->type == SxAtom && fmt[i]=='S')
			sx->sx[i]->type = SxString;
		if(sx->sx[i]->type != fmttype(fmt[i])){
			fprint(2, "sxmatch: %$ not %c\n", sx->sx[i], fmt[i]);
			return 0;
		}
	}
	if(i != sx->nsx)
		return 0;
	return 1;
}

/*
 * Check string against format string.
 */
static int
stringmatch(char *fmt, char *s)
{
	for(; *fmt && *s; fmt++, s++){
		switch(*fmt){
		case '0':
			if(*s == ' ')
				break;
			/* fall through */
		case '1':
			if(*s < '0' || *s > '9')
				return 0;
			break;
		case 'A':
			if(*s < 'A' || *s > 'Z')
				return 0;
			break;
		case 'a':
			if(*s < 'a' || *s > 'z')
				return 0;
			break;
		case '+':
			if(*s != '-' && *s != '+')
				return 0;
			break;
		default:
			if(*s != *fmt)
				return 0;
			break;
		}
	}
	if(*fmt || *s)
		return 0;
	return 1;
}

/*
 * Parse simple S expressions and IMAP elements.
 */
static int
isatom(Sx *v, char *name)
{
	int n;
	
	if(v == nil || v->type != SxAtom)
		return 0;
	n = strlen(name);
	if(cistrncmp(v->data, name, n) == 0)
		if(v->data[n] == 0 || (n>0 && v->data[n-1] == '['))
			return 1;
	return 0;
}

static int
isstring(Sx *sx)
{
	if(sx->type == SxAtom)
		sx->type = SxString;
	return sx->type == SxString;
}

static int
isnumber(Sx *sx)
{
	return sx->type == SxNumber;
}

static int
isnil(Sx *v)
{
	return v == nil || 
		(v->type==SxList && v->nsx == 0) ||
		(v->type==SxAtom && strcmp(v->data, "NIL") == 0);
}

static int
islist(Sx *v)
{
	return isnil(v) || v->type==SxList;
}

static uint
parseflags(Sx *v)
{
	int f, i, j;
	
	if(v->type != SxList){
		warn("malformed flags: %$", v);
		return 0;
	}
	f = 0;
	for(i=0; i<v->nsx; i++){
		if(v->sx[i]->type != SxAtom)
			continue;
		for(j=0; j<nelem(flagstab); j++)
			if(cistrcmp(v->sx[i]->data, flagstab[j].name) == 0)
				f |= flagstab[j].flag;
	}
	return f;
}
	
static char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
static int
parsemon(char *s)
{
	int i;
	
	for(i=0; months[i]; i+=3)
		if(memcmp(s, months+i, 3) == 0)
			return i/3;
	return -1;
}

static uint
parsedate(Sx *v)
{
	Tm tm;
	uint t;
	int delta;
	char *p;
	
	if(v->type != SxString || !stringmatch("01-Aaa-1111 01:11:11 +1111", v->data)){
	bad:
		warn("bad date: %$", v);
		return 0;
	}
	
	/* cannot use atoi because 09 is malformed octal! */
	memset(&tm, 0, sizeof tm);
	p = v->data;
	tm.mday = strtol(p, 0, 10);
	tm.mon = parsemon(p+3);
	if(tm.mon == -1)
		goto bad;
	tm.year = strtol(p+7, 0, 10) - 1900;
	tm.hour = strtol(p+12, 0, 10);
	tm.min = strtol(p+15, 0, 10);
	tm.sec = strtol(p+18, 0, 10);
	strcpy(tm.zone, "GMT");

	t = tm2sec(&tm);
	delta = ((p[22]-'0')*10+p[23]-'0')*3600 + ((p[24]-'0')*10+p[25]-'0')*60;
	if(p[21] == '-')
		delta = -delta;
	
	t -= delta;
	return t;
}

static uint
parsenumber(Sx *v)
{
	if(v->type != SxNumber)
		return 0;
	return v->number;
}

static void
hash(DigestState *ds, char *tag, char *val)
{
	if(val == nil)
		val = "";
	md5((uchar*)tag, strlen(tag)+1, nil, ds);
	md5((uchar*)val, strlen(val)+1, nil, ds);
}

static Hdr*
parseenvelope(Sx *v)
{
	Hdr *hdr;
	uchar digest[16];
	DigestState ds;
	
	if(v->type != SxList || !sxmatch(v, "SSLLLLLLSS")){
		warn("bad envelope: %$", v);
		return nil;
	}

	hdr = emalloc(sizeof *hdr);
	hdr->date = nstring(v->sx[0]);
	hdr->subject = unrfc2047(nstring(v->sx[1]));
	hdr->from = copyaddrs(v->sx[2]);
	hdr->sender = copyaddrs(v->sx[3]);
	hdr->replyto = copyaddrs(v->sx[4]);
	hdr->to = copyaddrs(v->sx[5]);
	hdr->cc = copyaddrs(v->sx[6]);
	hdr->bcc = copyaddrs(v->sx[7]);
	hdr->inreplyto = unrfc2047(nstring(v->sx[8]));
	hdr->messageid = unrfc2047(nstring(v->sx[9]));
	
	memset(&ds, 0, sizeof ds);
	hash(&ds, "date", hdr->date);
	hash(&ds, "subject", hdr->subject);
	hash(&ds, "from", hdr->from);
	hash(&ds, "sender", hdr->sender);
	hash(&ds, "replyto", hdr->replyto);
	hash(&ds, "to", hdr->to);
	hash(&ds, "cc", hdr->cc);
	hash(&ds, "bcc", hdr->bcc);
	hash(&ds, "inreplyto", hdr->inreplyto);
	hash(&ds, "messageid", hdr->messageid);
	md5(0, 0, digest, &ds);
	hdr->digest = esmprint("%.16H", digest);

	return hdr;
}

static void
strlwr(char *s)
{
	char *t;
	
	if(s == nil)
		return;
	for(t=s; *t; t++)
		if('A' <= *t && *t <= 'Z')
			*t += 'a' - 'A';
}

static void
nocr(char *s)
{
	char *r, *w;
	
	if(s == nil)
		return;
	for(r=w=s; *r; r++)
		if(*r != '\r')
			*w++ = *r;
	*w = 0;
}

/*
 * substitute all occurrences of a with b in s.
 */
static char*
gsub(char *s, char *a, char *b)
{
	char *p, *t, *w, *last;
	int n;
	
	n = 0;
	for(p=s; (p=strstr(p, a)) != nil; p+=strlen(a))
		n++;
	if(n == 0)
		return s;
	t = emalloc(strlen(s)+n*strlen(b)+1);
	w = t;
	for(p=s; last=p, (p=strstr(p, a)) != nil; p+=strlen(a)){
		memmove(w, last, p-last);
		w += p-last;
		memmove(w, b, strlen(b));
		w += strlen(b);
	}
	strcpy(w, last);
	free(s);
	return t;
}

/*
 * Table-driven IMAP "unexpected response" parser.
 * All the interesting data is in the unexpected responses.
 */
static void xlist(Imap*, Sx*);
static void xrecent(Imap*, Sx*);
static void xexists(Imap*, Sx*);
static void xok(Imap*, Sx*);
static void xflags(Imap*, Sx*);
static void xfetch(Imap*, Sx*);
static void xexpunge(Imap*, Sx*);
static void xbye(Imap*, Sx*);
static void xsearch(Imap*, Sx*);

static struct {
	int		num;
	char		*name;
	char		*fmt;
	void		(*fn)(Imap*, Sx*);
} unextab[] = {
	0,	"BYE",		nil,			xbye,
	0,	"FLAGS",		"AAL",		xflags,
	0,	"LIST",		"AALSS",		xlist,
	0,	"OK",		nil,			xok,
	0,	"SEARCH",	"AAN*",		xsearch,

	1,	"EXISTS",		"ANA",		xexists,
	1,	"EXPUNGE",	"ANA",		xexpunge,
	1,	"FETCH",		"ANAL",		xfetch,
	1,	"RECENT",	"ANA",		xrecent
};

static void
unexpected(Imap *z, Sx *sx)
{
	int i, num;
	char *name;

	if(sx->nsx >= 3 && sx->sx[1]->type == SxNumber && sx->sx[2]->type == SxAtom){
		num = 1;
		name = sx->sx[2]->data;
	}else if(sx->nsx >= 2 && sx->sx[1]->type == SxAtom){
		num = 0;
		name = sx->sx[1]->data;
	}else
		return;
	
	for(i=0; i<nelem(unextab); i++){
		if(unextab[i].num == num && cistrcmp(unextab[i].name, name) == 0){
			if(unextab[i].fmt && !sxmatch(sx, unextab[i].fmt)){
				warn("malformed %s: %$", name, sx);
				continue;
			}
			unextab[i].fn(z, sx);
		}
	}
}

static int
alldollars(char *s)
{
	for(; *s; s++)
		if(*s != '$')
			return 0;
	return 1;
}

static void
xlist(Imap *z, Sx *sx)
{
	int inbox;
	char *s, *t;
	Box *box;

	s = estrdup(sx->sx[4]->data);
	if(sx->sx[3]->data && strcmp(sx->sx[3]->data, "/") != 0){
		s = gsub(s, "/", "_");
		s = gsub(s, sx->sx[3]->data, "/");
	}

	/*
	 * INBOX is the special imap name for the main mailbox.
	 * All other mailbox names have the root prefix removed, if applicable.
	 */
	inbox = 0;
	if(cistrcmp(s, "INBOX") == 0){
		inbox = 1;
		free(s);
		s = estrdup("mbox");
	} else if(z->root && strstr(s, z->root) == s) {
		t = estrdup(s+strlen(z->root));
		free(s);
		s = t;
	}
	
	/* 
	 * Plan 9 calls the main mailbox mbox.  
	 * Rename any existing mbox by appending a $.
	 */
	if(!inbox && strncmp(s, "mbox", 4) == 0 && alldollars(s+4)){
		t = emalloc(strlen(s)+2);
		strcpy(t, s);
		strcat(t, "$");
		free(s);
		s = t;
	}

	box = boxcreate(s);
	if(box == nil)
		return;
	box->imapname = estrdup(sx->sx[4]->data);
	if(inbox)
		z->inbox = box;
	box->mark = 0;
	box->flags = parseflags(sx->sx[2]);
}

static void
xrecent(Imap *z, Sx *sx)
{
	if(z->box)
		z->box->recent = sx->sx[1]->number;
}

static void
xexists(Imap *z, Sx *sx)
{
	if(z->box){
		z->box->exists = sx->sx[1]->number;
		if(z->box->exists < z->box->maxseen)
			z->box->maxseen = z->box->exists;
	}
}

static void
xflags(Imap *z, Sx *sx)
{
	/*
	 * This response contains in sx->sx[2] the list of flags
	 * that can be validly attached to messages in z->box.
	 * We don't have any use for this list, since we 
	 * use only the standard flags.
	 */
}

static void
xbye(Imap *z, Sx *sx)
{
	close(z->fd);
	z->fd = -1;
	z->connected = 0;
}

static void
xexpunge(Imap *z, Sx *sx)
{
	int i, n;
	Box *b;

	if((b=z->box) == nil)
		return;
	n = sx->sx[1]->number;
	for(i=0; i<b->nmsg; i++){
		if(b->msg[i]->imapid == n){
			msgplumb(b->msg[i], 1);
			msgfree(b->msg[i]);
			b->nmsg--;
			memmove(b->msg+i, b->msg+i+1, (b->nmsg-i)*sizeof b->msg[0]);
			i--;
			b->maxseen--;
			b->exists--;
			continue;
		}
		if(b->msg[i]->imapid > n)
			b->msg[i]->imapid--;
		b->msg[i]->ix = i;
	}
}

static void
xsearch(Imap *z, Sx *sx)
{
	int i;
	
	free(z->uid);
	z->uid = emalloc((sx->nsx-2)*sizeof z->uid[0]);
	z->nuid = sx->nsx-2;
	for(i=0; i<z->nuid; i++)
		z->uid[i] = sx->sx[i+2]->number;
}

/* 
 * Table-driven FETCH message info parser.
 */
static void xmsgflags(Msg*, Sx*, Sx*);
static void xmsgdate(Msg*, Sx*, Sx*);
static void xmsgrfc822size(Msg*, Sx*, Sx*);
static void xmsgenvelope(Msg*, Sx*, Sx*);
static void xmsgbody(Msg*, Sx*, Sx*);
static void xmsgbodydata(Msg*, Sx*, Sx*);

static struct {
	char *name;
	void (*fn)(Msg*, Sx*, Sx*);
} msgtab[] = {
	"FLAGS", xmsgflags,
	"INTERNALDATE", xmsgdate,
	"RFC822.SIZE", xmsgrfc822size,
	"ENVELOPE", xmsgenvelope,
	"BODY", xmsgbody,
	"BODY[", xmsgbodydata
};

static void
xfetch(Imap *z, Sx *sx)
{
	int i, j, n, uid;
	Msg *msg;

	if(z->box == nil){
		warn("FETCH but no open box: %$", sx);
		return;
	}

	/* * 152 FETCH (UID 185 FLAGS () ...) */
	if(sx->sx[3]->nsx%2){
		warn("malformed FETCH: %$", sx);
		return;
	}

	n = sx->sx[1]->number;
	sx = sx->sx[3];
	for(i=0; i<sx->nsx; i+=2){
		if(isatom(sx->sx[i], "UID")){
			if(sx->sx[i+1]->type == SxNumber){
				uid = sx->sx[i+1]->number;
				goto haveuid;
			}
		}
	}
/* This happens: too bad.
	warn("FETCH without UID: %$", sx);
*/
	return;

haveuid:
	msg = msgbyimapuid(z->box, uid, 1);
	if(msg->imapid && msg->imapid != n)
		warn("msg id mismatch: want %d have %d", msg->id, n);
	msg->imapid = n;
	for(i=0; i<sx->nsx; i+=2){
		for(j=0; j<nelem(msgtab); j++)
			if(isatom(sx->sx[i], msgtab[j].name))
				msgtab[j].fn(msg, sx->sx[i], sx->sx[i+1]);
	}
}

static void
xmsgflags(Msg *msg, Sx *k, Sx *v)
{
	USED(k);
	msg->flags = parseflags(v);
}

static void
xmsgdate(Msg *msg, Sx *k, Sx *v)
{
	USED(k);
	msg->date = parsedate(v);
}

static void
xmsgrfc822size(Msg *msg, Sx *k, Sx *v)
{
	USED(k);
	msg->size = parsenumber(v);
}

static char*
nstring(Sx *v)
{
	char *p;
	
	if(isnil(v))
		return estrdup("");
	p = v->data;
	v->data = nil;
	return p;
}

static char*
copyaddrs(Sx *v)
{
	char *s, *sep;
	char *name, *email, *host, *mbox;
	int i;
	Fmt fmt;
	
	if(v->nsx == 0)
		return nil;

	fmtstrinit(&fmt);
	sep = "";
	for(i=0; i<v->nsx; i++){
		if(!sxmatch(v->sx[i], "SSSS"))
			warn("bad address: %$", v->sx[i]);
		name = unrfc2047(nstring(v->sx[i]->sx[0]));
		/* ignore sx[1] - route */
		mbox = unrfc2047(nstring(v->sx[i]->sx[2]));
		host = unrfc2047(nstring(v->sx[i]->sx[3]));
		if(mbox == nil || host == nil){	/* rfc822 group syntax */
			free(name);
			free(mbox);
			free(host);
			continue;
		}
		email = esmprint("%s@%s", mbox, host);
		free(mbox);
		free(host);
		fmtprint(&fmt, "%s%q %q", sep, name ? name : "", email ? email : "");
		free(name);
		free(email);
		sep = " ";
	}
	s = fmtstrflush(&fmt);
	if(s == nil)
		sysfatal("out of memory");
	return s;
}

static void
xmsgenvelope(Msg *msg, Sx *k, Sx *v)
{
	hdrfree(msg->part[0]->hdr);
	msg->part[0]->hdr = parseenvelope(v);
	msgplumb(msg, 0);
}

static struct {
	char *name;
	int offset;
} paramtab[] = {
	"charset",	offsetof(Part, charset),
	"name",		offsetof(Part, filename)
};

static void
parseparams(Part *part, Sx *v)
{
	int i, j;
	char *s, *t, **p;
	
	if(isnil(v))
		return;
	if(v->nsx%2){
		warn("bad message params: %$", v);
		return;
	}
	for(i=0; i<v->nsx; i+=2){
		s = nstring(v->sx[i]);
		t = nstring(v->sx[i+1]);
		for(j=0; j<nelem(paramtab); j++){
			if(cistrcmp(paramtab[j].name, s) == 0){
				p = (char**)((char*)part+paramtab[j].offset);
				free(*p);
				*p = t;
				t = nil;
				break;
			}
		}
		free(s);
		free(t);
	}			
}

static void
parsestructure(Part *part, Sx *v)
{
	int i;
	char *s, *t;
	
	if(isnil(v))
		return;
	if(v->type != SxList){
	bad:
		warn("bad structure: %$", v);
		return;
	}
	if(islist(v->sx[0])){
		/* multipart */
		for(i=0; i<v->nsx && islist(v->sx[i]); i++)
			parsestructure(partcreate(part->msg, part), v->sx[i]);
		free(part->type);
		if(i != v->nsx-1 || !isstring(v->sx[i])){
			warn("bad multipart structure: %$", v);
			part->type = estrdup("multipart/mixed");
			return;
		}
		s = nstring(v->sx[i]);
		strlwr(s);
		part->type = esmprint("multipart/%s", s);
		free(s);
		return;
	}
	/* single part */
	if(!isstring(v->sx[0]) || v->nsx < 2)
		goto bad;
	s = nstring(v->sx[0]);
	t = nstring(v->sx[1]);
	strlwr(s);
	strlwr(t);
	free(part->type);
	part->type = esmprint("%s/%s", s, t);
	if(v->nsx < 7 || !islist(v->sx[2]) || !isstring(v->sx[3]) 
	|| !isstring(v->sx[4]) || !isstring(v->sx[5]) || !isnumber(v->sx[6]))
		goto bad;
	parseparams(part, v->sx[2]);
	part->idstr = nstring(v->sx[3]);
	part->desc = nstring(v->sx[4]);
	part->encoding = nstring(v->sx[5]);
	part->size = v->sx[6]->number;
	if(strcmp(s, "message") == 0 && strcmp(t, "rfc822") == 0){
		if(v->nsx < 10 || !islist(v->sx[7]) || !islist(v->sx[8]) || !isnumber(v->sx[9]))
			goto bad;
		part->hdr = parseenvelope(v->sx[7]);
		parsestructure(partcreate(part->msg, part), v->sx[8]);
		part->lines = v->sx[9]->number;
	}
	if(strcmp(s, "text") == 0){
		if(v->nsx < 8 || !isnumber(v->sx[7]))
			goto bad;
		part->lines = v->sx[7]->number;
	}
}

static void
xmsgbody(Msg *msg, Sx *k, Sx *v)
{
	if(v->type != SxList){
		warn("bad body: %$", v);
		return;
	}
	/*
	 * To follow the structure exactly we should
	 * be doing this to partcreate(msg, msg->part[0]),
	 * and we should leave msg->part[0] with type message/rfc822,
	 * but the extra layer is redundant - what else would be in a mailbox?
	 */
	parsestructure(msg->part[0], v);
	if(msg->box->maxseen < msg->imapid)
		msg->box->maxseen = msg->imapid;
	if(msg->imapuid >= msg->box->uidnext)
		msg->box->uidnext = msg->imapuid+1;
}

static void
xmsgbodydata(Msg *msg, Sx *k, Sx *v)
{
	int i;
	char *name, *p;
	Part *part;
	
	name = k->data;
	name += 5;	/* body[ */
	p = strchr(name, ']');
	if(p)
		*p = 0;
	
	/* now name is something like 1 or 3.2.MIME - walk down parts from root */
	part = msg->part[0];


	while('1' <= name[0] && name[0] <= '9'){
		i = strtol(name, &p, 10);
		if(*p == '.')
			p++;
		else if(*p != 0){
			warn("bad body name: %$", k);
			return;
		}
		if((part = subpart(part, i-1)) == nil){
			warn("unknown body part: %$", k);
			return;
		}
		name = p;
	}


	if(cistrcmp(name, "") == 0){
		free(part->raw);
		part->raw = nstring(v);
		nocr(part->raw);
	}else if(cistrcmp(name, "HEADER") == 0){
		free(part->rawheader);
		part->rawheader = nstring(v);
		nocr(part->rawheader);
	}else if(cistrcmp(name, "MIME") == 0){
		free(part->mimeheader);
		part->mimeheader = nstring(v);
		nocr(part->mimeheader);
	}else if(cistrcmp(name, "TEXT") == 0){
		free(part->rawbody);
		part->rawbody = nstring(v);
		nocr(part->rawbody);
	}
}

/*
 * Table-driven OK info parser.
 */
static void xokuidvalidity(Imap*, Sx*);
static void xokpermflags(Imap*, Sx*);
static void xokunseen(Imap*, Sx*);
static void xokreadwrite(Imap*, Sx*);
static void xokreadonly(Imap*, Sx*);

struct {
	char *name;
	char fmt;
	void (*fn)(Imap*, Sx*);
} oktab[] = {
	"UIDVALIDITY", 'N',	xokuidvalidity,
	"PERMANENTFLAGS", 'L',	xokpermflags,
	"UNSEEN", 'N',	xokunseen,
	"READ-WRITE", 0,	xokreadwrite,
	"READ-ONLY",	0, xokreadonly
};

static void
xok(Imap *z, Sx *sx)
{
	int i;
	char *name;
	Sx *arg;

	if(sx->nsx >= 4 && sx->sx[2]->type == SxAtom && sx->sx[2]->data[0] == '['){
		if(sx->sx[3]->type == SxAtom && sx->sx[3]->data[0] == ']')
			arg = nil;
		else if(sx->sx[4]->type == SxAtom && sx->sx[4]->data[0] == ']')
			arg = sx->sx[3];
		else{
			warn("cannot parse OK: %$", sx);
			return;
		}
		name = sx->sx[2]->data+1;
		for(i=0; i<nelem(oktab); i++){
			if(cistrcmp(name, oktab[i].name) == 0){
				if(oktab[i].fmt && (arg==nil || arg->type != fmttype(oktab[i].fmt))){
					warn("malformed %s: %$", name, arg);
					continue;
				}
				oktab[i].fn(z, arg);
			}
		}
	}
}

static void
xokuidvalidity(Imap *z, Sx *sx)
{
	int i;
	Box *b;
	
	if((b=z->box) == nil)
		return;
	if(b->validity != sx->number){
		b->validity = sx->number;
		b->uidnext = 1;
		for(i=0; i<b->nmsg; i++)
			msgfree(b->msg[i]);
		free(b->msg);
		b->msg = nil;
		b->nmsg = 0;
	}
}

static void
xokpermflags(Imap *z, Sx *sx)
{
/*	z->permflags = parseflags(sx); */
}

static void
xokunseen(Imap *z, Sx *sx)
{
/*	z->unseen = sx->number; */
}

static void
xokreadwrite(Imap *z, Sx *sx)
{
/*	z->boxmode = ORDWR; */
}

static void
xokreadonly(Imap *z, Sx *sx)
{
/*	z->boxmode = OREAD; */
}