shithub: riscv

ref: 09b250f079e38883e8ad5dc47e4aa93258e0c083
dir: /sys/src/cmd/upas/fs/mbox.c/

View raw version
#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <libsec.h>
#include "dat.h"

typedef struct Header Header;

struct Header {
	char *type;
	void (*f)(Message*, Header*, char*);
	int len;
};

/* headers */
static	void	ctype(Message*, Header*, char*);
static	void	cencoding(Message*, Header*, char*);
static	void	cdisposition(Message*, Header*, char*);
static	void	date822(Message*, Header*, char*);
static	void	from822(Message*, Header*, char*);
static	void	to822(Message*, Header*, char*);
static	void	sender822(Message*, Header*, char*);
static	void	replyto822(Message*, Header*, char*);
static	void	subject822(Message*, Header*, char*);
static	void	inreplyto822(Message*, Header*, char*);
static	void	cc822(Message*, Header*, char*);
static	void	bcc822(Message*, Header*, char*);
static	void	messageid822(Message*, Header*, char*);
static	void	mimeversion(Message*, Header*, char*);
static	void	nullsqueeze(Message*);
enum
{
	Mhead=	11,	/* offset of first mime header */
};

Header head[] =
{
	{ "date:", date822, },
	{ "from:", from822, },
	{ "to:", to822, },
	{ "sender:", sender822, },
	{ "reply-to:", replyto822, },
	{ "subject:", subject822, },
	{ "cc:", cc822, },
	{ "bcc:", bcc822, },
	{ "in-reply-to:", inreplyto822, },
	{ "mime-version:", mimeversion, },
	{ "message-id:", messageid822, },

[Mhead]	{ "content-type:", ctype, },
	{ "content-transfer-encoding:", cencoding, },
	{ "content-disposition:", cdisposition, },
	{ 0, },
};

static	void	fatal(char *fmt, ...);
static	void	initquoted(void);
static	void	startheader(Message*);
static	void	startbody(Message*);
static	char*	skipwhite(char*);
static	char*	skiptosemi(char*);
static	char*	getstring(char*, String*, int);
static	void	setfilename(Message*, char*);
static	char*	lowercase(char*);
static	int	is8bit(Message*);
static	int	headerline(char**, String*);
static	void	initheaders(void);
static void	parseattachments(Message*, Mailbox*);

int		debug;

char *Enotme = "path not served by this file server";

enum
{
	Chunksize = 1024,
};

Mailboxinit *boxinit[] = {
	imap4mbox,
	pop3mbox,
	planbmbox,
	planbvmbox,
	plan9mbox,
};

char*
syncmbox(Mailbox *mb, int doplumb)
{
	return (*mb->sync)(mb, doplumb);
}

/* create a new mailbox */
char*
newmbox(char *path, char *name, int std)
{
	Mailbox *mb, **l;
	char *p, *rv;
	int i;

	initheaders();

	mb = emalloc(sizeof(*mb));
	strncpy(mb->path, path, sizeof(mb->path)-1);
	if(name == nil){
		p = strrchr(path, '/');
		if(p == nil)
			p = path;
		else
			p++;
		if(*p == 0){
			free(mb);
			return "bad mbox name";
		}
		strncpy(mb->name, p, sizeof(mb->name)-1);
	} else {
		strncpy(mb->name, name, sizeof(mb->name)-1);
	}

	rv = nil;
	// check for a mailbox type
	for(i=0; i<nelem(boxinit); i++)
		if((rv = (*boxinit[i])(mb, path)) != Enotme)
			break;
	if(i == nelem(boxinit)){
		free(mb);
		return "bad path";
	}

	// on error, give up
	if(rv){
		free(mb);
		return rv;
	}

	// make sure name isn't taken
	qlock(&mbllock);
	for(l = &mbl; *l != nil; l = &(*l)->next){
		if(strcmp((*l)->name, mb->name) == 0){
			if(strcmp(path, (*l)->path) == 0)
				rv = nil;
			else
				rv = "mbox name in use";
			if(mb->close)
				(*mb->close)(mb);
			free(mb);
			qunlock(&mbllock);
			return rv;
		}
	}

	// always try locking
	mb->dolock = 1;

	mb->refs = 1;
	mb->next = nil;
	mb->id = newid();
	mb->root = newmessage(nil);
	mb->std = std;
	*l = mb;
	qunlock(&mbllock);

	qlock(mb);
	if(mb->ctl){
		henter(PATH(mb->id, Qmbox), "ctl",
			(Qid){PATH(mb->id, Qmboxctl), 0, QTFILE}, nil, mb);
	}
	rv = syncmbox(mb, 0);
	qunlock(mb);

	return rv;
}

