shithub: riscv

ref: f7a16b5e2cc38aed7985b0144bb2655d14ea6f5a
dir: /sys/src/cmd/upas/vf/vf.c/

View raw version
/*
 *  this is a filter that changes mime types and names of
 *  suspect executable attachments.
 */
#include "common.h"
#include <ctype.h>

Biobuf in;
Biobuf out;

typedef struct Mtype Mtype;
typedef struct Hdef Hdef;
typedef struct Hline Hline;
typedef struct Part Part;

static int	badfile(char *name);
static int	badtype(char *type);
static void	ctype(Part*, Hdef*, char*);
static void	cencoding(Part*, Hdef*, char*);
static void	cdisposition(Part*, Hdef*, char*);
static int	decquoted(char *out, char *in, char *e);
static char*	getstring(char *p, String *s, int dolower);
static void	init_hdefs(void);
static int	isattribute(char **pp, char *attr);
static int	latin1toutf(char *out, char *in, char *e);
static String*	mkboundary(void);
static Part*	part(Part *pp);
static Part*	passbody(Part *p, int dobound);
static void	passnotheader(void);
static void	passunixheader(void);
static Part*	problemchild(Part *p);
static void	readheader(Part *p);
static Hline*	readhl(void);
static void	readmtypes(void);
static int	save(Part *p, char *file);
static void	setfilename(Part *p, char *name);
static char*	skiptosemi(char *p);
static char*	skipwhite(char *p);
static String*	tokenconvert(String *t);
static void	writeheader(Part *p, int);

enum
{
	/* encodings */
	Enone=	0,
	Ebase64,
	Equoted,

	/* disposition possibilities */
	Dnone=	0,
	Dinline,
	Dfile,
	Dignore,

	PAD64=	'=',
};

/*
 *  a message part; either the whole message or a subpart
 */
struct Part
{
	Part	*pp;		/* parent part */
	Hline	*hl;		/* linked list of header lines */
	int	disposition;
	int	encoding;
	int	badfile;
	int	badtype;
	String	*boundary;	/* boundary for multiparts */
	int	blen;
	String	*charset;	/* character set */
	String	*type;		/* content type */
	String	*filename;	/* file name */
	Biobuf	*tmpbuf;		/* diversion input buffer */
};

/*
 *  a (multi)line header
 */
struct Hline
{
	Hline	*next;
	String		*s;
};

/*
 *  header definitions for parsing
 */
struct Hdef
{
	char *type;
	void (*f)(Part*, Hdef*, char*);
	int len;
};

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

/*
 *  acceptable content types and their extensions
 */
struct Mtype {
	Mtype	*next;
	char 	*ext;		/* extension */
	char	*gtype;		/* generic content type */
	char	*stype;		/* specific content type */
	char	class;
};
Mtype *mtypes;

int justreject;
char *savefile;

void
usage(void)
{
	fprint(2, "usage: upas/vf [-r] [-s savefile]\n");
	exits("usage");
}

void
main(int argc, char **argv)
{
	ARGBEGIN{
	case 'r':
		justreject = 1;
		break;
	case 's':
		savefile = EARGF(usage());
		break;
	default:
		usage();
	}ARGEND

	if(argc)
		usage();

	tmfmtinstall();

	Binit(&in, 0, OREAD);
	Binit(&out, 1, OWRITE);

	init_hdefs();
	readmtypes();

	/* pass through our standard 'From ' line */
	passunixheader();

	/* parse with the top level part */
	part(nil);

	exits(0);
}

void
refuse(char *reason)
{
	char *full;
	static char msg[] =
		"mail refused: we don't accept executable attachments";

	full = smprint("%s: %s", msg, reason);
	postnote(PNGROUP, getpid(), full);
	exits(full);
}


/*
 *  parse a part; returns the ancestor whose boundary terminated
 *  this part or nil on EOF.
 */
