Subversion Repositories planix.SVN

Rev

Rev 33 | 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 type "M" uchar*
#pragma varargck argpos pop3cmd 2

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

        char *host;
        char *user;
        char *port;

        int ppop;
        int refreshtime;
        int debug;
        int pipeline;
        int encrypted;
        int needtls;
        int notls;
        int needssl;

        // open network connection
        Biobuf bin;
        Biobuf bout;
        int fd;
        char *lastline; // from Brdstr

        Thumbprint *thumb;
};

char*
geterrstr(void)
{
        static char err[Errlen];

        err[0] = '\0';
        errstr(err, sizeof(err));
        return err;
}

//
// get pop3 response line , without worrying
// about multiline responses; the clients
// will deal with that.
//
static int
isokay(char *s)
{
        return s!=nil && strncmp(s, "+OK", 3)==0;
}

static void
pop3cmd(Pop *pop, char *fmt, ...)
{
        char buf[128], *p;
        va_list va;

        va_start(va, fmt);
        vseprint(buf, buf+sizeof(buf), fmt, va);
        va_end(va);

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

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

static char*
pop3resp(Pop *pop)
{
        char *s;
        char *p;

        alarm(60*1000);
        if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
                close(pop->fd);
                pop->fd = -1;
                alarm(0);
                return "unexpected eof";
        }
        alarm(0);

        p = s+strlen(s)-1;
        while(p >= s && (*p == '\r' || *p == '\n'))
                *p-- = '\0';

        if(pop->debug)
                fprint(2, "-> %s\n", s);
        free(pop->lastline);
        pop->lastline = s;
        return s;
}

static int
pop3log(char *fmt, ...)
{
        va_list ap;

        va_start(ap,fmt);
        syslog(0, "/sys/log/pop3", fmt, ap);
        va_end(ap);
        return 0;
}

static char*
pop3pushtls(Pop *pop)
{
        int fd;
        uchar digest[SHA1dlen];
        TLSconn conn;

        memset(&conn, 0, sizeof conn);
        // conn.trace = pop3log;
        fd = tlsClient(pop->fd, &conn);
        if(fd < 0)
                return "tls error";
        if(conn.cert==nil || conn.certlen <= 0){
                close(fd);
                return "server did not provide TLS certificate";
        }
        sha1(conn.cert, conn.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 && (!pop->thumb || !okThumbprint(digest, SHA1dlen, pop->thumb))){
                fmtinstall('H', encodefmt);
                close(fd);
                free(conn.cert);
                fprint(2, "upas/fs pop3: server certificate %.*H not recognized\n", SHA1dlen, digest);
                return "bad server certificate";
        }
        free(conn.cert);
        close(pop->fd);
        pop->fd = fd;
        pop->encrypted = 1;
        Binit(&pop->bin, pop->fd, OREAD);
        Binit(&pop->bout, pop->fd, OWRITE);
        return nil;
}

//
// get capability list, possibly start tls
//
static char*
pop3capa(Pop *pop)
{
        char *s;
        int hastls;

        pop3cmd(pop, "CAPA");
        if(!isokay(pop3resp(pop)))
                return nil;

        hastls = 0;
        for(;;){
                s = pop3resp(pop);
                if(strcmp(s, ".") == 0 || strcmp(s, "unexpected eof") == 0)
                        break;
                if(strcmp(s, "STLS") == 0)
                        hastls = 1;
                if(strcmp(s, "PIPELINING") == 0)
                        pop->pipeline = 1;
                if(strcmp(s, "EXPIRE 0") == 0)
                        return "server does not allow mail to be left on server";
        }

        if(hastls && !pop->notls){
                pop3cmd(pop, "STLS");
                if(!isokay(s = pop3resp(pop)))
                        return s;
                if((s = pop3pushtls(pop)) != nil)
                        return s;
        }
        return nil;
}