// close the named mailbox
void
freembox(char *name)
{
	Mailbox **l, *mb;

	qlock(&mbllock);
	for(l=&mbl; *l != nil; l=&(*l)->next){
		if(strcmp(name, (*l)->name) == 0){
			mb = *l;
			*l = mb->next;
			mboxdecref(mb);
			break;
		}
	}
	hfree(PATH(0, Qtop), name);
	qunlock(&mbllock);
}

static void
initheaders(void)
{
	Header *h;
	static int already;

	if(already)
		return;
	already = 1;

	for(h = head; h->type != nil; h++)
		h->len = strlen(h->type);
}

/*
 *  parse a Unix style header
 */
void
parseunix(Message *m)
{
	char *p;
	String *h;

	h = s_new();
	for(p = m->start + 5; *p && *p != '\r' && *p != '\n'; p++)
		s_putc(h, *p);
	s_terminate(h);
	s_restart(h);

	m->unixfrom = s_parse(h, s_reset(m->unixfrom));
	m->unixdate = s_append(s_reset(m->unixdate), h->ptr);

	s_free(h);
}

/*
 *  parse a message
 */
void
parseheaders(Message *m, int justmime, Mailbox *mb, int addfrom)
{
	String *hl;
	Header *h;
	char *p, *q;
	int i;

	if(m->whole == m->whole->whole){
		henter(PATH(mb->id, Qmbox), m->name,
			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
	} else {
		henter(PATH(m->whole->id, Qdir), m->name,
			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
	}
	for(i = 0; i < Qmax; i++)
		henter(PATH(m->id, Qdir), dirtab[i],
			(Qid){PATH(m->id, i), 0, QTFILE}, m, mb);

	// parse mime headers
	p = m->header;
	hl = s_new();
	while(headerline(&p, hl)){
		if(justmime)
			h = &head[Mhead];
		else
			h = head;
		for(; h->type; h++){
			if(cistrncmp(s_to_c(hl), h->type, h->len) == 0){
				(*h->f)(m, h, s_to_c(hl));
				break;
			}
		}
		s_reset(hl);
	}
	s_free(hl);

	// the blank line isn't really part of the body or header
	if(justmime){
		m->mhend = p;
		m->hend = m->header;
	} else {
		m->hend = p;
	}
	if(*p == '\n')
		p++;
	m->rbody = m->body = p;

	// if type is text, get any nulls out of the body.  This is
	// for the two seans and imap clients that get confused.
	if(strncmp(s_to_c(m->type), "text/", 5) == 0)
		nullsqueeze(m);

	//
	// cobble together Unix-style from line
	// for local mailbox messages, we end up recreating the
	// original header.
	// for pop3 messages, the best we can do is 
	// use the From: information and the RFC822 date.
	//
	if(m->unixdate == nil || strcmp(s_to_c(m->unixdate), "???") == 0
	|| strcmp(s_to_c(m->unixdate), "Thu Jan 1 00:00:00 GMT 1970") == 0){
		if(m->unixdate){
			s_free(m->unixdate);
			m->unixdate = nil;
		}
		// look for the date in the first Received: line.
		// it's likely to be the right time zone (it's
	 	// the local system) and in a convenient format.
		if(cistrncmp(m->header, "received:", 9)==0){
			if((q = strchr(m->header, ';')) != nil){
				p = q;
				while((p = strchr(p, '\n')) != nil){
					if(p[1] != ' ' && p[1] != '\t' && p[1] != '\n')
						break;
					p++;
				}
				if(p){
					*p = '\0';
					m->unixdate = date822tounix(q+1);
					*p = '\n';
				}
			}
		}

		// fall back on the rfc822 date	
		if(m->unixdate==nil && m->date822)
			m->unixdate = date822tounix(s_to_c(m->date822));
	}

	if(m->unixheader != nil)
		s_free(m->unixheader);

	// only fake header for top-level messages for pop3 and imap4
	// clients (those protocols don't include the unix header).
	// adding the unix header all the time screws up mime-attached
	// rfc822 messages.
	if(!addfrom && !m->unixfrom){
		m->unixheader = nil;
		return;
	}

	m->unixheader = s_copy("From ");
	if(m->unixfrom && strcmp(s_to_c(m->unixfrom), "???") != 0)
		s_append(m->unixheader, s_to_c(m->unixfrom));
	else if(m->from822)
		s_append(m->unixheader, s_to_c(m->from822));
	else
		s_append(m->unixheader, "???");

	s_append(m->unixheader, " ");
	if(m->unixdate)
		s_append(m->unixheader, s_to_c(m->unixdate));
	else
		s_append(m->unixheader, "Thu Jan  1 00:00:00 GMT 1970");

	s_append(m->unixheader, "\n");
}

String*
promote(String **sp)
{
	String *s;

	if(*sp != nil)
		s = s_clone(*sp);
	else
		s = nil;
	return s;
}

void
parsebody(Message *m, Mailbox *mb)
{
	Message *nm;

	// recurse
	if(strncmp(s_to_c(m->type), "multipart/", 10) == 0){
		parseattachments(m, mb);
	} else if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
		decode(m);
		parseattachments(m, mb);
		nm = m->part;

		// promote headers
		if(m->replyto822 == nil && m->from822 == nil && m->sender822 == nil){
			m->from822 = promote(&nm->from822);
			m->to822 = promote(&nm->to822);
			m->date822 = promote(&nm->date822);
			m->sender822 = promote(&nm->sender822);
			m->replyto822 = promote(&nm->replyto822);
			m->subject822 = promote(&nm->subject822);
			m->unixdate = promote(&nm->unixdate);
		}
	}
}

void
parse(Message *m, int justmime, Mailbox *mb, int addfrom)
{
	parseheaders(m, justmime, mb, addfrom);
	parsebody(m, mb);
}

static void
parseattachments(Message *m, Mailbox *mb)
{
	Message *nm, **l;
	char *p, *x;

	// if there's a boundary, recurse...
	if(m->boundary != nil){
		p = m->body;
		nm = nil;
		l = &m->part;
		for(;;){
			x = strstr(p, s_to_c(m->boundary));

			/* no boundary, we're done */
			if(x == nil){
				if(nm != nil)
					nm->rbend = nm->bend = nm->end = m->bend;
				break;
			}

			/* boundary must be at the start of a line */
			if(x != m->body && *(x-1) != '\n'){
				p = x+1;
				continue;
			}

			if(nm != nil)
				nm->rbend = nm->bend = nm->end = x;
			x += strlen(s_to_c(m->boundary));

			/* is this the last part? ignore anything after it */
			if(strncmp(x, "--", 2) == 0)
				break;

			p = strchr(x, '\n');
			if(p == nil)
				break;
			nm = newmessage(m);
			nm->start = nm->header = nm->body = nm->rbody = ++p;
			nm->mheader = nm->header;
			*l = nm;
			l = &nm->next;
		}
		for(nm = m->part; nm != nil; nm = nm->next)
			parse(nm, 1, mb, 0);
		return;
	}

	// if we've got an rfc822 message, recurse...
	if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
		nm = newmessage(m);
		m->part = nm;
		nm->start = nm->header = nm->body = nm->rbody = m->body;
		nm->end = nm->bend = nm->rbend = m->bend;
		parse(nm, 0, mb, 0);
	}
}

