shithub: riscv

ref: b5a6dc7849cbd9f1fd23183ba46f0d5deb24e81d
dir: /sys/src/cmd/upas/imap4d/msg.c/

View raw version
#include "imap4d.h"

static	char	*headaddrspec(char*, char*);
static	Maddr	*headaddresses(void);
static	Maddr	*headaddress(void);
static	char	*headatom(char*);
static	int	headchar(int eat);
static	char	*headdomain(char*);
static	Maddr	*headmaddr(Maddr*);
static	char	*headphrase(char*, char*);
static	char	*headquoted(int start, int stop);
static	char	*headskipwhite(int);
static	void	headskip(void);
static	char	*headsubdomain(void);
static	char	*headtext(void);
static	void	headtoend(void);
static	char	*headword(void);
static	void	mimedescription(Header*);
static	void	mimedisposition(Header*);
static	void	mimeencoding(Header*);
static	void	mimeid(Header*);
static	void	mimelanguage(Header*);
//static	void	mimemd5(Header*);
static	void	mimetype(Header*);
static	int	msgbodysize(Msg*);
static	int	msgheader(Msg*, Header*, char*);

/*
 * stop list for header fields
 */
static	char	*headfieldstop = ":";
static	char	*mimetokenstop = "()<>@,;:\\\"/[]?=";
static	char	*headatomstop = "()<>@,;:\\\".[]";
static	uchar	*headstr;
static	uchar	*lastwhite;

long
selectfields(char *dst, long n, char *hdr, Slist *fields, int matches)
{
	char *s;
	uchar *start;
	long m, nf;
	Slist *f;

	headstr = (uchar*)hdr;
	m = 0;
	for(;;){
		start = headstr;
		s = headatom(headfieldstop);
		if(s == nil)
			break;
		headskip();
		for(f = fields; f != nil; f = f->next){
			if(cistrcmp(s, f->s) == !matches){
				nf = headstr - start;
				if(m + nf > n)
					return 0;
				memmove(&dst[m], start, nf);
				m += nf;
			}
		}
		free(s);
	}
	if(m + 3 > n)
		return 0;
	dst[m++] = '\r';
	dst[m++] = '\n';
	dst[m] = '\0';
	return m;
}

static Mimehdr*
mkmimehdr(char *s, char *t, Mimehdr *next)
{
	Mimehdr *mh;

	mh = MK(Mimehdr);
	mh->s = s;
	mh->t = t;
	mh->next = next;
	return mh;
}

static void
freemimehdr(Mimehdr *mh)
{
	Mimehdr *last;

	while(mh != nil){
		last = mh;
		mh = mh->next;
		free(last->s);
		free(last->t);
		free(last);
	}
}

static void
freeheader(Header *h)
{
	freemimehdr(h->type);
	freemimehdr(h->id);
	freemimehdr(h->description);
	freemimehdr(h->encoding);
//	freemimehdr(h->md5);
	freemimehdr(h->disposition);
	freemimehdr(h->language);
	free(h->buf);
}

static void
freemaddr(Maddr *a)
{
	Maddr *p;

	while(a != nil){
		p = a;
		a = a->next;
		free(p->personal);
		free(p->box);
		free(p->host);
		free(p);
	}
}

void
freemsg(Box *box, Msg *m)
{
	Msg *k, *last;

	if(box != nil)
		fstreedelete(box, m);
	free(m->ibuf);
	freemaddr(m->to);
	if(m->replyto != m->from)
		freemaddr(m->replyto);
	if(m->sender != m->from)
		freemaddr(m->sender);
	if(m->from != m->unixfrom)
		freemaddr(m->from);
	freemaddr(m->unixfrom);
	freemaddr(m->cc);
	freemaddr(m->bcc);
	free(m->unixdate);
	freeheader(&m->head);
	freeheader(&m->mime);
	for(k = m->kids; k != nil; ){
		last = k;
		k = k->next;
		freemsg(0, last);
	}
	free(m->fs);
	free(m);
}

uint
msgsize(Msg *m)
{
	return m->head.size + m->size;
}

