Subversion Repositories planix.SVN

Rev

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

#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <libsec.h>
#include <auth.h>
#include "dat.h"

#pragma varargck argpos imap4cmd 2
#pragma varargck        type    "Z"     char*

int     doublequote(Fmt*);
int     pipeline = 1;

static char Eio[] = "i/o error";

typedef struct Imap Imap;
struct Imap {
        char *freep;    // free this to free the strings below

        char *host;
        char *user;
        char *mbox;

        int mustssl;
        int refreshtime;
        int debug;

        ulong tag;
        ulong validity;
        int nmsg;
        int size;
        char *base;
        char *data;

        vlong *uid;
        int nuid;
        int muid;

        Thumbprint *thumb;

        // open network connection
        Biobuf bin;
        Biobuf bout;
        int fd;
};

static char*
removecr(char *s)
{
        char *r, *w;

        for(r=w=s; *r; r++)
                if(*r != '\r')
                        *w++ = *r;
        *w = '\0';
        return s;
}

//
// send imap4 command
//
static void
imap4cmd(Imap *imap, char *fmt, ...)
{
        char buf[128], *p;
        va_list va;

        va_start(va, fmt);
        p = buf+sprint(buf, "9X%lud ", imap->tag);
        vseprint(p, buf+sizeof(buf), fmt, va);
        va_end(va);

        p = buf+strlen(buf);
        if(p > (buf+sizeof(buf)-3))
                sysfatal("imap4 command too long");

        if(imap->debug)
                fprint(2, "-> %s\n", buf);
        strcpy(p, "\r\n");
        Bwrite(&imap->bout, buf, strlen(buf));
        Bflush(&imap->bout);
}

enum {
        OK,
        NO,
        BAD,
        BYE,
        EXISTS,
        STATUS,
        FETCH,
        UNKNOWN,
};

static char *verblist[] = {
[OK]            "OK",
[NO]            "NO",
[BAD]   "BAD",
[BYE]   "BYE",
[EXISTS]        "EXISTS",
[STATUS]        "STATUS",
[FETCH] "FETCH",
};

static int
verbcode(char *verb)
{
        int i;
        char *q;

        if(q = strchr(verb, ' '))
                *q = '\0';

        for(i=0; i<nelem(verblist); i++)
                if(verblist[i] && strcmp(verblist[i], verb)==0){
                        if(q)
                                *q = ' ';
                        return i;
                }
        if(q)
                *q = ' ';
        return UNKNOWN;
}

static void
strupr(char *s)
{
        for(; *s; s++)
                if('a' <= *s && *s <= 'z')
                        *s += 'A'-'a';
}

static void
imapgrow(Imap *imap, int n)
{
        int i;

        if(imap->data == nil){
                imap->base = emalloc(n+1);      
                imap->data = imap->base;
                imap->size = n+1;
        }
        if(n >= imap->size){
                // friggin microsoft - reallocate
                i = imap->data - imap->base;
                imap->base = erealloc(imap->base, i+n+1);
                imap->data = imap->base + i;
                imap->size = n+1;
        }
}