/*
 *  pick up a header line
 */
static int
headerline(char **pp, String *hl)
{
	char *p, *x;

	s_reset(hl);
	p = *pp;
	x = strpbrk(p, ":\n");
	if(x == nil || *x == '\n')
		return 0;
	for(;;){
		x = strchr(p, '\n');
		if(x == nil)
			x = p + strlen(p);
		s_nappend(hl, p, x-p);
		p = x;
		if(*p != '\n' || *++p != ' ' && *p != '\t')
			break;
		while(*p == ' ' || *p == '\t')
			p++;
		s_putc(hl, ' ');
	}
	*pp = p;
	return 1;
}

/* returns nil iff there are no addressees */
static String*
addr822(char *p)
{
	String *s, *list;
	int incomment, addrdone, inanticomment, quoted;
	int n;
	int c;

	list = s_new();
	s = s_new();
	quoted = incomment = addrdone = inanticomment = 0;
	n = 0;
	for(; *p; p++){
		c = *p;

		// whitespace is ignored
		if(!quoted && isspace(c) || c == '\r')
			continue;

		// strings are always treated as atoms
		if(!quoted && c == '"'){
			if(!addrdone && !incomment)
				s_putc(s, c);
			for(p++; *p; p++){
				if(!addrdone && !incomment)
					s_putc(s, *p);
				if(!quoted && *p == '"')
					break;
				if(*p == '\\')
					quoted = 1;
				else
					quoted = 0;
			}
			if(*p == 0)
				break;
			quoted = 0;
			continue;
		}

		// ignore everything in an expicit comment
		if(!quoted && c == '('){
			incomment = 1;
			continue;
		}
		if(incomment){
			if(!quoted && c == ')')
				incomment = 0;
			quoted = 0;
			continue;
		}

		// anticomments makes everything outside of them comments
		if(!quoted && c == '<' && !inanticomment){
			inanticomment = 1;
			s = s_reset(s);
			continue;
		}
		if(!quoted && c == '>' && inanticomment){
			addrdone = 1;
			inanticomment = 0;
			continue;
		}

		// commas separate addresses
		if(!quoted && c == ',' && !inanticomment){
			s_terminate(s);
			addrdone = 0;
			if(n++ != 0)
				s_append(list, " ");
			s_append(list, s_to_c(s));
			s = s_reset(s);
			continue;
		}

		// what's left is part of the address
		s_putc(s, c);

		// quoted characters are recognized only as characters
		if(c == '\\')
			quoted = 1;
		else
			quoted = 0;

	}

	if(*s_to_c(s) != 0){
		s_terminate(s);
		if(n++ != 0)
			s_append(list, " ");
		s_append(list, s_to_c(s));
	}
	s_free(s);

	if(n == 0){		/* no addressees given, just the keyword */
		s_free(list);
		return nil;
	}
	return list;
}