//
// log in using APOP if possible, password if allowed by user
//
static char*
pop3login(Pop *pop)
{
        int n;
        char *s, *p, *q;
        char ubuf[128], user[128];
        char buf[500];
        UserPasswd *up;

        s = pop3resp(pop);
        if(!isokay(s))
                return "error in initial handshake";

        if(pop->user)
                snprint(ubuf, sizeof ubuf, " user=%q", pop->user);
        else
                ubuf[0] = '\0';

        // look for apop banner
        if(pop->ppop==0 && (p = strchr(s, '<')) && (q = strchr(p+1, '>'))) {
                *++q = '\0';
                if((n=auth_respond(p, q-p, user, sizeof user, buf, sizeof buf, auth_getkey, "proto=apop role=client server=%q%s",
                        pop->host, ubuf)) < 0)
                        return "factotum failed";
                if(user[0]=='\0')
                        return "factotum did not return a user name";

                if(s = pop3capa(pop))
                        return s;

                pop3cmd(pop, "APOP %s %.*s", user, n, buf);
                if(!isokay(s = pop3resp(pop)))
                        return s;

                return nil;
        } else {
                if(pop->ppop == 0)
                        return "no APOP hdr from server";

                if(s = pop3capa(pop))
                        return s;

                if(pop->needtls && !pop->encrypted)
                        return "could not negotiate TLS";

                up = auth_getuserpasswd(auth_getkey, "proto=pass service=pop dom=%q%s",
                        pop->host, ubuf);
                if(up == nil)
                        return "no usable keys found";

                pop3cmd(pop, "USER %s", up->user);
                if(!isokay(s = pop3resp(pop))){
                        free(up);
                        return s;
                }
                pop3cmd(pop, "PASS %s", up->passwd);
                free(up);
                if(!isokay(s = pop3resp(pop)))
                        return s;

                return nil;
        }
}

//
// dial and handshake with pop server
//
static char*
pop3dial(Pop *pop)
{
        char *err;

        if((pop->fd = dial(netmkaddr(pop->host, "net", pop->needssl ? "pop3s" : "pop3"), 0, 0, 0)) < 0)
                return geterrstr();

        if(pop->needssl){
                if((err = pop3pushtls(pop)) != nil)
                        return err;
        }else{
                Binit(&pop->bin, pop->fd, OREAD);
                Binit(&pop->bout, pop->fd, OWRITE);
        }

        if(err = pop3login(pop)) {
                close(pop->fd);
                return err;
        }

        return nil;
}

//
// close connection
//
static void
pop3hangup(Pop *pop)
{
        pop3cmd(pop, "QUIT");
        pop3resp(pop);
        close(pop->fd);
}