char*
maddrstr(Maddr *a)
{
	char *host, *addr;

	host = a->host;
	if(host == nil)
		host = "";
	if(a->personal != nil)
		addr = smprint("%s <%s@%s>", a->personal, a->box, host);
	else
		addr = smprint("%s@%s", a->box, host);
	return addr;
}

int
msgfile(Msg *m, char *f)
{
	if(strlen(f) > Filelen)
		bye("internal error: msgfile name too long");
	strcpy(m->efs, f);
	return cdopen(m->fsdir, m->fs, OREAD);
}

int
msgismulti(Header *h)
{
	return h->type != nil && cistrcmp("multipart", h->type->s) == 0;
}

int
msgis822(Header *h)
{
	Mimehdr *t;

	t = h->type;
	return t != nil && cistrcmp("message", t->s) == 0 && cistrcmp("rfc822", t->t) == 0;
}

/*
 * check if a message has been deleted by someone else
 */
void
msgdead(Msg *m)
{
	if(m->expunged)
		return;
	*m->efs = '\0';
	if(!cdexists(m->fsdir, m->fs))
		m->expunged = 1;
}

static long
msgreadfile(Msg *m, char *file, char **ss)
{
	char *s, buf[Bufsize];
	int fd;
	long n, nn;
	vlong length;
	Dir *d;

	fd = msgfile(m, file);
	if(fd < 0){
		msgdead(m);
		return -1;
	}

	n = read(fd, buf, Bufsize);
	if(n < Bufsize){
		close(fd);
		if(n < 0){
			*ss = nil;
			return -1;
		}
		s = emalloc(n + 1);
		memmove(s, buf, n);
		s[n] = '\0';
		*ss = s;
		return n;
	}

	d = dirfstat(fd);
	if(d == nil){
		close(fd);
		return -1;
	}
	length = d->length;
	free(d);
	nn = length;
	s = emalloc(nn + 1);
	memmove(s, buf, n);
	if(nn > n)
		nn = readn(fd, s + n, nn - n) + n;
	close(fd);
	if(nn != length){
		free(s);
		return -1;
	}
	s[nn] = '\0';
	*ss = s;
	return nn;
}

/*
 * parse the address in the unix header
 * last line of defence, so must return something
 */
static Maddr *
unixfrom(char *s)
{
	char *e, *t;
	Maddr *a;

	if(s == nil)
		return nil;
	headstr = (uchar*)s;
	t = emalloc(strlen(s) + 2);
	e = headaddrspec(t, nil);
	if(e == nil)
		a = nil;
	else{
		if(*e != '\0')
			*e++ = '\0';
		else
			e = site;
		a = MKZ(Maddr);
		a->box = estrdup(t);
		a->host = estrdup(e);
	}
	free(t);
	return a;
}

/*
 * retrieve information from the unixheader file
 */
static int
msgunix(Msg *m, int top)
{
	char *s, *ss;
	Tm tm;

	if(m->unixdate != nil)
		return 1;
	if(!top){
bogus:
		m->unixdate = estrdup("");
		m->unixfrom = unixfrom(nil);
		return 1;
	}

	if(msgreadfile(m, "unixheader", &ss) < 0)
		goto bogus;
	s = ss;
	s = strchr(s, ' ');
	if(s == nil){
		free(ss);
		goto bogus;
	}
	s++;
	m->unixfrom = unixfrom(s);
	s = (char*)headstr;
	if(date2tm(&tm, s) == nil)
		s = m->info[Iunixdate];
	if(s == nil){
		free(ss);
		goto bogus;
	}
	m->unixdate = estrdup(s);
	free(ss);
	return 1;
}

/*
 * make sure the message has valid associated info
 * used for Isubject, Idigest, Iinreplyto, Imessageid.
 */
int
msginfo(Msg *m)
{
	char *s;
	int i;

	if(m->info[0] != nil)
		return 1;
	if(msgreadfile(m, "info", &m->ibuf) < 0)
		return 0;
	s = m->ibuf;
	for(i = 0; i < Imax; i++){
		m->info[i] = s;
		s = strchr(s, '\n');
		if(s == nil)
			return 0;
		if(s == m->info[i])
			m->info[i] = 0;
		*s++ = '\0';
	}
//	m->lines = strtoul(m->info[Ilines], 0, 0);
//	m->size = strtoull(m->info[Isize], 0, 0);
//	m->size += m->lines;			/* BOTCH: this hack belongs elsewhere */
	return 1;
}