//
// get imap4 response line.  there might be various 
// data or other informational lines mixed in.
//
static char*
imap4resp(Imap *imap)
{
        char *line, *p, *ep, *op, *q, *r, *en, *verb;
        int i, n;
        static char error[256];

        while(p = Brdline(&imap->bin, '\n')){
                ep = p+Blinelen(&imap->bin);
                while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r'))
                        *--ep = '\0';
                
                if(imap->debug)
                        fprint(2, "<- %s\n", p);
                strupr(p);

                switch(p[0]){
                case '+':
                        if(imap->tag == 0)
                                fprint(2, "unexpected: %s\n", p);
                        break;

                // ``unsolicited'' information; everything happens here.
                case '*':
                        if(p[1]!=' ')
                                continue;
                        p += 2;
                        line = p;
                        n = strtol(p, &p, 10);
                        if(*p==' ')
                                p++;
                        verb = p;
                        
                        if(p = strchr(verb, ' '))
                                p++;
                        else
                                p = verb+strlen(verb);

                        switch(verbcode(verb)){
                        case OK:
                        case NO:
                        case BAD:
                                // human readable text at p;
                                break;
                        case BYE:
                                // early disconnect
                                // human readable text at p;
                                break;

                        // * 32 EXISTS
                        case EXISTS:
                                imap->nmsg = n;
                                break;

                        // * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964)
                        case STATUS:
                                if(q = strstr(p, "MESSAGES"))
                                        imap->nmsg = atoi(q+8);
                                if(q = strstr(p, "UIDVALIDITY"))
                                        imap->validity = strtoul(q+11, 0, 10);
                                break;

                        case FETCH:
                                // * 1 FETCH (uid 8889 RFC822.SIZE 3031 body[] {3031}
                                // <3031 bytes of data>
                                // )
                                if(strstr(p, "RFC822.SIZE") && strstr(p, "BODY[]")){
                                        if((q = strchr(p, '{')) 
                                        && (n=strtol(q+1, &en, 0), *en=='}')){
                                                if(imap->data == nil || n >= imap->size)
                                                        imapgrow(imap, n);
                                                if((i = Bread(&imap->bin, imap->data, n)) != n){
                                                        snprint(error, sizeof error,
                                                                "short read %d != %d: %r\n",
                                                                i, n);
                                                        return error;
                                                }
                                                if(imap->debug)
                                                        fprint(2, "<- read %d bytes\n", n);
                                                imap->data[n] = '\0';
                                                if(imap->debug)
                                                        fprint(2, "<- %s\n", imap->data);
                                                imap->data += n;
                                                imap->size -= n;
                                                p = Brdline(&imap->bin, '\n');
                                                if(imap->debug)
                                                        fprint(2, "<- ignoring %.*s\n",
                                                                Blinelen(&imap->bin), p);
                                        }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
                                                *r = '\0';
                                                q++;
                                                n = r-q;
                                                if(imap->data == nil || n >= imap->size)
                                                        imapgrow(imap, n);
                                                memmove(imap->data, q, n);
                                                imap->data[n] = '\0';
                                                imap->data += n;
                                                imap->size -= n;
                                        }else
                                                return "confused about FETCH response";
                                        break;
                                }

                                // * 1 FETCH (UID 1 RFC822.SIZE 511)
                                if(q=strstr(p, "RFC822.SIZE")){
                                        imap->size = atoi(q+11);
                                        break;
                                }

                                // * 1 FETCH (UID 1 RFC822.HEADER {496}
                                // <496 bytes of data>
                                // )
                                // * 1 FETCH (UID 1 RFC822.HEADER "data")
                                if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){
                                        if((q = strchr(p, '{')) 
                                        && (n=strtol(q+1, &en, 0), *en=='}')){
                                                if(imap->data == nil || n >= imap->size)
                                                        imapgrow(imap, n);
                                                if((i = Bread(&imap->bin, imap->data, n)) != n){
                                                        snprint(error, sizeof error,
                                                                "short read %d != %d: %r\n",
                                                                i, n);
                                                        return error;
                                                }
                                                if(imap->debug)
                                                        fprint(2, "<- read %d bytes\n", n);
                                                imap->data[n] = '\0';
                                                if(imap->debug)
                                                        fprint(2, "<- %s\n", imap->data);
                                                imap->data += n;
                                                imap->size -= n;
                                                p = Brdline(&imap->bin, '\n');
                                                if(imap->debug)
                                                        fprint(2, "<- ignoring %.*s\n",
                                                                Blinelen(&imap->bin), p);
                                        }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
                                                *r = '\0';
                                                q++;
                                                n = r-q;
                                                if(imap->data == nil || n >= imap->size)
                                                        imapgrow(imap, n);
                                                memmove(imap->data, q, n);
                                                imap->data[n] = '\0';
                                                imap->data += n;
                                                imap->size -= n;
                                        }else
                                                return "confused about FETCH response";
                                        break;
                                }

                                // * 1 FETCH (UID 1)
                                // * 2 FETCH (UID 6)
                                if(q = strstr(p, "UID")){
                                        if(imap->nuid < imap->muid)
                                                imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10);
                                        break;
                                }
                        }

                        if(imap->tag == 0)
                                return line;
                        break;

                case '9':               // response to our message
                        op = p;
                        if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){
                                while(*p==' ')
                                        p++;
                                imap->tag++;
                                return p;
                        }
                        fprint(2, "expected %lud; got %s\n", imap->tag, op);
                        break;

                default:
                        if(imap->debug || *p)
                                fprint(2, "unexpected line: %s\n", p);
                }
        }
        snprint(error, sizeof error, "i/o error: %r\n");
        return error;
}

