blob: c2aed9a554884bd2f1e0b830886347227273a7bb [file] [log] [blame]
/*
* 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;
char* root;
char* user;
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,
"\\Recent", FlagRecent,
"\\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, char *root, char *user)
{
Imap *z;
fmtinstall('H', encodefmt);
fmtinstall('Z', imapquote);
z = emalloc(sizeof *z);
z->server = estrdup(server);
z->mode = mode;
z->user = user;
if(root)
if(root[0] != 0 && root[strlen(root)-1] != '/')
z->root = smprint("%s/", root);
else
z->root = root;
else
z->root = "";
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(z->user != nil)
up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q user=%q", z->server, z->user);
else
up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", z->server);
if(up == 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 *", z->root) < 0)
return -1;
if(z->root != nil && imapcmd(z, nil, "LIST %Z INBOX", "") < 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, *p;
if(nm == 0)
return 0;
qlock(&z->lk);
if(strcmp(nbox, "mbox") == 0)
name = estrdup("INBOX");
else{
p = esmprint("%s%s", z->root, nbox);
name = esmprint("%Z", p);
free(p);
}
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);
#ifdef PLAN9PORT
tmp = esmprint("%s:993", server);
if(threadspawnl(fd, "/usr/sbin/stunnel3", "stunnel3", "-c", "-r", tmp, nil) < 0
&& threadspawnl(fd, "/usr/bin/stunnel3", "stunnel3", "-c", "-r", tmp, nil) < 0
&& threadspawnl(fd, "/usr/sbin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0
&& threadspawnl(fd, "/usr/bin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0){
#else
tmp = esmprint("tcp!%s!993", server);
if(threadspawnl(fd, "/bin/tlsclient", "tlsclient", tmp, nil) < 0){
#endif
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;
}
/* cannot use atoi because 09 is malformed octal! */
memset(&tm, 0, sizeof tm);
p = v->data;
tm.mday = strtol(p, 0, 10);
tm.mon = parsemon(p+3);
if(tm.mon == -1)
goto bad;
tm.year = strtol(p+7, 0, 10) - 1900;
tm.hour = strtol(p+12, 0, 10);
tm.min = strtol(p+15, 0, 10);
tm.sec = strtol(p+18, 0, 10);
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 int
alldollars(char *s)
{
for(; *s; s++)
if(*s != '$')
return 0;
return 1;
}
static void
xlist(Imap *z, Sx *sx)
{
int inbox;
char *s, *t;
Box *box;
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, "/");
}
/*
* INBOX is the special imap name for the main mailbox.
* All other mailbox names have the root prefix removed, if applicable.
*/
inbox = 0;
if(cistrcmp(s, "INBOX") == 0){
inbox = 1;
free(s);
s = estrdup("mbox");
} else if(z->root && strstr(s, z->root) == s) {
t = estrdup(s+strlen(z->root));
free(s);
s = t;
}
/*
* Plan 9 calls the main mailbox mbox.
* Rename any existing mbox by appending a $.
*/
if(!inbox && strncmp(s, "mbox", 4) == 0 && alldollars(s+4)){
t = emalloc(strlen(s)+2);
strcpy(t, s);
strcat(t, "$");
free(s);
s = t;
}
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)
{
USED(z);
USED(sx);
/*
* This response contains in sx->sx[2] the list of flags
* that can be validly attached to messages in z->box.
* We don't have any use for this list, since we
* use only the standard flags.
*/
}
static void
xbye(Imap *z, Sx *sx)
{
USED(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;
}
}
}
/* This happens: too bad.
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;
if(isnil(v))
return estrdup("");
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)
{
USED(k);
hdrfree(msg->part[0]->hdr);
msg->part[0]->hdr = parseenvelope(v);
msgplumb(msg, 0);
}
static struct {
char *name;
int offset;
} paramtab[] = {
"charset", offsetof(Part, charset),
"name", offsetof(Part, filename)
};
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)
{
USED(k);
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;
}
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)
{
USED(z);
USED(sx);
/* z->permflags = parseflags(sx); */
}
static void
xokunseen(Imap *z, Sx *sx)
{
USED(z);
USED(sx);
/* z->unseen = sx->number; */
}
static void
xokreadwrite(Imap *z, Sx *sx)
{
USED(z);
USED(sx);
/* z->boxmode = ORDWR; */
}
static void
xokreadonly(Imap *z, Sx *sx)
{
USED(z);
USED(sx);
/* z->boxmode = OREAD; */
}