/*
 * make sure the message has valid mime structure
 * and sub-messages
 */
int
msgstruct(Msg *m, int top)
{
	char buf[12];
	int fd, ns, max;
	Msg *k, head, *last;

	if(m->kids != nil)
		return 1;
	if(m->expunged
	|| !msginfo(m)
	|| !msgheader(m, &m->mime, "mimeheader")){
		msgdead(m);
		return 0;
	}
	/* gack.  we need to get the header from the subpart here. */
	if(msgis822(&m->mime)){
		free(m->ibuf);
		m->info[0] = 0;
		m->efs = seprint(m->efs, m->efs + 5, "/1/");
		if(!msginfo(m)){
			msgdead(m);
			return 0;
		}
	}
	if(!msgunix(m, top)
	|| !msgbodysize(m)
	|| (top || msgis822(&m->mime) || msgismulti(&m->mime)) && !msgheader(m, &m->head, "rawheader")){
		msgdead(m);
		return 0;
	}

	/*
	 * if a message has no kids, it has a kid which is just the body of the real message
	 */
	if(!msgismulti(&m->head) && !msgismulti(&m->mime) && !msgis822(&m->head) && !msgis822(&m->mime)){
		k = MKZ(Msg);
		k->id = 1;
		k->fsdir = m->fsdir;
		k->parent = m->parent;
		ns = m->efs - m->fs;
		k->fs = emalloc(ns + (Filelen + 1));
		memmove(k->fs, m->fs, ns);
		k->efs = k->fs + ns;
		*k->efs = '\0';
		k->size = m->size;
		m->kids = k;
		return 1;
	}

	/*
	 * read in all child messages messages
	 */
	head.next = nil;
	last = &head;
	for(max = 1;; max++){
		snprint(buf, sizeof buf, "%d", max);
		fd = msgfile(m, buf);
		if(fd == -1)
			break;
		close(fd);
		m->efs[0] = 0;		/* BOTCH! */

		k = MKZ(Msg);
		k->id = max;
		k->fsdir = m->fsdir;
		k->parent = m;
		ns = strlen(m->fs) + 2*(Filelen + 1);
		k->fs = emalloc(ns);
		k->efs = seprint(k->fs, k->fs + ns, "%s%d/", m->fs, max);
		k->size = ~0UL;
		k->lines = ~0UL;
		last->next = k;
		last = k;
	}

	m->kids = head.next;

	/*
	 * if kids fail, just whack them
	 */
	top = top && (msgis822(&m->head) || msgismulti(&m->head));
	for(k = m->kids; k != nil; k = k->next)
		if(!msgstruct(k, top)){
			debuglog("kid fail %p %s", k, k->fs);
			for(k = m->kids; k != nil; ){
				last = k;
				k = k->next;
				freemsg(0, last);
			}
			m->kids = nil;
			break;
		}
	return 1;
}

/*
 * read in the message body to count \n without a preceding \r
 */
static int
msgbodysize(Msg *m)
{
	char buf[Bufsize + 2], *s, *se;
	uint length, size, lines, needr;
	int n, fd, c;
	Dir *d;

	if(m->lines != ~0UL)
		return 1;
	fd = msgfile(m, "rawbody");
	if(fd < 0)
		return 0;
	d = dirfstat(fd);
	if(d == nil){
		close(fd);
		return 0;
	}
	length = d->length;
	free(d);

	size = 0;
	lines = 0;
	needr = 0;
	buf[0] = ' ';
	for(;;){
		n = read(fd, &buf[1], Bufsize);
		if(n <= 0)
			break;
		size += n;
		se = &buf[n + 1];
		for(s = &buf[1]; s < se; s++){
			c = *s;
			if(c == '\0')
				*s = ' ';
			if(c != '\n')
				continue;
			if(s[-1] != '\r')
				needr++;
			lines++;
		}
		buf[0] = buf[n];
	}
	if(size != length)
		bye("bad length reading rawbody %d != %d; n %d %s", size, length, n, m->fs);
	size += needr;
	m->size = size;
	m->lines = lines;
	close(fd);
	return 1;
}