/*
 * per rfc2822 §4.5.3, permit multiple to, cc and bcc headers by
 * concatenating their values.
 */

static void
to822(Message *m, Header *h, char *p)
{
	String *s;

	p += strlen(h->type);
	s = addr822(p);
	if (m->to822 == nil)
		m->to822 = s;
	else if (s != nil) {
		s_append(m->to822, " ");
		s_append(m->to822, s_to_c(s));
		s_free(s);
	}
}

static void
cc822(Message *m, Header *h, char *p)
{
	String *s;

	p += strlen(h->type);
	s = addr822(p);
	if (m->cc822 == nil)
		m->cc822 = s;
	else if (s != nil) {
		s_append(m->cc822, " ");
		s_append(m->cc822, s_to_c(s));
		s_free(s);
	}
}

static void
bcc822(Message *m, Header *h, char *p)
{
	String *s;

	p += strlen(h->type);
	s = addr822(p);
	if (m->bcc822 == nil)
		m->bcc822 = s;
	else if (s != nil) {
		s_append(m->bcc822, " ");
		s_append(m->bcc822, s_to_c(s));
		s_free(s);
	}
}

static void
from822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	s_free(m->from822);
	m->from822 = addr822(p);
}

static void
sender822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	s_free(m->sender822);
	m->sender822 = addr822(p);
}

static void
replyto822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	s_free(m->replyto822);
	m->replyto822 = addr822(p);
}

static void
mimeversion(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	s_free(m->mimeversion);
	m->mimeversion = addr822(p);
}

static void
killtrailingwhite(char *p)
{
	char *e;

	e = p + strlen(p) - 1;
	while(e > p && isspace(*e))
		*e-- = 0;
}

static void
date822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	p = skipwhite(p);
	s_free(m->date822);
	m->date822 = s_copy(p);
	p = s_to_c(m->date822);
	killtrailingwhite(p);
}

static void
subject822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	p = skipwhite(p);
	s_free(m->subject822);
	m->subject822 = s_copy(p);
	p = s_to_c(m->subject822);
	killtrailingwhite(p);
}

static void
inreplyto822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	p = skipwhite(p);
	s_free(m->inreplyto822);
	m->inreplyto822 = s_copy(p);
	p = s_to_c(m->inreplyto822);
	killtrailingwhite(p);
}

static void
messageid822(Message *m, Header *h, char *p)
{
	p += strlen(h->type);
	p = skipwhite(p);
	s_free(m->messageid822);
	m->messageid822 = s_copy(p);
	p = s_to_c(m->messageid822);
	killtrailingwhite(p);
}

static int
isattribute(char **pp, char *attr)
{
	char *p;
	int n;

	n = strlen(attr);
	p = *pp;
	if(cistrncmp(p, attr, n) != 0)
		return 0;
	p += n;
	while(*p == ' ')
		p++;
	if(*p++ != '=')
		return 0;
	while(*p == ' ')
		p++;
	*pp = p;
	return 1;
}