static int
isokay(char *resp)
{
        return strncmp(resp, "OK", 2)==0;
}

//
// log in to IMAP4 server, select mailbox, no SSL at the moment
//
static char*
imap4login(Imap *imap)
{
        char *s;
        UserPasswd *up;

        imap->tag = 0;
        s = imap4resp(imap);
        if(!isokay(s))
                return "error in initial IMAP handshake";

        if(imap->user != nil)
                up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
        else
                up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
        if(up == nil)
                return "cannot find IMAP password";

        imap->tag = 1;
        imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd);
        free(up);
        if(!isokay(s = imap4resp(imap)))
                return s;

        imap4cmd(imap, "SELECT %Z", imap->mbox);
        if(!isokay(s = imap4resp(imap)))
                return s;

        return nil;
}

static char*
imaperrstr(char *host, char *port)
{
        /*
         * make mess big enough to hold a TLS certificate fingerprint
         * plus quite a bit of slop.
         */
        static char mess[3 * Errlen];
        char err[Errlen];

        err[0] = '\0';
        errstr(err, sizeof(err));
        snprint(mess, sizeof(mess), "%s/%s:%s", host, port, err);
        return mess;
}

static int
starttls(Imap *imap, TLSconn *connp)
{
        int sfd;
        uchar digest[SHA1dlen];

        fmtinstall('H', encodefmt);
        memset(connp, 0, sizeof *connp);
        sfd = tlsClient(imap->fd, connp);
        if(sfd < 0) {
                werrstr("tlsClient: %r");
                return -1;
        }
        if(connp->cert==nil || connp->certlen <= 0) {
                close(sfd);
                werrstr("server did not provide TLS certificate");
                return -1;
        }
        sha1(connp->cert, connp->certlen, digest, nil);
        /*
         * don't do this any more.  our local it people are rotating their
         * certificates faster than we can keep up.
         */
        if(0 && (!imap->thumb || !okThumbprint(digest, SHA1dlen, imap->thumb))){
                close(sfd);
                werrstr("server certificate %.*H not recognized",
                        SHA1dlen, digest);
                return -1;
        }
        close(imap->fd);
        imap->fd = sfd;
        return sfd;
}

//
// dial and handshake with the imap server
//
static char*
imap4dial(Imap *imap)
{
        char *err, *port;
        int sfd;
        TLSconn conn;

        if(imap->fd >= 0){
                imap4cmd(imap, "noop");
                if(isokay(imap4resp(imap)))
                        return nil;
                close(imap->fd);
                imap->fd = -1;
        }

        if(imap->mustssl)
                port = "imaps";
        else
                port = "imap";

        if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
                return imaperrstr(imap->host, port);

        if(imap->mustssl){
                sfd = starttls(imap, &conn);
                if (sfd < 0) {
                        free(conn.cert);
                        return imaperrstr(imap->host, port);
                }
                if(imap->debug){
                        char fn[128];
                        int fd;

                        snprint(fn, sizeof fn, "%s/ctl", conn.dir);
                        fd = open(fn, ORDWR);
                        if(fd < 0)
                                fprint(2, "opening ctl: %r\n");
                        if(fprint(fd, "debug") < 0)
                                fprint(2, "writing ctl: %r\n");
                        close(fd);
                }
        }
        Binit(&imap->bin, imap->fd, OREAD);
        Binit(&imap->bout, imap->fd, OWRITE);

        if(err = imap4login(imap)) {
                close(imap->fd);
                return err;
        }

        return nil;
}

//
// close connection
//
static void
imap4hangup(Imap *imap)
{
        imap4cmd(imap, "LOGOUT");
        imap4resp(imap);
        close(imap->fd);
}

