Subversion Repositories planix.SVN

Rev

Blame | Last modification | View Log | RSS feed

#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(n+1);
        if(p == nil)
                return 0;

        for(; in < e; in++){
                r = (uchar)*in;
                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, "%ld", 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);
}