static Part*
part(Part *pp)
{
	Part *p, *np;

	p = mallocz(sizeof *p, 1);
	p->pp = pp;
	readheader(p);

	if(p->boundary != nil){
		/* the format of a multipart part is always:
		 *   header
		 *   null or ignored body
		 *   boundary
		 *   header
		 *   body
		 *   boundary
		 *   ...
		 */
		writeheader(p, 1);
		np = passbody(p, 1);
		if(np != p)
			return np;
		for(;;){
			np = part(p);
			if(np != p)
				return np;
		}
	} else {
		/* no boundary */
		/* may still be multipart if this is a forwarded message */
		if(p->type && cistrcmp(s_to_c(p->type), "message/rfc822") == 0){
			/* the format of forwarded message is:
			 *   header
			 *   header
			 *   body
			 */
			writeheader(p, 1);
			passnotheader();
			return part(p);
		} else {
			/*
			 * This is the meat.  This may be an executable.
			 * if so, wrap it and change its type
			 */
			if(p->badtype || p->badfile){
				if(p->badfile == 2){
					if(savefile != nil)
						save(p, savefile);
					syslog(0, "vf", "vf rejected %s %s",
						p->type? s_to_c(p->type): "?",
						p->filename?s_to_c(p->filename):"?");
					fprint(2, "The mail contained an executable attachment.\n");
					fprint(2, "We refuse all mail containing such.\n");
					refuse(nil);
				}
				np = problemchild(p);
				if(np != p)
					return np;
				/* if problemchild returns p, it turns out p is okay: fall thru */
			}
			writeheader(p, 1);
			return passbody(p, 1);
		}
	}
}

/*
 *  read and parse a complete header
 */
static void
readheader(Part *p)
{
	Hline *hl, **l;
	Hdef *hd;

	l = &p->hl;
	for(;;){
		hl = readhl();
		if(hl == nil)
			break;
		*l = hl;
		l = &hl->next;

		for(hd = hdefs; hd->type != nil; hd++){
			if(cistrncmp(s_to_c(hl->s), hd->type, hd->len) == 0){
				(*hd->f)(p, hd, s_to_c(hl->s));
				break;
			}
		}
	}
}

/*
 *  read a possibly multiline header line
 */
static Hline*
readhl(void)
{
	Hline *hl;
	String *s;
	char *p;
	int n;

	p = Brdline(&in, '\n');
	if(p == nil)
		return nil;
	n = Blinelen(&in);
	if(memchr(p, ':', n) == nil){
		Bseek(&in, -n, 1);
		return nil;
	}
	s = s_nappend(s_new(), p, n);
	for(;;){
		p = Brdline(&in, '\n');
		if(p == nil)
			break;
		n = Blinelen(&in);
		if(*p != ' ' && *p != '\t'){
			Bseek(&in, -n, 1);
			break;
		}
		s = s_nappend(s, p, n);
	}
	hl = malloc(sizeof *hl);
	hl->s = s;
	hl->next = nil;
	return hl;
}

/*
 *  write out a complete header
 */
static void
writeheader(Part *p, int xfree)
{
	Hline *hl, *next;

	for(hl = p->hl; hl != nil; hl = next){
		Bprint(&out, "%s", s_to_c(hl->s));
		if(xfree)
			s_free(hl->s);
		next = hl->next;
		if(xfree)
			free(hl);
	}
	if(xfree)
		p->hl = nil;
}

/*
 *  pass a body through.  return if we hit one of our ancestors'
 *  boundaries or EOF.  if we hit a boundary, return a pointer to
 *  that ancestor.  if we hit EOF, return nil.
 */
static Part*
passbody(Part *p, int dobound)
{
	Part *pp;
	Biobuf *b;
	char *cp;

	for(;;){
		if(p->tmpbuf){
			b = p->tmpbuf;
			cp = Brdline(b, '\n');
			if(cp == nil){
				Bterm(b);
				p->tmpbuf = nil;
				goto Stdin;
			}
		}else{
		Stdin:
			b = &in;
			cp = Brdline(b, '\n');
		}
		if(cp == nil)
			return nil;
		for(pp = p; pp != nil; pp = pp->pp)
			if(pp->boundary != nil
			&& strncmp(cp, s_to_c(pp->boundary), pp->blen) == 0){
				if(dobound)
					Bwrite(&out, cp, Blinelen(b));
				else
					Bseek(b, -Blinelen(b), 1);
				return pp;
			}
		Bwrite(&out, cp, Blinelen(b));
	}
}

/*
 *  save the message somewhere
 */
static vlong bodyoff;	/* clumsy hack */

static int
save(Part *p, char *file)
{
	int fd;
	Tm tm;

	Bterm(&out);
	memset(&out, 0, sizeof(out));

	fd = open(file, OWRITE);
	if(fd < 0)
		return -1;
	seek(fd, 0, 2);
	Binit(&out, fd, OWRITE);
	Bprint(&out, "From virusfilter %τ\n", thedate(&tm));
	writeheader(p, 0);
	bodyoff = Boffset(&out);
	passbody(p, 0);
	Bprint(&out, "\n");
	Bterm(&out);
	close(fd);

	memset(&out, 0, sizeof out);
	Binit(&out, 1, OWRITE);
	return 0;
}

/*
 * write to a file but save the fd for passbody.
 */