//
// download a single message
//
static char*
imap4fetch(Mailbox *mb, Message *m)
{
        int i;
        char *p, *s, sdigest[2*SHA1dlen+1];
        Imap *imap;

        imap = mb->aux;

        imap->size = 0;

        if(!isokay(s = imap4resp(imap)))
                return s;

        p = imap->base;
        if(p == nil)
                return "did not get message body";

        removecr(p);
        free(m->start);
        m->start = p;
        m->end = p+strlen(p);
        m->bend = m->rbend = m->end;
        m->header = m->start;

        imap->base = nil;
        imap->data = nil;

        parse(m, 0, mb, 1);

        // digest headers
        sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
        for(i = 0; i < SHA1dlen; i++)
                sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
        m->sdigest = s_copy(sdigest);

        return nil;
}

//
// check for new messages on imap4 server
// download new messages, mark deleted messages
//
static char*
imap4read(Imap *imap, Mailbox *mb, int doplumb)
{
        char *s;
        int i, ignore, nnew, t;
        Message *m, *next, **l;

        imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox);
        if(!isokay(s = imap4resp(imap)))
                return s;

        imap->nuid = 0;
        imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0]));
        imap->muid = imap->nmsg;

        if(imap->nmsg > 0){
                imap4cmd(imap, "UID FETCH 1:* UID");
                if(!isokay(s = imap4resp(imap)))
                        return s;
        }

        l = &mb->root->part;
        for(i=0; i<imap->nuid; i++){
                ignore = 0;
                while(*l != nil){
                        if((*l)->imapuid == imap->uid[i]){
                                ignore = 1;
                                l = &(*l)->next;
                                break;
                        }else{
                                // old mail, we don't have it anymore
                                if(doplumb)
                                        mailplumb(mb, *l, 1);
                                (*l)->inmbox = 0;
                                (*l)->deleted = 1;
                                l = &(*l)->next;
                        }
                }
                if(ignore)
                        continue;

                // new message
                m = newmessage(mb->root);
                m->mallocd = 1;
                m->inmbox = 1;
                m->imapuid = imap->uid[i];

                // add to chain, will download soon
                *l = m;
                l = &m->next;
        }

        // whatever is left at the end of the chain is gone
        while(*l != nil){
                if(doplumb)
                        mailplumb(mb, *l, 1);
                (*l)->inmbox = 0;
                (*l)->deleted = 1;
                l = &(*l)->next;
        }

        // download new messages
        t = imap->tag;
        if(pipeline)
        switch(rfork(RFPROC|RFMEM)){
        case -1:
                sysfatal("rfork: %r");
        default:
                break;
        case 0:
                for(m = mb->root->part; m != nil; m = m->next){
                        if(m->start != nil)
                                continue;
                        if(imap->debug)
                                fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
                                        t, (ulong)m->imapuid);
                        Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
                                t++, (ulong)m->imapuid);
                }
                Bflush(&imap->bout);
                _exits(nil);
        }

        nnew = 0;
        for(m=mb->root->part; m!=nil; m=next){
                next = m->next;
                if(m->start != nil)
                        continue;

                if(!pipeline){
                        Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
                                (ulong)imap->tag, (ulong)m->imapuid);
                        Bflush(&imap->bout);
                }

                if(s = imap4fetch(mb, m)){
                        // message disappeared?  unchain
                        fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s);
                        delmessage(mb, m);
                        mb->root->subname--;
                        continue;
                }
                nnew++;
                if(doplumb)
                        mailplumb(mb, m, 0);
        }
        if(pipeline)
                waitpid();

        if(nnew || mb->vers == 0){
                mb->vers++;
                henter(PATH(0, Qtop), mb->name,
                        (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
        }
        return nil;
}

//
// sync mailbox
//
static void
imap4purge(Imap *imap, Mailbox *mb)
{
        int ndel;
        Message *m, *next;

        ndel = 0;
        for(m=mb->root->part; m!=nil; m=next){
                next = m->next;
                if(m->deleted && m->refs==0){
                        if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){
                                imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid);
                                if(isokay(imap4resp(imap))){
                                        ndel++;
                                        delmessage(mb, m);
                                }
                        }else
                                delmessage(mb, m);
                }
        }

        if(ndel){
                imap4cmd(imap, "EXPUNGE");
                imap4resp(imap);
        }
}