static void
ctype(Message *m, Header *h, char *p)
{
	String *s;

	p += h->len;
	p = skipwhite(p);

	p = getstring(p, m->type, 1);
	
	while(*p){
		if(isattribute(&p, "boundary")){
			s = s_new();
			p = getstring(p, s, 0);
			m->boundary = s_reset(m->boundary);
			s_append(m->boundary, "--");
			s_append(m->boundary, s_to_c(s));
			s_free(s);
		} else if(cistrncmp(p, "multipart", 9) == 0){
			/*
			 *  the first unbounded part of a multipart message,
			 *  the preamble, is not displayed or saved
			 */
		} else if(isattribute(&p, "name")){
			if(m->filename == nil)
				setfilename(m, p);
		} else if(isattribute(&p, "charset")){
			p = getstring(p, s_reset(m->charset), 0);
		}
		
		p = skiptosemi(p);
	}
}

static void
cencoding(Message *m, Header *h, char *p)
{
	p += h->len;
	p = skipwhite(p);
	if(cistrncmp(p, "base64", 6) == 0)
		m->encoding = Ebase64;
	else if(cistrncmp(p, "quoted-printable", 16) == 0)
		m->encoding = Equoted;
}

static void
cdisposition(Message *m, Header *h, char *p)
{
	p += h->len;
	p = skipwhite(p);
	while(*p){
		if(cistrncmp(p, "inline", 6) == 0){
			m->disposition = Dinline;
		} else if(cistrncmp(p, "attachment", 10) == 0){
			m->disposition = Dfile;
		} else if(cistrncmp(p, "filename=", 9) == 0){
			p += 9;
			setfilename(m, p);
		}
		p = skiptosemi(p);
	}

}

ulong msgallocd, msgfreed;

Message*
newmessage(Message *parent)
{
	static int id;
	Message *m;

	msgallocd++;

	m = emalloc(sizeof(*m));
	memset(m, 0, sizeof(*m));
	m->disposition = Dnone;
	m->type = s_copy("text/plain");
	m->charset = s_copy("iso-8859-1");
	m->id = newid();
	if(parent)
		sprint(m->name, "%d", ++(parent->subname));
	if(parent == nil)
		parent = m;
	m->whole = parent;
	m->hlen = -1;
	return m;
}

// delete a message from a mailbox
void
delmessage(Mailbox *mb, Message *m)
{
	Message **l;
	int i;

	mb->vers++;
	msgfreed++;

	if(m->whole != m){
		// unchain from parent
		for(l = &m->whole->part; *l && *l != m; l = &(*l)->next)
			;
		if(*l != nil)
			*l = m->next;

		// clear out of name lookup hash table
		if(m->whole->whole == m->whole)
			hfree(PATH(mb->id, Qmbox), m->name);
		else
			hfree(PATH(m->whole->id, Qdir), m->name);
		for(i = 0; i < Qmax; i++)
			hfree(PATH(m->id, Qdir), dirtab[i]);
	}

	/* recurse through sub-parts */
	while(m->part)
		delmessage(mb, m->part);

	/* free memory */
	if(m->mallocd)
		free(m->start);
	if(m->hallocd)
		free(m->header);
	if(m->ballocd)
		free(m->body);
	s_free(m->unixfrom);
	s_free(m->unixdate);
	s_free(m->unixheader);
	s_free(m->from822);
	s_free(m->sender822);
	s_free(m->to822);
	s_free(m->bcc822);
	s_free(m->cc822);
	s_free(m->replyto822);
	s_free(m->date822);
	s_free(m->inreplyto822);
	s_free(m->subject822);
	s_free(m->messageid822);
	s_free(m->addrs);
	s_free(m->mimeversion);
	s_free(m->sdigest);
	s_free(m->boundary);
	s_free(m->type);
	s_free(m->charset);
	s_free(m->filename);

	free(m);
}

// mark messages (identified by path) for deletion
void
delmessages(int ac, char **av)
{
	Mailbox *mb;
	Message *m;
	int i, needwrite;

	qlock(&mbllock);
	for(mb = mbl; mb != nil; mb = mb->next)
		if(strcmp(av[0], mb->name) == 0){
			qlock(mb);
			break;
		}
	qunlock(&mbllock);
	if(mb == nil)
		return;

	needwrite = 0;
	for(i = 1; i < ac; i++){
		for(m = mb->root->part; m != nil; m = m->next)
			if(strcmp(m->name, av[i]) == 0){
				if(!m->deleted){
					mailplumb(mb, m, 1);
					needwrite = 1;
					m->deleted = 1;
					logmsg("deleting", m);
				}
				break;
			}
	}
	if(needwrite)
		syncmbox(mb, 1);
	qunlock(mb);
}