static char*
savetmp(Part *p)
{
	char *name;
	int fd;

	name = mktemp(smprint("%s/vf.XXXXXXXXXXX", UPASTMP));
	if((fd = create(name, OWRITE|OEXCL, 0666)) < 0){
		fprint(2, "%s: error creating temporary file: %r\n", argv0);
		refuse("can't create temporary file");
	}
	close(fd);
	if(save(p, name) < 0){
		fprint(2, "%s: error saving temporary file: %r\n", argv0);
		refuse("can't write temporary file");
	}
	if(p->tmpbuf){
		fprint(2, "%s: error in savetmp: already have tmp file!\n",
			argv0);
		refuse("already have temporary file");
	}
	p->tmpbuf = Bopen(name, OREAD|ORCLOSE);
	if(p->tmpbuf == nil){
		fprint(2, "%s: error reading temporary file: %r\n", argv0);
		refuse("error reading temporary file");
	}
	Bseek(p->tmpbuf, bodyoff, 0);
	return name;
}

/*
 * Run the external checker to do content-based checks.
 */
static int
runchecker(Part *p)
{
	int pid;
	char *name;
	Waitmsg *w;

	if(access("/mail/lib/validateattachment", AEXEC) < 0)
		return 0;

	name = savetmp(p);
	fprint(2, "run checker %s\n", name);
	switch(pid = fork()){
	case -1:
		sysfatal("fork: %r");
	case 0:
		dup(2, 1);
		execl("/mail/lib/validateattachment", "validateattachment",
			name, nil);
		_exits("exec failed");
	}

	/*
	 * Okay to return on error - will let mail through but wrapped.
	 */
	w = wait();
	if(w == nil){
		syslog(0, "mail", "vf wait failed: %r");
		return 0;
	}
	if(w->pid != pid){
		syslog(0, "mail", "vf wrong pid %d != %d", w->pid, pid);
		return 0;
	}
	if(p->filename) {
		free(name);
		name = strdup(s_to_c(p->filename));
	}
	if(strstr(w->msg, "discard")){
		syslog(0, "mail", "vf validateattachment rejected %s", name);
		refuse("rejected by validateattachment");
	}
	if(strstr(w->msg, "accept")){
		syslog(0, "mail", "vf validateattachment accepted %s", name);
		return 1;
	}
	free(w);
	free(name);
	return 0;
}

/*
 *  emit a multipart Part that explains the problem
 */
static Part*
problemchild(Part *p)
{
	Part *np;
	Hline *hl;
	String *boundary;
	char *cp;

	/*
	 * We don't know whether the attachment is okay.
	 * If there's an external checker, let it have a crack at it.
	 */
	if(runchecker(p) > 0)
		return p;

	if(justreject)
		return p;

	syslog(0, "mail", "vf wrapped %s %s", p->type?s_to_c(p->type):"?",
		p->filename?s_to_c(p->filename):"?");

	boundary = mkboundary();
	/* print out non-mime headers */
	for(hl = p->hl; hl != nil; hl = hl->next)
		if(cistrncmp(s_to_c(hl->s), "content-", 8) != 0)
			Bprint(&out, "%s", s_to_c(hl->s));

	/* add in our own multipart headers and message */
	Bprint(&out, "Content-Type: multipart/mixed;\n");
	Bprint(&out, "\tboundary=\"%s\"\n", s_to_c(boundary));
	Bprint(&out, "Content-Disposition: inline\n");
	Bprint(&out, "\n");
	Bprint(&out, "This is a multi-part message in MIME format.\n");
	Bprint(&out, "--%s\n", s_to_c(boundary));
	Bprint(&out, "Content-Disposition: inline\n");
	Bprint(&out, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
	Bprint(&out, "Content-Transfer-Encoding: 7bit\n");
	Bprint(&out, "\n");
	Bprint(&out, "from postmaster@%s:\n", sysname());
	Bprint(&out, "The following attachment had content that we can't\n");
	Bprint(&out, "prove to be harmless.  To avoid possible automatic\n");
	Bprint(&out, "execution, we changed the content headers.\n");
	Bprint(&out, "The original header was:\n\n");

	/* print out original header lines */
	for(hl = p->hl; hl != nil; hl = hl->next)
		if(cistrncmp(s_to_c(hl->s), "content-", 8) == 0)
			Bprint(&out, "\t%s", s_to_c(hl->s));
	Bprint(&out, "--%s\n", s_to_c(boundary));

	/* change file name */
	if(p->filename)
		s_append(p->filename, ".suspect");
	else
		p->filename = s_copy("file.suspect");

	/* print out new header */
	Bprint(&out, "Content-Type: application/octet-stream\n");
	Bprint(&out, "Content-Disposition: attachment; filename=\"%s\"\n", s_to_c(p->filename));
	switch(p->encoding){
	case Enone:
		break;
	case Ebase64:
		Bprint(&out, "Content-Transfer-Encoding: base64\n");
		break;
	case Equoted:
		Bprint(&out, "Content-Transfer-Encoding: quoted-printable\n");
		break;
	}

	/* pass the body */
	np = passbody(p, 0);

	/* add the new boundary and the original terminator */
	Bprint(&out, "--%s--\n", s_to_c(boundary));
	if(np && np->boundary){
		cp = Brdline(&in, '\n');
		Bwrite(&out, cp, Blinelen(&in));
	}

	return np;
}

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;
}