/*
 * prepend hdrname: val to the cached header
 */
static void
msgaddhead(Msg *m, char *hdrname, char *val)
{
	char *s;
	long size, n;

	n = strlen(hdrname) + strlen(val) + 4;
	size = m->head.size + n;
	s = emalloc(size + 1);
	snprint(s, size + 1, "%s: %s\r\n%s", hdrname, val, m->head.buf);
	free(m->head.buf);
	m->head.buf = s;
	m->head.size = size;
	m->head.lines++;
}

static void
msgadddate(Msg *m)
{
	char buf[64];
	Tm tm;

	/* don't bother if we don't have a date */
	if(m->info[Idate] == 0)
		return;

	date2tm(&tm, m->info[Idate]);
	snprint(buf, sizeof buf, "%δ", &tm);
	msgaddhead(m, "Date", buf);
}

/*
 * read in the entire header,
 * and parse out any existing mime headers
 */
static int
msgheader(Msg *m, Header *h, char *file)
{
	char *s, *ss, *t, *te;
	int dated, c;
	long ns;
	uint lines, n, nn;

	if(h->buf != nil)
		return 1;

	ns = msgreadfile(m, file, &ss);
	if(ns < 0)
		return 0;
	s = ss;
	n = ns;

	/*
	 * count lines ending with \n and \r\n
	 * add an extra line at the end, since upas/fs headers
	 * don't have a terminating \r\n
	 */
	lines = 1;
	te = s + ns;
	for(t = s; t < te; t++){
		c = *t;
		if(c == '\0')
			*t = ' ';
		if(c != '\n')
			continue;
		if(t == s || t[-1] != '\r')
			n++;
		lines++;
	}
	if(t > s && t[-1] != '\n'){
		if(t[-1] != '\r')
			n++;
		n++;
	}
	if(n > 0)
		n += 2;
	h->buf = emalloc(n + 1);
	h->size = n;
	h->lines = lines;

	/*
	 * make sure all headers end in \r\n
	 */
	nn = 0;
	for(t = s; t < te; t++){
		c = *t;
		if(c == '\n'){
			if(!nn || h->buf[nn - 1] != '\r')
				h->buf[nn++] = '\r';
			lines++;
		}
		h->buf[nn++] = c;
	}
	if(nn && h->buf[nn-1] != '\n'){
		if(h->buf[nn-1] != '\r')
			h->buf[nn++] = '\r';
		h->buf[nn++] = '\n';
	}
	if(nn > 0){
		h->buf[nn++] = '\r';
		h->buf[nn++] = '\n';
	}
	h->buf[nn] = '\0';
	if(nn != n)
		bye("misconverted header %d %d", nn, n);
	free(s);

	/*
	 * and parse some mime headers
	 */
	headstr = (uchar*)h->buf;
	dated = 0;
	while(s = headatom(headfieldstop)){
		if(cistrcmp(s, "content-type") == 0)
			mimetype(h);
		else if(cistrcmp(s, "content-transfer-encoding") == 0)
			mimeencoding(h);
		else if(cistrcmp(s, "content-id") == 0)
			mimeid(h);
		else if(cistrcmp(s, "content-description") == 0)
			mimedescription(h);
		else if(cistrcmp(s, "content-disposition") == 0)
			mimedisposition(h);
//		else if(cistrcmp(s, "content-md5") == 0)
//			mimemd5(h);
		else if(cistrcmp(s, "content-language") == 0)
			mimelanguage(h);
		else if(h == &m->head){
			if(cistrcmp(s, "from") == 0)
				m->from = headmaddr(m->from);
			else if(cistrcmp(s, "to") == 0)
				m->to = headmaddr(m->to);
			else if(cistrcmp(s, "reply-to") == 0)
				m->replyto = headmaddr(m->replyto);
			else if(cistrcmp(s, "sender") == 0)
				m->sender = headmaddr(m->sender);
			else if(cistrcmp(s, "cc") == 0)
				m->cc = headmaddr(m->cc);
			else if(cistrcmp(s, "bcc") == 0)
				m->bcc = headmaddr(m->bcc);
			else if(cistrcmp(s, "date") == 0)
				dated = 1;
		}
		headskip();
		free(s);
	}

	if(h == &m->head){
		if(m->from == nil){
			m->from = m->unixfrom;
			if(m->from != nil){
				s = maddrstr(m->from);
				msgaddhead(m, "From", s);
				free(s);
			}
		}
		if(m->sender == nil)
			m->sender = m->from;
		if(m->replyto == nil)
			m->replyto = m->from;

		if(m->info[Idate] == 0)
			m->info[Idate] = m->unixdate;
		if(!dated && m->from != nil)
			msgadddate(m);
	}
	return 1;
}