//
// connect to imap4 server, sync mailbox
//
static char*
imap4sync(Mailbox *mb, int doplumb)
{
        char *err;
        Imap *imap;

        imap = mb->aux;

        if(err = imap4dial(imap)){
                mb->waketime = time(0) + imap->refreshtime;
                return err;
        }

        if((err = imap4read(imap, mb, doplumb)) == nil){
                imap4purge(imap, mb);
                mb->d->atime = mb->d->mtime = time(0);
        }
        /*
         * don't hang up; leave connection open for next time.
         */
        // imap4hangup(imap);
        mb->waketime = time(0) + imap->refreshtime;
        return err;
}

static char Eimap4ctl[] = "bad imap4 control message";

static char*
imap4ctl(Mailbox *mb, int argc, char **argv)
{
        int n;
        Imap *imap;

        imap = mb->aux;
        if(argc < 1)
                return Eimap4ctl;

        if(argc==1 && strcmp(argv[0], "debug")==0){
                imap->debug = 1;
                return nil;
        }

        if(argc==1 && strcmp(argv[0], "nodebug")==0){
                imap->debug = 0;
                return nil;
        }

        if(argc==1 && strcmp(argv[0], "thumbprint")==0){
                if(imap->thumb)
                        freeThumbprints(imap->thumb);
                imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude","x509");
        }
        if(strcmp(argv[0], "refresh")==0){
                if(argc==1){
                        imap->refreshtime = 60;
                        return nil;
                }
                if(argc==2){
                        n = atoi(argv[1]);
                        if(n < 15)
                                return Eimap4ctl;
                        imap->refreshtime = n;
                        return nil;
                }
        }

        return Eimap4ctl;
}

//
// free extra memory associated with mb
//
static void
imap4close(Mailbox *mb)
{
        Imap *imap;

        imap = mb->aux;
        free(imap->freep);
        free(imap->base);
        free(imap->uid);
        if(imap->fd >= 0)
                close(imap->fd);
        free(imap);
}

//
// open mailboxes of the form /imap/host/user
//
char*
imap4mbox(Mailbox *mb, char *path)
{
        char *f[10];
        int mustssl, nf;
        Imap *imap;

        quotefmtinstall();
        fmtinstall('Z', doublequote);
        if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0)
                return Enotme;
        mustssl = (strncmp(path, "/imaps/", 7) == 0);

        path = strdup(path);
        if(path == nil)
                return "out of memory";

        nf = getfields(path, f, 5, 0, "/");
        if(nf < 3){
                free(path);
                return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
        }

        imap = emalloc(sizeof(*imap));
        imap->fd = -1;
        imap->debug = debug;
        imap->freep = path;
        imap->mustssl = mustssl;
        imap->host = f[2];
        if(nf < 4)
                imap->user = nil;
        else
                imap->user = f[3];
        if(nf < 5)
                imap->mbox = "Inbox";
        else
                imap->mbox = f[4];
        imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude","x509");

        mb->aux = imap;
        mb->sync = imap4sync;
        mb->close = imap4close;
        mb->ctl = imap4ctl;
        mb->d = emalloc(sizeof(*mb->d));
        //mb->fetch = imap4fetch;

        return nil;
}

//
// Formatter for %"
// Use double quotes to protect white space, frogs, \ and "
//
enum
{
        Qok = 0,
        Qquote,
        Qbackslash,
};

static int
needtoquote(Rune r)
{
        if(r >= Runeself)
                return Qquote;
        if(r <= ' ')
                return Qquote;
        if(r=='\\' || r=='"')
                return Qbackslash;
        return Qok;
}

int
doublequote(Fmt *f)
{
        char *s, *t;
        int w, quotes;
        Rune r;

        s = va_arg(f->args, char*);
        if(s == nil || *s == '\0')
                return fmtstrcpy(f, "\"\"");

        quotes = 0;
        for(t=s; *t; t+=w){
                w = chartorune(&r, t);
                quotes |= needtoquote(r);
        }
        if(quotes == 0)
                return fmtstrcpy(f, s);

        fmtrune(f, '"');
        for(t=s; *t; t+=w){
                w = chartorune(&r, t);
                if(needtoquote(r) == Qbackslash)
                        fmtrune(f, '\\');
                fmtrune(f, r);
        }
        return fmtrune(f, '"');
}