/*
 *  parse content type header
 */
static void
ctype(Part *p, Hdef *h, char *cp)
{
	String *s;

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

	p->type = s_new();
	cp = getstring(cp, p->type, 1);
	if(badtype(s_to_c(p->type)))
		p->badtype = 1;

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

		cp = skiptosemi(cp);
	}
}

/*
 *  parse content encoding header
 */
static void
cencoding(Part *m, Hdef *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;
}

/*
 *  parse content disposition header
 */
static void
cdisposition(Part *p, Hdef *h, char *cp)
{
	cp += h->len;
	cp = skipwhite(cp);
	while(*cp){
		if(cistrncmp(cp, "inline", 6) == 0){
			p->disposition = Dinline;
		} else if(cistrncmp(cp, "attachment", 10) == 0){
			p->disposition = Dfile;
		} else if(cistrncmp(cp, "filename=", 9) == 0){
			cp += 9;
			setfilename(p, cp);
		}
		cp = skiptosemi(cp);
	}

}

static void
setfilename(Part *p, char *name)
{
	if(p->filename == nil)
		p->filename = s_new();
	getstring(name, s_reset(p->filename), 0);
	p->filename = tokenconvert(p->filename);
	p->badfile = badfile(s_to_c(p->filename));
}

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;
}

/*
 *  parse a possibly "'d string from a header.  A
 *  ';' terminates the string.
 */
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
init_hdefs(void)
{
	Hdef *hd;
	static int already;

	if(already)
		return;
	already = 1;

	for(hd = hdefs; hd->type != nil; hd++)
		hd->len = strlen(hd->type);
}

/*
 *  create a new boundary
 */
static String*
mkboundary(void)
{
	char buf[32];
	int i;
	static int already;

	if(already == 0){
		srand((time(0)<<16)|getpid());
		already = 1;
	}
	strcpy(buf, "upas-");
	for(i = 5; i < sizeof(buf)-1; i++)
		buf[i] = 'a' + nrand(26);
	buf[i] = 0;
	return s_copy(buf);
}

/*
 *  skip blank lines till header
 */
static void
passnotheader(void)
{
	char *cp;
	int i, n;

	while((cp = Brdline(&in, '\n')) != nil){
		n = Blinelen(&in);
		for(i = 0; i < n-1; i++)
			if(cp[i] != ' ' && cp[i] != '\t' && cp[i] != '\r'){
				Bseek(&in, -n, 1);
				return;
			}
		Bwrite(&out, cp, n);
	}
}

/*
 *  pass unix header lines
 */
static void
passunixheader(void)
{
	char *p;
	int n;

	while((p = Brdline(&in, '\n')) != nil){
		n = Blinelen(&in);
		if(strncmp(p, "From ", 5) != 0){
			Bseek(&in, -n, 1);
			break;
		}
		Bwrite(&out, p, n);
	}
}

/*
 *  Read mime types
 */
static void
readmtypes(void)
{
	Biobuf *b;
	char *p;
	char *f[6];
	Mtype *m;
	Mtype **l;

	b = Bopen("/sys/lib/mimetype", OREAD);
	if(b == nil)
		return;

	l = &mtypes;
	while((p = Brdline(b, '\n')) != nil){
		if(*p == '#')
			continue;
		p[Blinelen(b)-1] = 0;
		if(tokenize(p, f, nelem(f)) < 5)
			continue;
		m = mallocz(sizeof *m, 1);
		if(m == nil)
			goto err;
		m->ext = strdup(f[0]);
		if(m->ext == 0)
			goto err;
		m->gtype = strdup(f[1]);
		if(m->gtype == 0)
			goto err;
		m->stype = strdup(f[2]);
		if(m->stype == 0)
			goto err;
		m->class = *f[4];
		*l = m;
		l = &(m->next);
	}
	Bterm(b);
	return;
err:
	if(m == nil)
		return;
	free(m->ext);
	free(m->gtype);
	free(m->stype);
	free(m);
	Bterm(b);
}