/*
 *  the following are called with the mailbox qlocked
 */
void
msgincref(Message *m)
{
	m->refs++;
}
void
msgdecref(Mailbox *mb, Message *m)
{
	m->refs--;
	if(m->refs == 0 && m->deleted)
		syncmbox(mb, 1);
}

/*
 *  the following are called with mbllock'd
 */
void
mboxincref(Mailbox *mb)
{
	assert(mb->refs > 0);
	mb->refs++;
}
void
mboxdecref(Mailbox *mb)
{
	assert(mb->refs > 0);
	qlock(mb);
	mb->refs--;
	if(mb->refs == 0){
		delmessage(mb, mb->root);
		if(mb->ctl)
			hfree(PATH(mb->id, Qmbox), "ctl");
		if(mb->close)
			(*mb->close)(mb);
		free(mb);
	} else
		qunlock(mb);
}

int
cistrncmp(char *a, char *b, int n)
{
	while(n-- > 0){
		if(tolower(*a++) != tolower(*b++))
			return -1;
	}
	return 0;
}

int
cistrcmp(char *a, char *b)
{
	for(;;){
		if(tolower(*a) != tolower(*b++))
			return -1;
		if(*a++ == 0)
			break;
	}
	return 0;
}

static char*
skipwhite(char *p)
{
	while(isspace(*p))
		p++;
	return p;
}

static char*
skiptosemi(char *p)
{
	while(*p && *p != ';')
		p++;
	while(*p == ';' || isspace(*p))
		p++;
	return p;
}

static char*
getstring(char *p, String *s, int dolower)
{
	s = s_reset(s);
	p = skipwhite(p);
	if(*p == '"'){
		p++;
		for(;*p && *p != '"'; p++)
			if(dolower)
				s_putc(s, tolower(*p));
			else
				s_putc(s, *p);
		if(*p == '"')
			p++;
		s_terminate(s);

		return p;
	}

	for(; *p && !isspace(*p) && *p != ';'; p++)
		if(dolower)
			s_putc(s, tolower(*p));
		else
			s_putc(s, *p);
	s_terminate(s);

	return p;
}

static void
setfilename(Message *m, char *p)
{
	m->filename = s_reset(m->filename);
	getstring(p, m->filename, 0);
	for(p = s_to_c(m->filename); *p; p++)
		if(*p == ' ' || *p == '\t' || *p == ';')
			*p = '_';
}

//
// undecode message body
//
void
decode(Message *m)
{
	int i, len;
	char *x;

	if(m->decoded)
		return;
	switch(m->encoding){
	case Ebase64:
		len = m->bend - m->body;
		i = (len*3)/4+1;	// room for max chars + null
		x = emalloc(i);
		len = dec64((uchar*)x, i, m->body, len);
		if(m->ballocd)
			free(m->body);
		m->body = x;
		m->bend = x + len;
		m->ballocd = 1;
		break;
	case Equoted:
		len = m->bend - m->body;
		x = emalloc(len+2);	// room for null and possible extra nl
		len = decquoted(x, m->body, m->bend, 0);
		if(m->ballocd)
			free(m->body);
		m->body = x;
		m->bend = x + len;
		m->ballocd = 1;
		break;
	default:
		break;
	}
	m->decoded = 1;
}

// convert latin1 to utf
void
convert(Message *m)
{
	int len;
	char *x;

	// don't convert if we're not a leaf, not text, or already converted
	if(m->converted)
		return;
	if(m->part != nil)
		return;
	if(cistrncmp(s_to_c(m->type), "text", 4) != 0)
		return;

	len = xtoutf(s_to_c(m->charset), &x, m->body, m->bend);
	if(len > 0){
		if(m->ballocd)
			free(m->body);
		m->body = x;
		m->bend = x + len;
		m->ballocd = 1;
	}
	m->converted = 1;
}

static int
hex2int(int x)
{
	if(x >= '0' && x <= '9')
		return x - '0';
	if(x >= 'A' && x <= 'F')
		return (x - 'A') + 10;
	if(x >= 'a' && x <= 'f')
		return (x - 'a') + 10;
	return -1;
}