//
// download a single message
//
static char*
pop3download(Pop *pop, Message *m)
{
        char *s, *f[3], *wp, *ep;
        char sdigest[SHA1dlen*2+1];
        int i, l, sz;

        if(!pop->pipeline)
                pop3cmd(pop, "LIST %d", m->mesgno);
        if(!isokay(s = pop3resp(pop)))
                return s;

        if(tokenize(s, f, 3) != 3)
                return "syntax error in LIST response";

        if(atoi(f[1]) != m->mesgno)
                return "out of sync with pop3 server";

        sz = atoi(f[2])+200;    /* 200 because the plan9 pop3 server lies */
        if(sz == 0)
                return "invalid size in LIST response";

        m->start = wp = emalloc(sz+1);
        ep = wp+sz;

        if(!pop->pipeline)
                pop3cmd(pop, "RETR %d", m->mesgno);
        if(!isokay(s = pop3resp(pop))) {
                m->start = nil;
                free(wp);
                return s;
        }

        s = nil;
        while(wp <= ep) {
                s = pop3resp(pop);
                if(strcmp(s, "unexpected eof") == 0) {
                        free(m->start);
                        m->start = nil;
                        return "unexpected end of conversation";
                }
                if(strcmp(s, ".") == 0)
                        break;

                l = strlen(s)+1;
                if(s[0] == '.') {
                        s++;
                        l--;
                }
                /*
                 * grow by 10%/200bytes - some servers
                 *  lie about message sizes
                 */
                if(wp+l > ep) {
                        int pos = wp - m->start;
                        sz += ((sz / 10) < 200)? 200: sz/10;
                        m->start = erealloc(m->start, sz+1);
                        wp = m->start+pos;
                        ep = m->start+sz;
                }
                memmove(wp, s, l-1);
                wp[l-1] = '\n';
                wp += l;
        }

        if(s == nil || strcmp(s, ".") != 0)
                return "out of sync with pop3 server";

        m->end = wp;

        // make sure there's a trailing null
        // (helps in body searches)
        *m->end = 0;
        m->bend = m->rbend = m->end;
        m->header = m->start;

        // digest message
        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 pop server
// UIDL is not required by RFC 1939, but 
// netscape requires it, so almost every server supports it.
// we'll use it to make our lives easier.
//
static char*
pop3read(Pop *pop, Mailbox *mb, int doplumb)
{
        char *s, *p, *uidl, *f[2];
        int mesgno, ignore, nnew;
        Message *m, *next, **l;

        // Some POP servers disallow UIDL if the maildrop is empty.
        pop3cmd(pop, "STAT");
        if(!isokay(s = pop3resp(pop)))
                return s;

        // fetch message listing; note messages to grab
        l = &mb->root->part;
        if(strncmp(s, "+OK 0 ", 6) != 0) {
                pop3cmd(pop, "UIDL");
                if(!isokay(s = pop3resp(pop)))
                        return s;

                for(;;){
                        p = pop3resp(pop);
                        if(strcmp(p, ".") == 0 || strcmp(p, "unexpected eof") == 0)
                                break;

                        if(tokenize(p, f, 2) != 2)
                                continue;

                        mesgno = atoi(f[0]);
                        uidl = f[1];
                        if(strlen(uidl) > 75)   // RFC 1939 says 70 characters max
                                continue;

                        ignore = 0;
                        while(*l != nil) {
                                if(strcmp((*l)->uidl, uidl) == 0) {
                                        // matches mail we already have, note mesgno for deletion
                                        (*l)->mesgno = mesgno;
                                        ignore = 1;
                                        l = &(*l)->next;
                                        break;
                                } else {
                                        // old mail no longer in box mark deleted
                                        if(doplumb)
                                                mailplumb(mb, *l, 1);
                                        (*l)->inmbox = 0;
                                        (*l)->deleted = 1;
                                        l = &(*l)->next;
                                }
                        }
                        if(ignore)
                                continue;

                        m = newmessage(mb->root);
                        m->mallocd = 1;
                        m->inmbox = 1;
                        m->mesgno = mesgno;
                        strcpy(m->uidl, uidl);

                        // chain in; will fill in message later
                        *l = m;
                        l = &m->next;
                }
        }

        // whatever is left has been removed from the mbox, mark as deleted
        while(*l != nil) {
                if(doplumb)
                        mailplumb(mb, *l, 1);
                (*l)->inmbox = 0;
                (*l)->deleted = 1;
                l = &(*l)->next;
        }

        // download new messages
        nnew = 0;
        if(pop->pipeline){
                switch(rfork(RFPROC|RFMEM)){
                case -1:
                        fprint(2, "rfork: %r\n");
                        pop->pipeline = 0;

                default:
                        break;

                case 0:
                        for(m = mb->root->part; m != nil; m = m->next){
                                if(m->start != nil)
                                        continue;
                                Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", m->mesgno, m->mesgno);
                        }
                        Bflush(&pop->bout);
                        _exits(nil);
                }
        }

        for(m = mb->root->part; m != nil; m = next) {
                next = m->next;

                if(m->start != nil)
                        continue;

                if(s = pop3download(pop, m)) {
                        // message disappeared? unchain
                        fprint(2, "download %d: %s\n", m->mesgno, s);
                        delmessage(mb, m);
                        mb->root->subname--;
                        continue;
                }
                nnew++;
                parse(m, 0, mb, 1);

                if(doplumb)
                        mailplumb(mb, m, 0);
        }
        if(pop->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;     
}

//
// delete marked messages
//
static void
pop3purge(Pop *pop, Mailbox *mb)
{
        Message *m, *next;

        if(pop->pipeline){
                switch(rfork(RFPROC|RFMEM)){
                case -1:
                        fprint(2, "rfork: %r\n");
                        pop->pipeline = 0;

                default:
                        break;

                case 0:
                        for(m = mb->root->part; m != nil; m = next){
                                next = m->next;
                                if(m->deleted && m->refs == 0){
                                        if(m->inmbox)
                                                Bprint(&pop->bout, "DELE %d\r\n", m->mesgno);
                                }
                        }
                        Bflush(&pop->bout);
                        _exits(nil);
                }
        }
        for(m = mb->root->part; m != nil; m = next) {
                next = m->next;
                if(m->deleted && m->refs == 0) {
                        if(m->inmbox) {
                                if(!pop->pipeline)
                                        pop3cmd(pop, "DELE %d", m->mesgno);
                                if(isokay(pop3resp(pop)))
                                        delmessage(mb, m);
                        } else
                                delmessage(mb, m);
                }
        }
}


// connect to pop3 server, sync mailbox
static char*
pop3sync(Mailbox *mb, int doplumb)
{
        char *err;
        Pop *pop;

        pop = mb->aux;

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

        if((err = pop3read(pop, mb, doplumb)) == nil){
                pop3purge(pop, mb);
                mb->d->atime = mb->d->mtime = time(0);
        }
        pop3hangup(pop);
        mb->waketime = time(0) + pop->refreshtime;
        return err;
}

static char Epop3ctl[] = "bad pop3 control message";

static char*
pop3ctl(Mailbox *mb, int argc, char **argv)
{
        int n;
        Pop *pop;

        pop = mb->aux;
        if(argc < 1)
                return Epop3ctl;

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

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

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

        return Epop3ctl;
}

// free extra memory associated with mb
static void
pop3close(Mailbox *mb)
{
        Pop *pop;

        pop = mb->aux;
        free(pop->freep);
        free(pop);
}

//
// open mailboxes of the form /pop/host/user or /apop/host/user
//
char*
pop3mbox(Mailbox *mb, char *path)
{
        char *f[10];
        int nf, apop, ppop, popssl, apopssl, apoptls, popnotls, apopnotls, poptls;
        Pop *pop;

        quotefmtinstall();
        popssl = strncmp(path, "/pops/", 6) == 0;
        apopssl = strncmp(path, "/apops/", 7) == 0;
        poptls = strncmp(path, "/poptls/", 8) == 0;
        popnotls = strncmp(path, "/popnotls/", 10) == 0;
        ppop = popssl || poptls || popnotls || strncmp(path, "/pop/", 5) == 0;
        apoptls = strncmp(path, "/apoptls/", 9) == 0;
        apopnotls = strncmp(path, "/apopnotls/", 11) == 0;
        apop = apopssl || apoptls || apopnotls || strncmp(path, "/apop/", 6) == 0;

        if(!ppop && !apop)
                return Enotme;

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

        nf = getfields(path, f, nelem(f), 0, "/");
        if(nf != 3 && nf != 4) {
                free(path);
                return "bad pop3 path syntax /[a]pop[tls|ssl]/system[/user]";
        }

        pop = emalloc(sizeof(*pop));
        pop->freep = path;
        pop->host = f[2];
        if(nf < 4)
                pop->user = nil;
        else
                pop->user = f[3];
        pop->ppop = ppop;
        pop->needssl = popssl || apopssl;
        pop->needtls = poptls || apoptls;
        pop->refreshtime = 60;
        pop->notls = popnotls || apopnotls;
        pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude","x509");

        mb->aux = pop;
        mb->sync = pop3sync;
        mb->close = pop3close;
        mb->ctl = pop3ctl;
        mb->d = emalloc(sizeof(*mb->d));

        return nil;
}