/*
 *  if the class is 'm' or 'y', accept it
 *  if the class is 'p' check a previous extension
 *  otherwise, filename is bad
 */
static int
badfile(char *name)
{
	char *p;
	Mtype *m;
	int rv;

	p = strrchr(name, '.');
	if(p == nil)
		return 0;

	for(m = mtypes; m != nil; m = m->next)
		if(cistrcmp(p, m->ext) == 0){
			switch(m->class){
			case 'm':
			case 'y':
				return 0;
			case 'p':
				*p = 0;
				rv = badfile(name);
				*p = '.';
				return rv;
			case 'r':
				return 2;
			}
		}
	return 1;
}

/*
 *  if the class is 'm' or 'y' or 'p', accept it
 *  otherwise, filename is bad
 */
static int
badtype(char *type)
{
	Mtype *m;
	char *s, *fix;
	int rv = 1;

	fix = s = strchr(type, '/');
	if(s != nil)
		*s++ = 0;
	else
		s = "-";

	for(m = mtypes; m != nil; m = m->next){
		if(cistrcmp(type, m->gtype) != 0)
			continue;
		if(cistrcmp(s, m->stype) != 0)
			continue;
		switch(m->class){
		case 'y':
		case 'p':
		case 'm':
			rv = 0;
			break;
		}
		break;
	}

	if(fix != nil)
		*fix = '/';
	return rv;
}

/* rfc2047 non-ascii */
typedef struct Charset Charset;
struct Charset {
	char *name;
	int len;
	int convert;
} charsets[] =
{
	{ "us-ascii",		8,	1, },
	{ "utf-8",		5,	0, },
	{ "iso-8859-1",		10,	1, },
};

/*
 *  convert to UTF if need be
 */
static String*
tokenconvert(String *t)
{
	String *s;
	char decoded[1024];
	char utfbuf[2*1024];
	int i, len;
	char *e;
	char *token;

	token = s_to_c(t);
	len = s_len(t);

	if(token[0] != '=' || token[1] != '?' ||
	   token[len-2] != '?' || token[len-1] != '=')
		goto err;
	e = token+len-2;
	token += 2;

	/* bail if we don't understand the character set */
	for(i = 0; i < nelem(charsets); i++)
		if(cistrncmp(charsets[i].name, token, charsets[i].len) == 0)
		if(token[charsets[i].len] == '?'){
			token += charsets[i].len + 1;
			break;
		}
	if(i >= nelem(charsets))
		goto err;

	/* bail if it doesn't fit */
	if(strlen(token) > sizeof(decoded)-1)
		goto err;

	/* bail if we don't understand the encoding */
	if(cistrncmp(token, "b?", 2) == 0){
		token += 2;
		len = dec64((uchar*)decoded, sizeof(decoded), token, e-token);
		if(len == -1)
			goto err;
		decoded[len] = 0;
	} else if(cistrncmp(token, "q?", 2) == 0){
		token += 2;
		len = decquoted(decoded, token, e);
		if(len > 0 && decoded[len-1] == '\n')
			len--;
		decoded[len] = 0;
	} else
		goto err;

	s = nil;
	switch(charsets[i].convert){
	case 0:
		s = s_copy(decoded);
		break;
	case 1:
		s = s_new();
		latin1toutf(utfbuf, decoded, decoded+len);
		s_append(s, utfbuf);
		break;
	}

	return s;
err:
	return s_clone(t);
}

/*
 *  decode quoted
 */
enum
{
	Self=	1,
	Hex=	2,
};
uchar	tableqp[256];

static void
initquoted(void)
{
	int c;

	memset(tableqp, 0, 256);
	for(c = ' '; c <= '<'; c++)
		tableqp[c] = Self;
	for(c = '>'; c <= '~'; c++)
		tableqp[c] = Self;
	tableqp['\t'] = Self;
	tableqp['='] = Hex;
}

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 0;
}

static char*
decquotedline(char *out, char *in, char *e)
{
	int c, 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(tableqp[c]){
		case Self:
			*out++ = c;
			break;
		case Hex:
			c = hex2int(*in++)<<4;
			c |= hex2int(*in++);
			*out++ = c;
			break;
		}
	}
	if(!soft)
		*out++ = '\n';
	*out = 0;

	return out;
}

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

	if(tableqp[' '] == 0)
		initquoted();

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

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

	return p - out;
}

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

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