Subversion Repositories planix.SVN

Rev

Rev 2 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 *  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();

        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;
        char *cp;

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

        fd = open(file, OWRITE);
        if(fd < 0)
                return -1;
        seek(fd, 0, 2);
        Binit(&out, fd, OWRITE);
        cp = ctime(time(0));
        cp[28] = 0;
        Bprint(&out, "From virusfilter %s\n", cp);
        writeheader(p, 0);
        bodyoff = Boffset(&out);
        passbody(p, 1);
        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;

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

        boundary = mkboundary();
fprint(2, "x\n");
        /* 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));

fprint(2, "x\n");
        /* 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;
        }

fprint(2, "z\n");
        /* pass the body */
        np = passbody(p, 0);

fprint(2, "w\n");
        /* 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));
        }

fprint(2, "a %p\n", np);
        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);
                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;
}