/*
 * q is a quoted string.  remove enclosing " and and \ escapes
 */
static void
stripquotes(char *q)
{
	char *s;
	int c;

	if(q == nil)
		return;
	s = q++;
	while(c = *q++){
		if(c == '\\'){
			c = *q++;
			if(!c)
				return;
		}
		*s++ = c;
	}
	s[-1] = '\0';
}

/*
 * parser for rfc822 & mime header fields
 */

/*
 * params	:
 *		| params ';' token '=' token
 * 		| params ';' token '=' quoted-str
 */
static Mimehdr*
mimeparams(void)
{
	char *s, *t;
	Mimehdr head, *last;

	head.next = nil;
	last = &head;
	for(;;){
		if(headchar(1) != ';')
			break;
		s = headatom(mimetokenstop);
		if(s == nil || headchar(1) != '='){
			free(s);
			break;
		}
		if(headchar(0) == '"'){
			t = headquoted('"', '"');
			stripquotes(t);
		}else
			t = headatom(mimetokenstop);
		if(t == nil){
			free(s);
			break;
		}
		last->next = mkmimehdr(s, t, nil);
		last = last->next;
	}
	return head.next;
}

/*
 * type		: 'content-type' ':' token '/' token params
 */
static void
mimetype(Header *h)
{
	char *s, *t;

	if(headchar(1) != ':')
		return;
	s = headatom(mimetokenstop);
	if(s == nil || headchar(1) != '/'){
		free(s);
		return;
	}
	t = headatom(mimetokenstop);
	if(t == nil){
		free(s);
		return;
	}
	h->type = mkmimehdr(s, t, mimeparams());
}

/*
 * encoding	: 'content-transfer-encoding' ':' token
 */
static void
mimeencoding(Header *h)
{
	char *s;

	if(headchar(1) != ':')
		return;
	s = headatom(mimetokenstop);
	if(s == nil)
		return;
	h->encoding = mkmimehdr(s, nil, nil);
}

/*
 * mailaddr	: ':' addresses
 */
static Maddr*
headmaddr(Maddr *old)
{
	Maddr *a;

	if(headchar(1) != ':')
		return old;

	if(headchar(0) == '\n')
		return old;

	a = headaddresses();
	if(a == nil)
		return old;

	freemaddr(old);
	return a;
}

/*
 * addresses	: address | addresses ',' address
 */
static Maddr*
headaddresses(void)
{
	Maddr *addr, *tail, *a;

	addr = headaddress();
	if(addr == nil)
		return nil;
	tail = addr;
	while(headchar(0) == ','){
		headchar(1);
		a = headaddress();
		if(a == nil){
			freemaddr(addr);
			return nil;
		}
		tail->next = a;
		tail = a;
	}
	return addr;
}

/*
 * address	: mailbox | group
 * group	: phrase ':' mboxes ';' | phrase ':' ';'
 * mailbox	: addr-spec
 *		| optphrase '<' addr-spec '>'
 *		| optphrase '<' route ':' addr-spec '>'
 * optphrase	: | phrase
 * route	: '@' domain
 *		| route ',' '@' domain
 * personal names are the phrase before '<',
 * or a comment before or after a simple addr-spec
 */
