diff options
author | rsc <devnull@localhost> | 2005-10-29 16:26:44 +0000 |
---|---|---|
committer | rsc <devnull@localhost> | 2005-10-29 16:26:44 +0000 |
commit | 5cdb17983ae6e6367ad7a940cb219eab247a9304 (patch) | |
tree | 8ca1ef49af2a96e7daebe624d91fdf679814a057 /src/cmd/upas/fs/imap4.c | |
parent | cd3745196389579fb78b9b01ef1daefb5a57aa71 (diff) | |
download | plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.gz plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.bz2 plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.zip |
Thanks to John Cummings.
Diffstat (limited to 'src/cmd/upas/fs/imap4.c')
-rw-r--r-- | src/cmd/upas/fs/imap4.c | 876 |
1 files changed, 876 insertions, 0 deletions
diff --git a/src/cmd/upas/fs/imap4.c b/src/cmd/upas/fs/imap4.c new file mode 100644 index 00000000..152d15cf --- /dev/null +++ b/src/cmd/upas/fs/imap4.c @@ -0,0 +1,876 @@ +#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"; jpc */ + +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; +} + +// +// push tls onto a connection +// +int +mypushtls(int fd) +{ + int p[2]; + char buf[10]; + + if(pipe(p) < 0) + return -1; + + switch(fork()){ + case -1: + close(p[0]); + close(p[1]); + return -1; + case 0: + close(p[1]); + dup(p[0], 0); + dup(p[0], 1); + sprint(buf, "/fd/%d", fd); + execl("/bin/tlsrelay", "tlsrelay", "-f", buf, nil); + _exits(nil); + default: + break; + } + close(fd); + close(p[0]); + return p[1]; +} + +// +// dial and handshake with the imap server +// +static char* +imap4dial(Imap *imap) +{ + char *err, *port; + uchar digest[SHA1dlen]; + 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 = "imap4"; + + if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0) + return geterrstr(); + + if(imap->mustssl){ + memset(&conn, 0, sizeof conn); + sfd = tlsClient(imap->fd, &conn); + if(sfd < 0) + sysfatal("tlsClient: %r"); + if(conn.cert==nil || conn.certlen <= 0) + sysfatal("server did not provide TLS certificate"); + sha1(conn.cert, conn.certlen, digest, nil); + if(!imap->thumb || !okThumbprint(digest, imap->thumb)){ + fmtinstall('H', encodefmt); + sysfatal("server certificate %.*H not recognized", SHA1dlen, digest); + } + free(conn.cert); + close(imap->fd); + imap->fd = sfd; + + 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 +// +#if 0 /* jpc */ +static void +imap4hangup(Imap *imap) +{ + imap4cmd(imap, "LOGOUT"); + imap4resp(imap); + close(imap->fd); +} +#endif + +// +// 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"); + } + 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"); + + 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, '"'); +} |