diff options
Diffstat (limited to 'src/cmd/upas/nfs/imap.c')
-rw-r--r-- | src/cmd/upas/nfs/imap.c | 1715 |
1 files changed, 1715 insertions, 0 deletions
diff --git a/src/cmd/upas/nfs/imap.c b/src/cmd/upas/nfs/imap.c new file mode 100644 index 00000000..80ca59cb --- /dev/null +++ b/src/cmd/upas/nfs/imap.c @@ -0,0 +1,1715 @@ +/* + * Locking here is not quite right. + * Calling qlock(&z->lk) can block the proc, + * and when it comes back, boxes and msgs might have been freed + * (if the refresh proc was holding the lock and in the middle of a + * redial). I've tried to be careful about not assuming boxes continue + * to exist across imap commands, but maybe this isn't really tenable. + * Maybe instead we should ref count the boxes and messages. + */ + +#include "a.h" +#include <libsec.h> + +struct Imap +{ + int connected; + int autoreconnect; + int ticks; /* until boom! */ + char* server; + int mode; + int fd; + Biobuf b; + Ioproc* io; + QLock lk; + QLock rlk; + Rendez r; + + Box* inbox; + Box* box; + Box* nextbox; + + /* SEARCH results */ + uint *uid; + uint nuid; +}; + +static struct { + char *name; + int flag; +} flagstab[] = +{ + "Junk", FlagJunk, + "NonJunk", FlagNonJunk, + "\\Answered", FlagReplied, + "\\Flagged", FlagFlagged, + "\\Deleted", FlagDeleted, + "\\Draft", FlagDraft, + "\\Seen", FlagSeen, + "\\NoInferiors", FlagNoInferiors, + "\\NoSelect", FlagNoSelect, + "\\Marked", FlagMarked, + "\\UnMarked", FlagUnMarked, +}; + +int chattyimap; + +static char *tag = "+"; + +static void checkbox(Imap*, Box*); +static char* copyaddrs(Sx*); +static void freeup(UserPasswd*); +static int getbox(Imap*, Box*); +static int getboxes(Imap*); +static char* gsub(char*, char*, char*); +static int imapcmd(Imap*, Box*, char*, ...); +static Sx* imapcmdsx(Imap*, Box*, char*, ...); +static Sx* imapcmdsx0(Imap*, char*, ...); +static Sx* imapvcmdsx(Imap*, Box*, char*, va_list); +static Sx* imapvcmdsx0(Imap*, char*, va_list); +static int imapdial(char*, int); +static int imaplogin(Imap*); +static int imapquote(Fmt*); +static int imapreconnect(Imap*); +static void imaprefreshthread(void*); +static void imaptimerproc(void*); +static Sx* imapwaitsx(Imap*); +static int isatom(Sx *v, char *name); +static int islist(Sx *v); +static int isnil(Sx *v); +static int isnumber(Sx *sx); +static int isstring(Sx *sx); +static int ioimapdial(Ioproc*, char*, int); +static char* nstring(Sx*); +static void unexpected(Imap*, Sx*); +static Sx* zBrdsx(Imap*); + +/* + * Imap connection maintenance and login. + */ + +Imap* +imapconnect(char *server, int mode) +{ + Imap *z; + + fmtinstall('H', encodefmt); + fmtinstall('Z', imapquote); + + z = emalloc(sizeof *z); + z->server = estrdup(server); + z->mode = mode; + z->fd = -1; + z->autoreconnect = 0; + z->io = ioproc(); + + qlock(&z->lk); + if(imapreconnect(z) < 0){ + free(z); + return nil; + } + + z->r.l = &z->rlk; + z->autoreconnect = 1; + qunlock(&z->lk); + + proccreate(imaptimerproc, z, STACK); + mailthread(imaprefreshthread, z); + + return z; +} + +void +imaphangup(Imap *z, int ticks) +{ + z->ticks = ticks; + if(ticks == 0){ + close(z->fd); + z->fd = -1; + } +} + +static int +imapreconnect(Imap *z) +{ + Sx *sx; + + z->autoreconnect = 0; + z->box = nil; + z->inbox = nil; + + if(z->fd >= 0){ + close(z->fd); + z->fd = -1; + } + + if(chattyimap) + fprint(2, "dial %s...\n", z->server); + if((z->fd = ioimapdial(z->io, z->server, z->mode)) < 0) + return -1; + z->connected = 1; + Binit(&z->b, z->fd, OREAD); + if((sx = zBrdsx(z)) == nil){ + werrstr("no greeting"); + goto err; + } + if(chattyimap) + fprint(2, "<I %#$\n", sx); + if(sx->nsx >= 2 && isatom(sx->sx[0], "*") && isatom(sx->sx[1], "PREAUTH")){ + freesx(sx); + goto preauth; + } + if(!oksx(sx)){ + werrstr("bad greeting - %#$", sx); + goto err; + } + freesx(sx); + sx = nil; + if(imaplogin(z) < 0) + goto err; +preauth: + if(getboxes(z) < 0 || getbox(z, z->inbox) < 0) + goto err; + z->autoreconnect = 1; + return 0; + +err: + if(z->fd >= 0){ + close(z->fd); + z->fd = -1; + } + if(sx) + freesx(sx); + z->autoreconnect = 1; + z->connected = 0; + return -1; +} + +static int +imaplogin(Imap *z) +{ + Sx *sx; + UserPasswd *up; + + if((up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", z->server)) == nil){ + werrstr("getuserpasswd - %r"); + return -1; + } + + sx = imapcmdsx(z, nil, "LOGIN %Z %Z", up->user, up->passwd); + freeup(up); + if(sx == nil) + return -1; + if(!oksx(sx)){ + freesx(sx); + werrstr("login rejected - %#$", sx); + return -1; + } + return 0; +} + +static int +getboxes(Imap *z) +{ + int i; + Box **r, **w, **e; + + for(i=0; i<nboxes; i++){ + boxes[i]->mark = 1; + boxes[i]->exists = 0; + boxes[i]->maxseen = 0; + } + if(imapcmd(z, nil, "LIST %Z *", "") < 0) + return -1; + if(z->nextbox && z->nextbox->mark) + z->nextbox = nil; + for(r=boxes, w=boxes, e=boxes+nboxes; r<e; r++){ + if((*r)->mark) +{fprint(2, "*** free box %s %s\n", (*r)->name, (*r)->imapname); + boxfree(*r); +} + else + *w++ = *r; + } + nboxes = w - boxes; + return 0; +} + +static int +getbox(Imap *z, Box *b) +{ + int i; + Msg **r, **w, **e; + + if(b == nil) + return 0; + + for(i=0; i<b->nmsg; i++) + b->msg[i]->imapid = 0; + if(imapcmd(z, b, "UID FETCH 1:* FLAGS") < 0) + return -1; + for(r=b->msg, w=b->msg, e=b->msg+b->nmsg; r<e; r++){ + if((*r)->imapid == 0) + msgfree(*r); + else{ + (*r)->ix = w-b->msg; + *w++ = *r; + } + } + b->nmsg = w - b->msg; + b->imapinit = 1; + checkbox(z, b); + return 0; +} + +static void +freeup(UserPasswd *up) +{ + memset(up->user, 0, strlen(up->user)); + memset(up->passwd, 0, strlen(up->passwd)); + free(up); +} + +static void +imaptimerproc(void *v) +{ + Imap *z; + + z = v; + for(;;){ + sleep(60*1000); + qlock(z->r.l); + rwakeup(&z->r); + qunlock(z->r.l); + } +} + +static void +checkbox(Imap *z, Box *b) +{ + if(imapcmd(z, b, "NOOP") >= 0){ + if(!b->imapinit) + getbox(z, b); + if(!b->imapinit) + return; + if(b==z->box && b->exists > b->maxseen){ + imapcmd(z, b, "UID FETCH %d:* FULL", + b->uidnext); + } + } +} + +static void +imaprefreshthread(void *v) +{ + Imap *z; + + z = v; + for(;;){ + qlock(z->r.l); + rsleep(&z->r); + qunlock(z->r.l); + + qlock(&z->lk); + if(z->inbox) + checkbox(z, z->inbox); + qunlock(&z->lk); + } +} + +/* + * Run a single command and return the Sx. Does NOT redial. + */ +static Sx* +imapvcmdsx0(Imap *z, char *fmt, va_list arg) +{ + char *s; + Fmt f; + int prefix, len; + Sx *sx; + + if(canqlock(&z->lk)) + abort(); + + if(z->fd < 0 || !z->connected) + return nil; + + prefix = strlen(tag)+1; + fmtstrinit(&f); + fmtprint(&f, "%s ", tag); + fmtvprint(&f, fmt, arg); + fmtprint(&f, "\r\n"); + s = fmtstrflush(&f); + len = strlen(s); + s[len-2] = 0; + if(chattyimap) + fprint(2, "I> %s\n", s); + s[len-2] = '\r'; + if(iowrite(z->io, z->fd, s, len) < 0){ + z->connected = 0; + free(s); + return nil; + } + sx = imapwaitsx(z); + free(s); + return sx; +} + +static Sx* +imapcmdsx0(Imap *z, char *fmt, ...) +{ + va_list arg; + Sx *sx; + + va_start(arg, fmt); + sx = imapvcmdsx0(z, fmt, arg); + va_end(arg); + return sx; +} + +/* + * Run a single command on box b. Does redial. + */ +static Sx* +imapvcmdsx(Imap *z, Box *b, char *fmt, va_list arg) +{ + int tries; + Sx *sx; + + tries = 0; + z->nextbox = b; + + if(z->fd < 0 || !z->connected){ +reconnect: + if(!z->autoreconnect) + return nil; + if(imapreconnect(z) < 0) + return nil; + if(b && z->nextbox == nil) /* box disappeared on reconnect */ + return nil; + } + + if(b && b != z->box){ + if(z->box) + z->box->imapinit = 0; + z->box = b; + if((sx=imapcmdsx0(z, "SELECT %Z", b->imapname)) == nil){ + z->box = nil; + if(tries++ == 0 && (z->fd < 0 || !z->connected)) + goto reconnect; + return nil; + } + freesx(sx); + } + + if((sx=imapvcmdsx0(z, fmt, arg)) == nil){ + if(tries++ == 0 && (z->fd < 0 || !z->connected)) + goto reconnect; + return nil; + } + return sx; +} + +static int +imapcmd(Imap *z, Box *b, char *fmt, ...) +{ + Sx *sx; + va_list arg; + + va_start(arg, fmt); + sx = imapvcmdsx(z, b, fmt, arg); + va_end(arg); + if(sx == nil) + return -1; + if(sx->nsx < 2 || !isatom(sx->sx[1], "OK")){ + werrstr("%$", sx); + freesx(sx); + return -1; + } + freesx(sx); + return 0; +} + +static Sx* +imapcmdsx(Imap *z, Box *b, char *fmt, ...) +{ + Sx *sx; + va_list arg; + + va_start(arg, fmt); + sx = imapvcmdsx(z, b, fmt, arg); + va_end(arg); + return sx; +} + +static Sx* +imapwaitsx(Imap *z) +{ + Sx *sx; + + while((sx = zBrdsx(z)) != nil){ + if(chattyimap) + fprint(2, "<| %#$\n", sx); + if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && cistrcmp(sx->sx[0]->data, tag) == 0) + return sx; + if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && strcmp(sx->sx[0]->data, "*") == 0) + unexpected(z, sx); + if(sx->type == SxList && sx->nsx == 0){ + freesx(sx); + break; + } + freesx(sx); + } + z->connected = 0; + return nil; +} + +/* + * Imap interface to mail file system. + */ + +static void +_bodyname(char *buf, char *ebuf, Part *p, char *extra) +{ + if(buf >= ebuf){ + fprint(2, "***** BUFFER TOO SMALL\n"); + return; + } + *buf = 0; + if(p->parent){ + _bodyname(buf, ebuf, p->parent, ""); + buf += strlen(buf); + seprint(buf, ebuf, ".%d", p->pix+1); + } + buf += strlen(buf); + seprint(buf, ebuf, "%s", extra); +} + +static char* +bodyname(Part *p, char *extra) +{ + static char buf[256]; + memset(buf, 0, sizeof buf); /* can't see why this is necessary, but it is */ + _bodyname(buf, buf+sizeof buf, p, extra); + return buf+1; /* buf[0] == '.' */ +} + +static void +fetch1(Imap *z, Part *p, char *s) +{ + qlock(&z->lk); + imapcmd(z, p->msg->box, "UID FETCH %d BODY[%s]", + p->msg->imapuid, bodyname(p, s)); + qunlock(&z->lk); +} + +void +imapfetchrawheader(Imap *z, Part *p) +{ + fetch1(z, p, ".HEADER"); +} + +void +imapfetchrawmime(Imap *z, Part *p) +{ + fetch1(z, p, ".MIME"); +} + +void +imapfetchrawbody(Imap *z, Part *p) +{ + fetch1(z, p, ".TEXT"); +} + +void +imapfetchraw(Imap *z, Part *p) +{ + fetch1(z, p, ""); +} + +static int +imaplistcmd(Imap *z, Box *box, char *before, Msg **m, uint nm, char *after) +{ + int i, r; + char *cmd; + Fmt fmt; + + if(nm == 0) + return 0; + + fmtstrinit(&fmt); + fmtprint(&fmt, "%s ", before); + for(i=0; i<nm; i++){ + if(i > 0) + fmtrune(&fmt, ','); + fmtprint(&fmt, "%ud", m[i]->imapuid); + } + fmtprint(&fmt, " %s", after); + cmd = fmtstrflush(&fmt); + + r = 0; + if(imapcmd(z, box, "%s", cmd) < 0) + r = -1; + free(cmd); + return r; +} + +int +imapcopylist(Imap *z, char *nbox, Msg **m, uint nm) +{ + int rv; + char *name; + + if(nm == 0) + return 0; + + qlock(&z->lk); + name = esmprint("%Z", nbox); + rv = imaplistcmd(z, m[0]->box, "UID COPY", m, nm, name); + free(name); + qunlock(&z->lk); + return rv; +} + +int +imapremovelist(Imap *z, Msg **m, uint nm) +{ + int rv; + + if(nm == 0) + return 0; + + qlock(&z->lk); + rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, "+FLAGS.SILENT (\\Deleted)"); + /* careful - box might be gone; use z->box instead */ + if(rv == 0 && z->box) + rv = imapcmd(z, z->box, "EXPUNGE"); + qunlock(&z->lk); + return rv; +} + +int +imapflaglist(Imap *z, int op, int flag, Msg **m, uint nm) +{ + char *mod, *s, *sep; + int i, rv; + Fmt fmt; + + if(op > 0) + mod = "+"; + else if(op == 0) + mod = ""; + else + mod = "-"; + + fmtstrinit(&fmt); + fmtprint(&fmt, "%sFLAGS (", mod); + sep = ""; + for(i=0; i<nelem(flagstab); i++){ + if(flagstab[i].flag & flag){ + fmtprint(&fmt, "%s%s", sep, flagstab[i].name); + sep = " "; + } + } + fmtprint(&fmt, ")"); + s = fmtstrflush(&fmt); + + qlock(&z->lk); + rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, s); + qunlock(&z->lk); + free(s); + return rv; +} + +int +imapsearchbox(Imap *z, Box *b, char *search, Msg ***mm) +{ + uint *uid; + int i, nuid; + Msg **m; + int nm; + + qlock(&z->lk); + if(imapcmd(z, b, "UID SEARCH CHARSET UTF-8 TEXT %Z", search) < 0){ + qunlock(&z->lk); + return -1; + } + + uid = z->uid; + nuid = z->nuid; + z->uid = nil; + z->nuid = 0; + qunlock(&z->lk); + + m = emalloc(nuid*sizeof m[0]); + nm = 0; + for(i=0; i<nuid; i++) + if((m[nm] = msgbyimapuid(b, uid[i], 0)) != nil) + nm++; + *mm = m; + free(uid); + return nm; +} + +void +imapcheckbox(Imap *z, Box *b) +{ + if(b == nil) + return; + qlock(&z->lk); + checkbox(z, b); + qunlock(&z->lk); +} + +/* + * Imap utility routines + */ +static long +_ioimapdial(va_list *arg) +{ + char *server; + int mode; + + server = va_arg(*arg, char*); + mode = va_arg(*arg, int); + return imapdial(server, mode); +} +static int +ioimapdial(Ioproc *io, char *server, int mode) +{ + return iocall(io, _ioimapdial, server, mode); +} + +static long +_ioBrdsx(va_list *arg) +{ + Biobuf *b; + Sx **sx; + + b = va_arg(*arg, Biobuf*); + sx = va_arg(*arg, Sx**); + *sx = Brdsx(b); + if((*sx) && (*sx)->type == SxList && (*sx)->nsx == 0){ + freesx(*sx); + *sx = nil; + } + return 0; +} +static Sx* +ioBrdsx(Ioproc *io, Biobuf *b) +{ + Sx *sx; + + iocall(io, _ioBrdsx, b, &sx); + return sx; +} + +static Sx* +zBrdsx(Imap *z) +{ + if(z->ticks && --z->ticks==0){ + close(z->fd); + z->fd = -1; + return nil; + } + return ioBrdsx(z->io, &z->b); +} + +static int +imapdial(char *server, int mode) +{ + int p[2]; + int fd[3]; + char *tmp; + + switch(mode){ + default: + case Unencrypted: + return dial(netmkaddr(server, "tcp", "143"), nil, nil, nil); + + case Starttls: + werrstr("starttls not supported"); + return -1; + + case Tls: + if(pipe(p) < 0) + return -1; + fd[0] = dup(p[0], -1); + fd[1] = dup(p[0], -1); + fd[2] = dup(2, -1); + tmp = esmprint("%s:993", server); + if(threadspawnl(fd, "/usr/sbin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0){ + free(tmp); + close(p[0]); + close(p[1]); + close(fd[0]); + close(fd[1]); + close(fd[2]); + return -1; + } + free(tmp); + close(p[0]); + return p[1]; + + case Cmd: + if(pipe(p) < 0) + return -1; + fd[0] = dup(p[0], -1); + fd[1] = dup(p[0], -1); + fd[2] = dup(2, -1); + if(threadspawnl(fd, "/usr/local/plan9/bin/rc", "rc", "-c", server, nil) < 0){ + close(p[0]); + close(p[1]); + close(fd[0]); + close(fd[1]); + close(fd[2]); + return -1; + } + close(p[0]); + return p[1]; + } +} + +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; +} + +static int +imapquote(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; + if(f->flags&FmtSharp) + quotes = 1; + 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, '"'); +} + +static int +fmttype(char c) +{ + switch(c){ + case 'A': + return SxAtom; + case 'L': + return SxList; + case 'N': + return SxNumber; + case 'S': + return SxString; + default: + return -1; + } +} + +/* + * Check S expression against format string. + */ +static int +sxmatch(Sx *sx, char *fmt) +{ + int i; + + for(i=0; fmt[i]; i++){ + if(fmt[i] == '*') + fmt--; /* like i-- but better */ + if(i == sx->nsx && fmt[i+1] == '*') + return 1; + if(i >= sx->nsx) + return 0; + if(sx->sx[i] == nil) + return 0; + if(sx->sx[i]->type == SxAtom && strcmp(sx->sx[i]->data, "NIL") == 0){ + if(fmt[i] == 'L'){ + free(sx->sx[i]->data); + sx->sx[i]->data = nil; + sx->sx[i]->type = SxList; + sx->sx[i]->sx = nil; + sx->sx[i]->nsx = 0; + } + else if(fmt[i] == 'S'){ + free(sx->sx[i]->data); + sx->sx[i]->data = nil; + sx->sx[i]->type = SxString; + } + } + if(sx->sx[i]->type == SxAtom && fmt[i]=='S') + sx->sx[i]->type = SxString; + if(sx->sx[i]->type != fmttype(fmt[i])){ + fprint(2, "sxmatch: %$ not %c\n", sx->sx[i], fmt[i]); + return 0; + } + } + if(i != sx->nsx) + return 0; + return 1; +} + +/* + * Check string against format string. + */ +static int +stringmatch(char *fmt, char *s) +{ + for(; *fmt && *s; fmt++, s++){ + switch(*fmt){ + case '0': + if(*s == ' ') + break; + /* fall through */ + case '1': + if(*s < '0' || *s > '9') + return 0; + break; + case 'A': + if(*s < 'A' || *s > 'Z') + return 0; + break; + case 'a': + if(*s < 'a' || *s > 'z') + return 0; + break; + case '+': + if(*s != '-' && *s != '+') + return 0; + break; + default: + if(*s != *fmt) + return 0; + break; + } + } + if(*fmt || *s) + return 0; + return 1; +} + +/* + * Parse simple S expressions and IMAP elements. + */ +static int +isatom(Sx *v, char *name) +{ + int n; + + if(v == nil || v->type != SxAtom) + return 0; + n = strlen(name); + if(cistrncmp(v->data, name, n) == 0) + if(v->data[n] == 0 || (n>0 && v->data[n-1] == '[')) + return 1; + return 0; +} + +static int +isstring(Sx *sx) +{ + if(sx->type == SxAtom) + sx->type = SxString; + return sx->type == SxString; +} + +static int +isnumber(Sx *sx) +{ + return sx->type == SxNumber; +} + +static int +isnil(Sx *v) +{ + return v == nil || + (v->type==SxList && v->nsx == 0) || + (v->type==SxAtom && strcmp(v->data, "NIL") == 0); +} + +static int +islist(Sx *v) +{ + return isnil(v) || v->type==SxList; +} + +static uint +parseflags(Sx *v) +{ + int f, i, j; + + if(v->type != SxList){ + warn("malformed flags: %$", v); + return 0; + } + f = 0; + for(i=0; i<v->nsx; i++){ + if(v->sx[i]->type != SxAtom) + continue; + for(j=0; j<nelem(flagstab); j++) + if(cistrcmp(v->sx[i]->data, flagstab[j].name) == 0) + f |= flagstab[j].flag; + } + return f; +} + +static char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; +static int +parsemon(char *s) +{ + int i; + + for(i=0; months[i]; i+=3) + if(memcmp(s, months+i, 3) == 0) + return i/3; + return -1; +} + +static uint +parsedate(Sx *v) +{ + Tm tm; + uint t; + int delta; + char *p; + + if(v->type != SxString || !stringmatch("01-Aaa-1111 01:11:11 +1111", v->data)){ + bad: + warn("bad date: %$", v); + return 0; + } + memset(&tm, 0, sizeof tm); + p = v->data; + tm.mday = atoi(p); + tm.mon = parsemon(p+3); + if(tm.mon == -1) + goto bad; + tm.year = atoi(p+7) - 1900; + tm.hour = atoi(p+12); + tm.min = atoi(p+15); + tm.sec = atoi(p+18); + strcpy(tm.zone, "GMT"); + + t = tm2sec(&tm); + delta = ((p[22]-'0')*10+p[23]-'0')*3600 + ((p[24]-'0')*10+p[25]-'0')*60; + if(p[21] == '-') + delta = -delta; + + t -= delta; + return t; +} + +static uint +parsenumber(Sx *v) +{ + if(v->type != SxNumber) + return 0; + return v->number; +} + +static void +hash(DigestState *ds, char *tag, char *val) +{ + if(val == nil) + val = ""; + md5((uchar*)tag, strlen(tag)+1, nil, ds); + md5((uchar*)val, strlen(val)+1, nil, ds); +} + +static Hdr* +parseenvelope(Sx *v) +{ + Hdr *hdr; + uchar digest[16]; + DigestState ds; + + if(v->type != SxList || !sxmatch(v, "SSLLLLLLSS")){ + warn("bad envelope: %$", v); + return nil; + } + + hdr = emalloc(sizeof *hdr); + hdr->date = nstring(v->sx[0]); + hdr->subject = unrfc2047(nstring(v->sx[1])); + hdr->from = copyaddrs(v->sx[2]); + hdr->sender = copyaddrs(v->sx[3]); + hdr->replyto = copyaddrs(v->sx[4]); + hdr->to = copyaddrs(v->sx[5]); + hdr->cc = copyaddrs(v->sx[6]); + hdr->bcc = copyaddrs(v->sx[7]); + hdr->inreplyto = unrfc2047(nstring(v->sx[8])); + hdr->messageid = unrfc2047(nstring(v->sx[9])); + + memset(&ds, 0, sizeof ds); + hash(&ds, "date", hdr->date); + hash(&ds, "subject", hdr->subject); + hash(&ds, "from", hdr->from); + hash(&ds, "sender", hdr->sender); + hash(&ds, "replyto", hdr->replyto); + hash(&ds, "to", hdr->to); + hash(&ds, "cc", hdr->cc); + hash(&ds, "bcc", hdr->bcc); + hash(&ds, "inreplyto", hdr->inreplyto); + hash(&ds, "messageid", hdr->messageid); + md5(0, 0, digest, &ds); + hdr->digest = esmprint("%.16H", digest); + + return hdr; +} + +static void +strlwr(char *s) +{ + char *t; + + if(s == nil) + return; + for(t=s; *t; t++) + if('A' <= *t && *t <= 'Z') + *t += 'a' - 'A'; +} + +static void +nocr(char *s) +{ + char *r, *w; + + if(s == nil) + return; + for(r=w=s; *r; r++) + if(*r != '\r') + *w++ = *r; + *w = 0; +} + +/* + * substitute all occurrences of a with b in s. + */ +static char* +gsub(char *s, char *a, char *b) +{ + char *p, *t, *w, *last; + int n; + + n = 0; + for(p=s; (p=strstr(p, a)) != nil; p+=strlen(a)) + n++; + if(n == 0) + return s; + t = emalloc(strlen(s)+n*strlen(b)+1); + w = t; + for(p=s; last=p, (p=strstr(p, a)) != nil; p+=strlen(a)){ + memmove(w, last, p-last); + w += p-last; + memmove(w, b, strlen(b)); + w += strlen(b); + } + strcpy(w, last); + free(s); + return t; +} + +/* + * Table-driven IMAP "unexpected response" parser. + * All the interesting data is in the unexpected responses. + */ +static void xlist(Imap*, Sx*); +static void xrecent(Imap*, Sx*); +static void xexists(Imap*, Sx*); +static void xok(Imap*, Sx*); +static void xflags(Imap*, Sx*); +static void xfetch(Imap*, Sx*); +static void xexpunge(Imap*, Sx*); +static void xbye(Imap*, Sx*); +static void xsearch(Imap*, Sx*); + +static struct { + int num; + char *name; + char *fmt; + void (*fn)(Imap*, Sx*); +} unextab[] = { + 0, "BYE", nil, xbye, + 0, "FLAGS", "AAL", xflags, + 0, "LIST", "AALSS", xlist, + 0, "OK", nil, xok, + 0, "SEARCH", "AAN*", xsearch, + + 1, "EXISTS", "ANA", xexists, + 1, "EXPUNGE", "ANA", xexpunge, + 1, "FETCH", "ANAL", xfetch, + 1, "RECENT", "ANA", xrecent, +}; + +static void +unexpected(Imap *z, Sx *sx) +{ + int i, num; + char *name; + + if(sx->nsx >= 3 && sx->sx[1]->type == SxNumber && sx->sx[2]->type == SxAtom){ + num = 1; + name = sx->sx[2]->data; + }else if(sx->nsx >= 2 && sx->sx[1]->type == SxAtom){ + num = 0; + name = sx->sx[1]->data; + }else + return; + + for(i=0; i<nelem(unextab); i++){ + if(unextab[i].num == num && cistrcmp(unextab[i].name, name) == 0){ + if(unextab[i].fmt && !sxmatch(sx, unextab[i].fmt)){ + warn("malformed %s: %$", name, sx); + continue; + } + unextab[i].fn(z, sx); + } + } +} + + +static void +xlist(Imap *z, Sx *sx) +{ + int inbox; + char *s, *t; + Box *box; + + if(sx->sx[3]->data && strcmp(sx->sx[3]->data, "/") != 0) + warn("box separator %q not / - need to implement translation", sx->sx[3]->data); + s = estrdup(sx->sx[4]->data); + if(sx->sx[3]->data && strcmp(sx->sx[3]->data, "/") != 0){ + s = gsub(s, "/", "_"); + s = gsub(s, sx->sx[3]->data, "/"); + } + + /* + * Plan 9 calls the main mailbox mbox. + * Rename any existing mbox by appending a $. + */ + inbox = 0; + if(strncmp(s, "mbox", 4) == 0){ + t = emalloc(strlen(s)+2); + strcpy(t, s); + strcat(t, "$"); + free(s); + s = t; + }else if(cistrcmp(s, "INBOX") == 0){ + inbox = 1; + free(s); + s = estrdup("mbox"); + } + + box = boxcreate(s); + if(box == nil) + return; + box->imapname = estrdup(sx->sx[4]->data); + if(inbox) + z->inbox = box; + box->mark = 0; + box->flags = parseflags(sx->sx[2]); +} + +static void +xrecent(Imap *z, Sx *sx) +{ + if(z->box) + z->box->recent = sx->sx[1]->number; +} + +static void +xexists(Imap *z, Sx *sx) +{ + if(z->box){ + z->box->exists = sx->sx[1]->number; + if(z->box->exists < z->box->maxseen) + z->box->maxseen = z->box->exists; + } +} + +static void +xflags(Imap *z, Sx *sx) +{ + if(z->box) + z->box->flags = parseflags(sx->sx[2]); +} + +static void +xbye(Imap *z, Sx *sx) +{ + close(z->fd); + z->fd = -1; + z->connected = 0; +} + +static void +xexpunge(Imap *z, Sx *sx) +{ + int i, n; + Box *b; + + if((b=z->box) == nil) + return; + n = sx->sx[1]->number; + for(i=0; i<b->nmsg; i++){ + if(b->msg[i]->imapid == n){ + msgplumb(b->msg[i], 1); + msgfree(b->msg[i]); + b->nmsg--; + memmove(b->msg+i, b->msg+i+1, (b->nmsg-i)*sizeof b->msg[0]); + i--; + b->maxseen--; + b->exists--; + continue; + } + if(b->msg[i]->imapid > n) + b->msg[i]->imapid--; + b->msg[i]->ix = i; + } +} + +static void +xsearch(Imap *z, Sx *sx) +{ + int i; + + free(z->uid); + z->uid = emalloc((sx->nsx-2)*sizeof z->uid[0]); + z->nuid = sx->nsx-2; + for(i=0; i<z->nuid; i++) + z->uid[i] = sx->sx[i+2]->number; +} + +/* + * Table-driven FETCH message info parser. + */ +static void xmsgflags(Msg*, Sx*, Sx*); +static void xmsgdate(Msg*, Sx*, Sx*); +static void xmsgrfc822size(Msg*, Sx*, Sx*); +static void xmsgenvelope(Msg*, Sx*, Sx*); +static void xmsgbody(Msg*, Sx*, Sx*); +static void xmsgbodydata(Msg*, Sx*, Sx*); + +static struct { + char *name; + void (*fn)(Msg*, Sx*, Sx*); +} msgtab[] = { + "FLAGS", xmsgflags, + "INTERNALDATE", xmsgdate, + "RFC822.SIZE", xmsgrfc822size, + "ENVELOPE", xmsgenvelope, + "BODY", xmsgbody, + "BODY[", xmsgbodydata, +}; + +static void +xfetch(Imap *z, Sx *sx) +{ + int i, j, n, uid; + Msg *msg; + + if(z->box == nil){ + warn("FETCH but no open box: %$", sx); + return; + } + + /* * 152 FETCH (UID 185 FLAGS () ...) */ + if(sx->sx[3]->nsx%2){ + warn("malformed FETCH: %$", sx); + return; + } + + n = sx->sx[1]->number; + sx = sx->sx[3]; + for(i=0; i<sx->nsx; i+=2){ + if(isatom(sx->sx[i], "UID")){ + if(sx->sx[i+1]->type == SxNumber){ + uid = sx->sx[i+1]->number; + goto haveuid; + } + } + } + warn("FETCH without UID: %$", sx); + return; + +haveuid: + msg = msgbyimapuid(z->box, uid, 1); + if(msg->imapid && msg->imapid != n) + warn("msg id mismatch: want %d have %d", msg->id, n); + msg->imapid = n; + for(i=0; i<sx->nsx; i+=2){ + for(j=0; j<nelem(msgtab); j++) + if(isatom(sx->sx[i], msgtab[j].name)) + msgtab[j].fn(msg, sx->sx[i], sx->sx[i+1]); + } +} + +static void +xmsgflags(Msg *msg, Sx *k, Sx *v) +{ + USED(k); + msg->flags = parseflags(v); +} + +static void +xmsgdate(Msg *msg, Sx *k, Sx *v) +{ + USED(k); + msg->date = parsedate(v); +} + +static void +xmsgrfc822size(Msg *msg, Sx *k, Sx *v) +{ + USED(k); + msg->size = parsenumber(v); +} + +static char* +nstring(Sx *v) +{ + char *p; + + p = v->data; + v->data = nil; + return p; +} + +static char* +copyaddrs(Sx *v) +{ + char *s, *sep; + char *name, *email, *host, *mbox; + int i; + Fmt fmt; + + if(v->nsx == 0) + return nil; + + fmtstrinit(&fmt); + sep = ""; + for(i=0; i<v->nsx; i++){ + if(!sxmatch(v->sx[i], "SSSS")) + warn("bad address: %$", v->sx[i]); + name = unrfc2047(nstring(v->sx[i]->sx[0])); + /* ignore sx[1] - route */ + mbox = unrfc2047(nstring(v->sx[i]->sx[2])); + host = unrfc2047(nstring(v->sx[i]->sx[3])); + if(mbox == nil || host == nil){ /* rfc822 group syntax */ + free(name); + free(mbox); + free(host); + continue; + } + email = esmprint("%s@%s", mbox, host); + free(mbox); + free(host); + fmtprint(&fmt, "%s%q %q", sep, name ? name : "", email ? email : ""); + free(name); + free(email); + sep = " "; + } + s = fmtstrflush(&fmt); + if(s == nil) + sysfatal("out of memory"); + return s; +} + +static void +xmsgenvelope(Msg *msg, Sx *k, Sx *v) +{ + hdrfree(msg->part[0]->hdr); + msg->part[0]->hdr = parseenvelope(v); +} + +static struct { + char *name; + int offset; +} paramtab[] = { + "charset", offsetof(Part, charset), +}; + +static void +parseparams(Part *part, Sx *v) +{ + int i, j; + char *s, *t, **p; + + if(isnil(v)) + return; + if(v->nsx%2){ + warn("bad message params: %$", v); + return; + } + for(i=0; i<v->nsx; i+=2){ + s = nstring(v->sx[i]); + t = nstring(v->sx[i+1]); + for(j=0; j<nelem(paramtab); j++){ + if(cistrcmp(paramtab[j].name, s) == 0){ + p = (char**)((char*)part+paramtab[j].offset); + free(*p); + *p = t; + t = nil; + break; + } + } + free(s); + free(t); + } +} + +static void +parsestructure(Part *part, Sx *v) +{ + int i; + char *s, *t; + + if(isnil(v)) + return; + if(v->type != SxList){ + bad: + warn("bad structure: %$", v); + return; + } + if(islist(v->sx[0])){ + /* multipart */ + for(i=0; i<v->nsx && islist(v->sx[i]); i++) + parsestructure(partcreate(part->msg, part), v->sx[i]); + free(part->type); + if(i != v->nsx-1 || !isstring(v->sx[i])){ + warn("bad multipart structure: %$", v); + part->type = estrdup("multipart/mixed"); + return; + } + s = nstring(v->sx[i]); + strlwr(s); + part->type = esmprint("multipart/%s", s); + free(s); + return; + } + /* single part */ + if(!isstring(v->sx[0]) || v->nsx < 2) + goto bad; + s = nstring(v->sx[0]); + t = nstring(v->sx[1]); + strlwr(s); + strlwr(t); + free(part->type); + part->type = esmprint("%s/%s", s, t); + if(v->nsx < 7 || !islist(v->sx[2]) || !isstring(v->sx[3]) + || !isstring(v->sx[4]) || !isstring(v->sx[5]) || !isnumber(v->sx[6])) + goto bad; + parseparams(part, v->sx[2]); + part->idstr = nstring(v->sx[3]); + part->desc = nstring(v->sx[4]); + part->encoding = nstring(v->sx[5]); + part->size = v->sx[6]->number; + if(strcmp(s, "message") == 0 && strcmp(t, "rfc822") == 0){ + if(v->nsx < 10 || !islist(v->sx[7]) || !islist(v->sx[8]) || !isnumber(v->sx[9])) + goto bad; + part->hdr = parseenvelope(v->sx[7]); + parsestructure(partcreate(part->msg, part), v->sx[8]); + part->lines = v->sx[9]->number; + } + if(strcmp(s, "text") == 0){ + if(v->nsx < 8 || !isnumber(v->sx[7])) + goto bad; + part->lines = v->sx[7]->number; + } +} + +static void +xmsgbody(Msg *msg, Sx *k, Sx *v) +{ + if(v->type != SxList){ + warn("bad body: %$", v); + return; + } + /* + * To follow the structure exactly we should + * be doing this to partcreate(msg, msg->part[0]), + * and we should leave msg->part[0] with type message/rfc822, + * but the extra layer is redundant - what else would be in a mailbox? + */ + parsestructure(msg->part[0], v); + if(msg->box->maxseen < msg->imapid) + msg->box->maxseen = msg->imapid; + if(msg->imapuid >= msg->box->uidnext) + msg->box->uidnext = msg->imapuid+1; + msgplumb(msg, 0); +} + +static void +xmsgbodydata(Msg *msg, Sx *k, Sx *v) +{ + int i; + char *name, *p; + Part *part; + + name = k->data; + name += 5; /* body[ */ + p = strchr(name, ']'); + if(p) + *p = 0; + + /* now name is something like 1 or 3.2.MIME - walk down parts from root */ + part = msg->part[0]; + + while('1' <= name[0] && name[0] <= '9'){ + i = strtol(name, &p, 10); + if(*p == '.') + p++; + else if(*p != 0){ + warn("bad body name: %$", k); + return; + } + if((part = subpart(part, i-1)) == nil){ + warn("unknown body part: %$", k); + return; + } + name = p; + } + + if(cistrcmp(name, "") == 0){ + free(part->raw); + part->raw = nstring(v); + nocr(part->raw); + }else if(cistrcmp(name, "HEADER") == 0){ + free(part->rawheader); + part->rawheader = nstring(v); + nocr(part->rawheader); + }else if(cistrcmp(name, "MIME") == 0){ + free(part->mimeheader); + part->mimeheader = nstring(v); + nocr(part->mimeheader); + }else if(cistrcmp(name, "TEXT") == 0){ + free(part->rawbody); + part->rawbody = nstring(v); + nocr(part->rawbody); + } +} + +/* + * Table-driven OK info parser. + */ +static void xokuidvalidity(Imap*, Sx*); +static void xokpermflags(Imap*, Sx*); +static void xokunseen(Imap*, Sx*); +static void xokreadwrite(Imap*, Sx*); +static void xokreadonly(Imap*, Sx*); + +struct { + char *name; + char fmt; + void (*fn)(Imap*, Sx*); +} oktab[] = { + "UIDVALIDITY", 'N', xokuidvalidity, + "PERMANENTFLAGS", 'L', xokpermflags, + "UNSEEN", 'N', xokunseen, + "READ-WRITE", 0, xokreadwrite, + "READ-ONLY", 0, xokreadonly, +}; + +static void +xok(Imap *z, Sx *sx) +{ + int i; + char *name; + Sx *arg; + + if(sx->nsx >= 4 && sx->sx[2]->type == SxAtom && sx->sx[2]->data[0] == '['){ + if(sx->sx[3]->type == SxAtom && sx->sx[3]->data[0] == ']') + arg = nil; + else if(sx->sx[4]->type == SxAtom && sx->sx[4]->data[0] == ']') + arg = sx->sx[3]; + else{ + warn("cannot parse OK: %$", sx); + return; + } + name = sx->sx[2]->data+1; + for(i=0; i<nelem(oktab); i++){ + if(cistrcmp(name, oktab[i].name) == 0){ + if(oktab[i].fmt && (arg==nil || arg->type != fmttype(oktab[i].fmt))){ + warn("malformed %s: %$", name, arg); + continue; + } + oktab[i].fn(z, arg); + } + } + } +} + +static void +xokuidvalidity(Imap *z, Sx *sx) +{ + int i; + Box *b; + + if((b=z->box) == nil) + return; + if(b->validity != sx->number){ + b->validity = sx->number; + b->uidnext = 1; + for(i=0; i<b->nmsg; i++) + msgfree(b->msg[i]); + free(b->msg); + b->msg = nil; + b->nmsg = 0; + } +} + +static void +xokpermflags(Imap *z, Sx *sx) +{ +// z->permflags = parseflags(sx); +} + +static void +xokunseen(Imap *z, Sx *sx) +{ +// z->unseen = sx->number; +} + +static void +xokreadwrite(Imap *z, Sx *sx) +{ +// z->boxmode = ORDWR; +} + +static void +xokreadonly(Imap *z, Sx *sx) +{ +// z->boxmode = OREAD; +} + |