static Maddr*
headaddress(void)
{
	char *s, *e, *w, *personal;
	uchar *hs;
	int c;
	Maddr *addr;

	s = emalloc(strlen((char*)headstr) + 2);
	e = s;
	personal = headskipwhite(1);
	c = headchar(0);
	if(c == '<')
		w = nil;
	else{
		w = headword();
		c = headchar(0);
	}
	if(c == '.' || c == '@' || c == ',' || c == '\n' || c == '\0'){
		lastwhite = headstr;
		e = headaddrspec(s, w);
		if(personal == nil){
			hs = headstr;
			headstr = lastwhite;
			personal = headskipwhite(1);
			headstr = hs;
		}
	}else{
		if(c != '<' || w != nil){
			free(personal);
			if(!headphrase(e, w)){
				free(s);
				return nil;
			}

			/*
			 * ignore addresses with groups,
			 * so the only thing left if <
			 */
			c = headchar(1);
			if(c != '<'){
				free(s);
				return nil;
			}
			personal = estrdup(s);
		}else
			headchar(1);

		/*
		 * after this point, we need to free personal before returning.
		 * set e to nil to everything afterwards fails.
		 *
		 * ignore routes, they are useless, and heavily discouraged in rfc1123.
		 * imap4 reports them up to, but not including, the terminating :
		 */
		e = s;
		c = headchar(0);
		if(c == '@'){
			for(;;){
				c = headchar(1);
				if(c != '@'){
					e = nil;
					break;
				}
				headdomain(e);
				c = headchar(1);
				if(c != ','){
					e = s;
					break;
				}
			}
			if(c != ':')
				e = nil;
		}

		if(e != nil)
			e = headaddrspec(s, nil);
		if(headchar(1) != '>')
			e = nil;
	}

	/*
	 * e points to @host, or nil if an error occured
	 */
	if(e == nil){
		free(personal);
		addr = nil;
	}else{
		if(*e != '\0')
			*e++ = '\0';
		else
			e = site;
		addr = MKZ(Maddr);

		addr->personal = personal;
		addr->box = estrdup(s);
		addr->host = estrdup(e);
	}
	free(s);
	return addr;
}

/*
 * phrase	: word
 *		| phrase word
 * w is the optional initial word of the phrase
 * returns the end of the phrase, or nil if a failure occured
 */
static char*
headphrase(char *e, char *w)
{
	int c;

	for(;;){
		if(w == nil){
			w = headword();
			if(w == nil)
				return nil;
		}
		if(w[0] == '"')
			stripquotes(w);
		strcpy(e, w);
		free(w);
		w = nil;
		e = strchr(e, '\0');
		c = headchar(0);
		if(c <= ' ' || strchr(headatomstop, c) != nil && c != '"')
			break;
		*e++ = ' ';
		*e = '\0';
	}
	return e;
}

/*
 * find the ! in domain!rest, where domain must have at least
 * one internal '.'
 */
static char*
dombang(char *s)
{
	int dot, c;

	dot = 0;
	for(; c = *s; s++){
		if(c == '!'){
			if(!dot || dot == 1 && s[-1] == '.' || s[1] == '\0')
				return nil;
			return s;
		}
		if(c == '"')
			break;
		if(c == '.')
			dot++;
	}
	return nil;
}

/*
 * addr-spec	: local-part '@' domain
 *		| local-part			extension to allow ! and local names
 * local-part	: word
 *		| local-part '.' word
 *
 * if no '@' is present, rewrite d!e!f!u as @d,@e:u@f,
 * where d, e, f are valid domain components.
 * the @d,@e: is ignored, since routes are ignored.
 * perhaps they should be rewritten as e!f!u@d, but that is inconsistent with upas.
 *
 * returns a pointer to '@', the end if none, or nil if there was an error
 */
static char*
headaddrspec(char *e, char *w)
{
	char *s, *at, *b, *bang, *dom;
	int c;

	s = e;
	for(;;){
		if(w == nil){
			w = headword();
			if(w == nil)
				return nil;
		}
		strcpy(e, w);
		free(w);
		w = nil;
		e = strchr(e, '\0');
		lastwhite = headstr;
		c = headchar(0);
		if(c != '.')
			break;
		headchar(1);
		*e++ = '.';
		*e = '\0';
	}

	if(c != '@'){
		/*
		 * extenstion: allow name without domain
		 * check for domain!xxx
		 */
		bang = dombang(s);
		if(bang == nil)
			return e;

		/*
		 * if dom1!dom2!xxx, ignore dom1!
		 */
		dom = s;
		for(; b = dombang(bang + 1); bang = b)
			dom = bang + 1;

		/*
		 * convert dom!mbox into mbox@dom
		 */
		*bang = '@';
		strrev(dom, bang);
		strrev(bang + 1, e);
		strrev(dom, e);
		bang = &dom[e - bang - 1];
		if(dom > s){
			bang -= dom - s;
			for(e = s; *e = *dom; e++)
				dom++;
		}

		/*
		 * eliminate a trailing '.'
		 */
		if(e[-1] == '.')
			e[-1] = '\0';
		return bang;
	}
	headchar(1);

	at = e;
	*e++ = '@';
	*e = '\0';
	if(!headdomain(e))
		return nil;
	return at;
}