// underscores are translated in 2047 headers (uscores=1) 
// but not in the body (uscores=0)
static char*
decquotedline(char *out, char *in, char *e, int uscores)
{
	int c, c2, soft;

	/* dump trailing white space */
	while(e >= in && (*e == ' ' || *e == '\t' || *e == '\r' || *e == '\n'))
		e--;

	/* trailing '=' means no newline */
	if(*e == '='){
		soft = 1;
		e--;
	} else
		soft = 0;

	while(in <= e){
		c = (*in++) & 0xff;
		switch(c){
		case '_':
			if(uscores){
				*out++ = ' ';
				break;
			}
		default:
			*out++ = c;
			break;
		case '=':
			c  = hex2int(*in++);
			c2 = hex2int(*in++);
			if (c != -1 && c2 != -1)
				*out++ = c<<4 | c2;
			else {
				*out++ = '=';
				in -= 2;
			}
			break;
		}
	}
	if(!soft)
		*out++ = '\n';
	*out = 0;

	return out;
}

int
decquoted(char *out, char *in, char *e, int uscores)
{
	char *p, *nl;

	p = out;
	while((nl = strchr(in, '\n')) != nil && nl < e){
		p = decquotedline(p, in, nl, uscores);
		in = nl + 1;
	}
	if(in < e)
		p = decquotedline(p, in, e-1, uscores);

	// make sure we end with a new line
	if(*(p-1) != '\n'){
		*p++ = '\n';
		*p = 0;
	}

	return p - out;
}

static char*
lowercase(char *p)
{
	char *op;
	int c;

	for(op = p; c = *p; p++)
		if(isupper(c))
			*p = tolower(c);
	return op;
}

// translate latin1 directly since it fits neatly in utf
static int
latin1toutf(char **out, char *in, char *e)
{
	int n;
	char *p;
	Rune r;

	n = 0;
	for(p = in; p < e; p++)
		if(*p & 0x80)
			n++;
	if(n == 0)
		return 0;

	n += e-in;
	*out = p = malloc(UTFmax*n+1);
	if(p == nil)
		return 0;

	for(; in < e; in++){
		r = (*in) & 0xff;
		p += runetochar(p, &r);
	}
	*p = 0;
	return p - *out;
}

// translate any thing using the tcs program
int
xtoutf(char *charset, char **out, char *in, char *e)
{
	char *av[4];
	int totcs[2];
	int fromtcs[2];
	int n, len, sofar;
	char *p;

	// might not need to convert
	if(cistrcmp(charset, "us-ascii") == 0 || cistrcmp(charset, "utf-8") == 0)
		return 0;
	if(cistrcmp(charset, "iso-8859-1") == 0)
		return latin1toutf(out, in, e);

	len = e-in+1;
	sofar = 0;
	*out = p = malloc(len+1);
	if(p == nil)
		return 0;

	av[0] = charset;
	av[1] = "-f";
	av[2] = charset;
	av[3] = 0;
	if(pipe(totcs) < 0)
		goto error;
	if(pipe(fromtcs) < 0){
		close(totcs[0]); close(totcs[1]);
		goto error;
	}
	switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
	case -1:
		close(fromtcs[0]); close(fromtcs[1]);
		close(totcs[0]); close(totcs[1]);
		goto error;
	case 0:
		close(fromtcs[0]); close(totcs[1]);
		dup(fromtcs[1], 1);
		dup(totcs[0], 0);
		close(fromtcs[1]); close(totcs[0]);
		dup(open("/dev/null", OWRITE), 2);
		exec("/bin/tcs", av);
		_exits(0);
	default:
		close(fromtcs[1]); close(totcs[0]);
		switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
		case -1:
			close(fromtcs[0]); close(totcs[1]);
			goto error;
		case 0:
			close(fromtcs[0]);
			while(in < e){
				n = write(totcs[1], in, e-in);
				if(n <= 0)
					break;
				in += n;
			}
			close(totcs[1]);
			_exits(0);
		default:
			close(totcs[1]);
			for(;;){
				n = read(fromtcs[0], &p[sofar], len-sofar);
				if(n <= 0)
					break;
				sofar += n;
				p[sofar] = 0;
				if(sofar == len){
					len += 1024;
					p = realloc(p, len+1);
					if(p == nil)
						goto error;
					*out = p;
				}
			}
			close(fromtcs[0]);
			break;
		}
		break;
	}
	if(sofar == 0)
		goto error;
	return sofar;