/*
 * domain	: sub-domain
 *		| domain '.' sub-domain
 * returns the end of the domain, or nil if a failure occured
 */
static char*
headdomain(char *e)
{
	char *w;

	for(;;){
		w = headsubdomain();
		if(w == nil)
			return nil;
		strcpy(e, w);
		free(w);
		e = strchr(e, '\0');
		lastwhite = headstr;
		if(headchar(0) != '.')
			break;
		headchar(1);
		*e++ = '.';
		*e = '\0';
	}
	return e;
}

/*
 * id		: 'content-id' ':' msg-id
 * msg-id	: '<' addr-spec '>'
 */
static void
mimeid(Header *h)
{
	char *s, *e, *w;

	if(headchar(1) != ':')
		return;
	if(headchar(1) != '<')
		return;

	s = emalloc(strlen((char*)headstr) + 3);
	e = s;
	*e++ = '<';
	e = headaddrspec(e, nil);
	if(e == nil || headchar(1) != '>'){
		free(s);
		return;
	}
	e = strchr(e, '\0');
	*e++ = '>';
	e[0] = '\0';
	w = strdup(s);
	free(s);
	h->id = mkmimehdr(w, nil, nil);
}

/*
 * description	: 'content-description' ':' *text
 */
static void
mimedescription(Header *h)
{
	if(headchar(1) != ':')
		return;
	headskipwhite(0);
	h->description = mkmimehdr(headtext(), nil, nil);
}

/*
 * disposition	: 'content-disposition' ':' token params
 */
static void
mimedisposition(Header *h)
{
	char *s;

	if(headchar(1) != ':')
		return;
	s = headatom(mimetokenstop);
	if(s == nil)
		return;
	h->disposition = mkmimehdr(s, nil, mimeparams());
}

/*
 * md5		: 'content-md5' ':' token
 */
//static void
//mimemd5(Header *h)
//{
//	char *s;
//
//	if(headchar(1) != ':')
//		return;
//	s = headatom(mimetokenstop);
//	if(s == nil)
//		return;
//	h->md5 = mkmimehdr(s, nil, nil);
//}

/*
 * language	: 'content-language' ':' langs
 * langs	: token
 *		| langs commas token
 * commas	: ','
 *		| commas ','
 */
static void
mimelanguage(Header *h)
{
	char *s;
	Mimehdr head, *last;

	head.next = nil;
	last = &head;
	for(;;){
		s = headatom(mimetokenstop);
		if(s == nil)
			break;
		last->next = mkmimehdr(s, nil, nil);
		last = last->next;
		while(headchar(0) != ',')
			headchar(1);
	}
	h->language = head.next;
}

/*
 * token	: 1*<char 33-255, except "()<>@,;:\\\"/[]?=" aka mimetokenstop>
 * atom		: 1*<chars 33-255, except "()<>@,;:\\\".[]" aka headatomstop>
 * note this allows 8 bit characters, which occur in utf.
 */
static char*
headatom(char *disallowed)
{
	char *s;
	int c, ns, as;

	headskipwhite(0);

	s = emalloc(Stralloc);
	as = Stralloc;
	ns = 0;
	for(;;){
		c = *headstr++;
		if(c <= ' ' || strchr(disallowed, c) != nil){
			headstr--;
			break;
		}
		s[ns++] = c;
		if(ns >= as){
			as += Stralloc;
			s = erealloc(s, as);
		}
	}
	if(ns == 0){
		free(s);
		return 0;
	}
	s[ns] = '\0';
	return s;
}

/*
 * sub-domain	: atom | domain-lit
 */