error:
	free(*out);
	*out = nil;
	return 0;
}

void *
emalloc(ulong n)
{
	void *p;

	p = mallocz(n, 1);
	if(!p){
		fprint(2, "%s: out of memory alloc %lud\n", argv0, n);
		exits("out of memory");
	}
	setmalloctag(p, getcallerpc(&n));
	return p;
}

void *
erealloc(void *p, ulong n)
{
	if(n == 0)
		n = 1;
	p = realloc(p, n);
	if(!p){
		fprint(2, "%s: out of memory realloc %lud\n", argv0, n);
		exits("out of memory");
	}
	setrealloctag(p, getcallerpc(&p));
	return p;
}

void
mailplumb(Mailbox *mb, Message *m, int delete)
{
	Plumbmsg p;
	Plumbattr a[7];
	char buf[256];
	int ai;
	char lenstr[10], *from, *subject, *date;
	static int fd = -1;

	if(m->subject822 == nil)
		subject = "";
	else
		subject = s_to_c(m->subject822);

	if(m->from822 != nil)
		from = s_to_c(m->from822);
	else if(m->unixfrom != nil)
		from = s_to_c(m->unixfrom);
	else
		from = "";

	if(m->unixdate != nil)
		date = s_to_c(m->unixdate);
	else
		date = "";

	sprint(lenstr, "%zd", m->end-m->start);

	if(biffing && !delete)
		print("[ %s / %s / %s ]\n", from, subject, lenstr);

	if(!plumbing)
		return;

	if(fd < 0)
		fd = plumbopen("send", OWRITE);
	if(fd < 0)
		return;

	p.src = "mailfs";
	p.dst = "seemail";
	p.wdir = "/mail/fs";
	p.type = "text";

	ai = 0;
	a[ai].name = "filetype";
	a[ai].value = "mail";

	a[++ai].name = "sender";
	a[ai].value = from;
	a[ai-1].next = &a[ai];

	a[++ai].name = "length";
	a[ai].value = lenstr;
	a[ai-1].next = &a[ai];

	a[++ai].name = "mailtype";
	a[ai].value = delete?"delete":"new";
	a[ai-1].next = &a[ai];

	a[++ai].name = "date";
	a[ai].value = date;
	a[ai-1].next = &a[ai];

	if(m->sdigest){
		a[++ai].name = "digest";
		a[ai].value = s_to_c(m->sdigest);
		a[ai-1].next = &a[ai];
	}

	a[ai].next = nil;

	p.attr = a;
	snprint(buf, sizeof(buf), "%s/%s/%s",
		mntpt, mb->name, m->name);
	p.ndata = strlen(buf);
	p.data = buf;

	plumbsend(fd, &p);
}

//
// count the number of lines in the body (for imap4)
//
void
countlines(Message *m)
{
	int i;
	char *p;

	i = 0;
	for(p = strchr(m->rbody, '\n'); p != nil && p < m->rbend; p = strchr(p+1, '\n'))
		i++;
	sprint(m->lines, "%d", i);
}

char *LOG = "fs";

void
logmsg(char *s, Message *m)
{
	int pid;

	if(!logging)
		return;
	pid = getpid();
	if(m == nil)
		syslog(0, LOG, "%s.%d: %s", user, pid, s);
	else
		syslog(0, LOG, "%s.%d: %s msg from %s digest %s",
			user, pid, s,
			m->from822 ? s_to_c(m->from822) : "?",
			s_to_c(m->sdigest));
}

/*
 *  squeeze nulls out of the body
 */
static void
nullsqueeze(Message *m)
{
	char *p, *q;

	q = memchr(m->body, 0, m->end-m->body);
	if(q == nil)
		return;

	for(p = m->body; q < m->end; q++){
		if(*q == 0)
			continue;
		*p++ = *q;
	}
	m->bend = m->rbend = m->end = p;
}


//
// convert an RFC822 date into a Unix style date
// for when the Unix From line isn't there (e.g. POP3).
// enough client programs depend on having a Unix date
// that it's easiest to write this conversion code once, right here.
//
// people don't follow RFC822 particularly closely,
// so we use strtotm, which is a bunch of heuristics.
//

extern int strtotm(char*, Tm*);
String*
date822tounix(char *s)
{
	char *p, *q;
	Tm tm;

	if(strtotm(s, &tm) < 0)
		return nil;

	p = asctime(&tm);
	if(q = strchr(p, '\n'))
		*q = '\0';
	return s_copy(p);
}