static char *
headsubdomain(void)
{
	if(headchar(0) == '[')
		return headquoted('[', ']');
	return headatom(headatomstop);
}

/*
 * word	: atom | quoted-str
 */
static char *
headword(void)
{
	if(headchar(0) == '"')
		return headquoted('"', '"');
	return headatom(headatomstop);
}

/*
 * quoted-str	: '"' *(any char but '"\\\r', or '\' any char, or linear-white-space) '"'
 * domain-lit	: '[' *(any char but '[]\\\r', or '\' any char, or linear-white-space) ']'
 */
static char *
headquoted(int start, int stop)
{
	char *s;
	int c, ns, as;

	if(headchar(1) != start)
		return nil;
	s = emalloc(Stralloc);
	as = Stralloc;
	ns = 0;
	s[ns++] = start;
	for(;;){
		c = *headstr;
		if(c == stop){
			headstr++;
			break;
		}
		if(c == '\0'){
			free(s);
			return nil;
		}
		if(c == '\r'){
			headstr++;
			continue;
		}
		if(c == '\n'){
			headstr++;
			while(*headstr == ' ' || *headstr == '\t' || *headstr == '\r' || *headstr == '\n')
				headstr++;
			c = ' ';
		}else if(c == '\\'){
			headstr++;
			s[ns++] = c;
			c = *headstr;
			if(c == '\0'){
				free(s);
				return nil;
			}
			headstr++;
		}else
			headstr++;
		s[ns++] = c;
		if(ns + 1 >= as){	/* leave room for \c or "0 */
			as += Stralloc;
			s = erealloc(s, as);
		}
	}
	s[ns++] = stop;
	s[ns] = '\0';
	return s;
}

/*
 * headtext	: contents of rest of header line
 */
static char *
headtext(void)
{
	uchar *v;
	char *s;

	v = headstr;
	headtoend();
	s = emalloc(headstr - v + 1);
	memmove(s, v, headstr - v);
	s[headstr - v] = '\0';
	return s;
}

/*
 * white space is ' ' '\t' or nested comments.
 * skip white space.
 * if com and a comment is seen,
 * return it's contents and stop processing white space.
 */
static char*
headskipwhite(int com)
{
	char *s;
	int c, incom, as, ns;

	s = nil;
	as = Stralloc;
	ns = 0;
	if(com)
		s = emalloc(Stralloc);
	incom = 0;
	for(; c = *headstr; headstr++){
		switch(c){
		case ' ':
		case '\t':
		case '\r':
			c = ' ';
			break;
		case '\n':
			c = headstr[1];
			if(c != ' ' && c != '\t')
				goto done;
			c = ' ';
			break;
		case '\\':
			if(com && incom)
				s[ns++] = c;
			c = headstr[1];
			if(c == '\0')
				goto done;
			headstr++;
			break;
		case '(':
			incom++;
			if(incom == 1)
				continue;
			break;
		case ')':
			incom--;
			if(com && !incom){
				s[ns] = '\0';
				return s;
			}
			break;
		default:
			if(!incom)
				goto done;
			break;
		}
		if(com && incom && (c != ' ' || ns > 0 && s[ns-1] != ' ')){
			s[ns++] = c;
			if(ns + 1 >= as){	/* leave room for \c or 0 */
				as += Stralloc;
				s = erealloc(s, as);
			}
		}
	}
done:
	free(s);
	return nil;
}

/*
 * return the next non-white character
 */
static int
headchar(int eat)
{
	int c;

	headskipwhite(0);
	c = *headstr;
	if(eat && c != '\0' && c != '\n')
		headstr++;
	return c;
}

static void
headtoend(void)
{
	uchar *s;
	int c;

	for(;;){
		s = headstr;
		c = *s++;
		while(c == '\r')
			c = *s++;
		if(c == '\n'){
			c = *s++;
			if(c != ' ' && c != '\t')
				return;
		}
		if(c == '\0')
			return;
		headstr = s;
	}
}

static void
headskip(void)
{
	int c;

	while(c = *headstr){
		headstr++;
		if(c == '\n'){
			c = *headstr;
			if(c == ' ' || c == '\t')
				continue;
			return;
		}
	}
}