plan9fox/sys/src/cmd/ip/imap4d/mbox.c
2011-03-30 19:35:09 +03:00

864 lines
17 KiB
C

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <auth.h>
#include "imap4d.h"
static NamedInt flagChars[NFlags] =
{
{"s", MSeen},
{"a", MAnswered},
{"f", MFlagged},
{"D", MDeleted},
{"d", MDraft},
{"r", MRecent},
};
static int fsCtl = -1;
static void boxFlags(Box *box);
static int createImp(Box *box, Qid *qid);
static void fsInit(void);
static void mboxGone(Box *box);
static MbLock *openImp(Box *box, int new);
static int parseImp(Biobuf *b, Box *box);
static int readBox(Box *box);
static ulong uidRenumber(Msg *m, ulong uid, int force);
static int impFlags(Box *box, Msg *m, char *flags);
/*
* strategy:
* every mailbox file has an associated .imp file
* which maps upas/fs message digests to uids & message flags.
*
* the .imp files are locked by /mail/fs/usename/L.mbox.
* whenever the flags can be modified, the lock file
* should be opened, thereby locking the uid & flag state.
* for example, whenever new uids are assigned to messages,
* and whenever flags are changed internally, the lock file
* should be open and locked. this means the file must be
* opened during store command, and when changing the \seen
* flag for the fetch command.
*
* if no .imp file exists, a null one must be created before
* assigning uids.
*
* the .imp file has the following format
* imp : "imap internal mailbox description\n"
* uidvalidity " " uidnext "\n"
* messageLines
*
* messageLines :
* | messageLines digest " " uid " " flags "\n"
*
* uid, uidnext, and uidvalidity are 32 bit decimal numbers
* printed right justified in a field NUid characters long.
* the 0 uid implies that no uid has been assigned to the message,
* but the flags are valid. note that message lines are in mailbox
* order, except possibly for 0 uid messages.
*
* digest is an ascii hex string NDigest characters long.
*
* flags has a character for each of NFlag flag fields.
* if the flag is clear, it is represented by a "-".
* set flags are represented as a unique single ascii character.
* the currently assigned flags are, in order:
* MSeen s
* MAnswered a
* MFlagged f
* MDeleted D
* MDraft d
*/
Box*
openBox(char *name, char *fsname, int writable)
{
Box *box;
MbLock *ml;
int n, new;
if(cistrcmp(name, "inbox") == 0)
if(access("msgs", AEXIST) == 0)
name = "msgs";
else
name = "mbox";
fsInit();
debuglog("imap4d open %s %s\n", name, fsname);
if(fprint(fsCtl, "open '/mail/box/%s/%s' %s", username, name, fsname) < 0){
//ZZZ
char err[ERRMAX];
rerrstr(err, sizeof err);
if(strstr(err, "file does not exist") == nil)
fprint(2,
"imap4d at %lud: upas/fs open %s/%s as %s failed: '%s' %s",
time(nil), username, name, fsname, err,
ctime(time(nil))); /* NB: ctime result ends with \n */
fprint(fsCtl, "close %s", fsname);
return nil;
}
/*
* read box to find all messages
* each one has a directory, and is in numerical order
*/
box = MKZ(Box);
box->writable = writable;
n = strlen(name) + 1;
box->name = emalloc(n);
strcpy(box->name, name);
n += STRLEN(".imp");
box->imp = emalloc(n);
snprint(box->imp, n, "%s.imp", name);
n = strlen(fsname) + 1;
box->fs = emalloc(n);
strcpy(box->fs, fsname);
n = STRLEN("/mail/fs/") + strlen(fsname) + 1;
box->fsDir = emalloc(n);
snprint(box->fsDir, n, "/mail/fs/%s", fsname);
box->uidnext = 1;
new = readBox(box);
if(new >= 0){
ml = openImp(box, new);
if(ml != nil){
closeImp(box, ml);
return box;
}
}
closeBox(box, 0);
return nil;
}
/*
* check mailbox
* returns fd of open .imp file if imped.
* otherwise, return value is insignificant
*
* careful: called by idle polling proc
*/
MbLock*
checkBox(Box *box, int imped)
{
MbLock *ml;
Dir *d;
int new;
if(box == nil)
return nil;
/*
* if stat fails, mailbox must be gone
*/
d = cdDirstat(box->fsDir, ".");
if(d == nil){
mboxGone(box);
return nil;
}
new = 0;
if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers
|| box->mtime != d->mtime){
new = readBox(box);
if(new < 0){
free(d);
return nil;
}
}
free(d);
ml = openImp(box, new);
if(ml == nil)
box->writable = 0;
else if(!imped){
closeImp(box, ml);
ml = nil;
}
return ml;
}
/*
* mailbox is unreachable, so mark all messages expunged
* clean up .imp files as well.
*/
static void
mboxGone(Box *box)
{
Msg *m;
if(cdExists(mboxDir, box->name) < 0)
cdRemove(mboxDir, box->imp);
for(m = box->msgs; m != nil; m = m->next)
m->expunged = 1;
box->writable = 0;
}
/*
* read messages in the mailbox
* mark message that no longer exist as expunged
* returns -1 for failure, 0 if no new messages, 1 if new messages.
*/
static int
readBox(Box *box)
{
Msg *msgs, *m, *last;
Dir *d;
char *s;
long max, id;
int i, nd, fd, new;
fd = cdOpen(box->fsDir, ".", OREAD);
if(fd < 0){
syslog(0, "mail",
"imap4d at %lud: upas/fs stat of %s/%s aka %s failed: %r",
time(nil), username, box->name, box->fsDir);
mboxGone(box);
return -1;
}
/*
* read box to find all messages
* each one has a directory, and is in numerical order
*/
d = dirfstat(fd);
if(d == nil){
close(fd);
return -1;
}
box->mtime = d->mtime;
box->qid = d->qid;
last = nil;
msgs = box->msgs;
max = 0;
new = 0;
free(d);
while((nd = dirread(fd, &d)) > 0){
for(i = 0; i < nd; i++){
s = d[i].name;
id = strtol(s, &s, 10);
if(id <= max || *s != '\0'
|| (d[i].mode & DMDIR) != DMDIR)
continue;
max = id;
while(msgs != nil){
last = msgs;
msgs = msgs->next;
if(last->id == id)
goto continueDir;
last->expunged = 1;
}
new = 1;
m = MKZ(Msg);
m->id = id;
m->fsDir = box->fsDir;
m->fs = emalloc(2 * (MsgNameLen + 1));
m->efs = seprint(m->fs, m->fs + (MsgNameLen + 1), "%lud/", id);
m->size = ~0UL;
m->lines = ~0UL;
m->prev = last;
m->flags = MRecent;
if(!msgInfo(m))
freeMsg(m);
else{
if(last == nil)
box->msgs = m;
else
last->next = m;
last = m;
}
continueDir:;
}
free(d);
}
close(fd);
for(; msgs != nil; msgs = msgs->next)
msgs->expunged = 1;
/*
* make up the imap message sequence numbers
*/
id = 1;
for(m = box->msgs; m != nil; m = m->next){
if(m->seq && m->seq != id)
bye("internal error assigning message numbers");
m->seq = id++;
}
box->max = id - 1;
return new;
}
/*
* read in the .imp file, or make one if it doesn't exist.
* make sure all flags and uids are consistent.
* return the mailbox lock.
*/
#define IMPMAGIC "imap internal mailbox description\n"
static MbLock*
openImp(Box *box, int new)
{
Qid qid;
Biobuf b;
MbLock *ml;
int fd;
//ZZZZ
int once;
ml = mbLock();
if(ml == nil)
return nil;
fd = cdOpen(mboxDir, box->imp, OREAD);
once = 0;
ZZZhack:
if(fd < 0 || fqid(fd, &qid) < 0){
if(fd < 0){
char buf[ERRMAX];
errstr(buf, sizeof buf);
if(cistrstr(buf, "does not exist") == nil)
fprint(2, "imap4d at %lud: imp open failed: %s\n", time(nil), buf);
if(!once && cistrstr(buf, "locked") != nil){
once = 1;
fprint(2, "imap4d at %lud: imp %s/%s %s locked when it shouldn't be; spinning\n", time(nil), username, box->name, box->imp);
fd = openLocked(mboxDir, box->imp, OREAD);
goto ZZZhack;
}
}
if(fd >= 0)
close(fd);
fd = createImp(box, &qid);
if(fd < 0){
mbUnlock(ml);
return nil;
}
box->dirtyImp = 1;
if(box->uidvalidity == 0)
box->uidvalidity = box->mtime;
box->impQid = qid;
new = 1;
}else if(qid.path != box->impQid.path || qid.vers != box->impQid.vers){
Binit(&b, fd, OREAD);
if(!parseImp(&b, box)){
box->dirtyImp = 1;
if(box->uidvalidity == 0)
box->uidvalidity = box->mtime;
}
Bterm(&b);
box->impQid = qid;
new = 1;
}
if(new)
boxFlags(box);
close(fd);
return ml;
}
/*
* close the .imp file, after writing out any changes
*/
void
closeImp(Box *box, MbLock *ml)
{
Msg *m;
Qid qid;
Biobuf b;
char buf[NFlags+1];
int fd;
if(ml == nil)
return;
if(!box->dirtyImp){
mbUnlock(ml);
return;
}
fd = cdCreate(mboxDir, box->imp, OWRITE, 0664);
if(fd < 0){
mbUnlock(ml);
return;
}
Binit(&b, fd, OWRITE);
box->dirtyImp = 0;
Bprint(&b, "%s", IMPMAGIC);
Bprint(&b, "%.*lud %.*lud\n", NUid, box->uidvalidity, NUid, box->uidnext);
for(m = box->msgs; m != nil; m = m->next){
if(m->expunged)
continue;
wrImpFlags(buf, m->flags, strcmp(box->fs, "imap") == 0);
Bprint(&b, "%.*s %.*lud %s\n", NDigest, m->info[IDigest], NUid, m->uid, buf);
}
Bterm(&b);
if(fqid(fd, &qid) >= 0)
box->impQid = qid;
close(fd);
mbUnlock(ml);
}
void
wrImpFlags(char *buf, int flags, int killRecent)
{
int i;
for(i = 0; i < NFlags; i++){
if((flags & flagChars[i].v)
&& (flagChars[i].v != MRecent || !killRecent))
buf[i] = flagChars[i].name[0];
else
buf[i] = '-';
}
buf[i] = '\0';
}
int
emptyImp(char *mbox)
{
Dir *d;
long mode;
int fd;
fd = cdCreate(mboxDir, impName(mbox), OWRITE, 0664);
if(fd < 0)
return -1;
d = cdDirstat(mboxDir, mbox);
if(d == nil){
close(fd);
return -1;
}
fprint(fd, "%s%.*lud %.*lud\n", IMPMAGIC, NUid, d->mtime, NUid, 1UL);
mode = d->mode & 0777;
nulldir(d);
d->mode = mode;
dirfwstat(fd, d);
free(d);
return fd;
}
/*
* try to match permissions with mbox
*/
static int
createImp(Box *box, Qid *qid)
{
Dir *d;
long mode;
int fd;
fd = cdCreate(mboxDir, box->imp, OREAD, 0664);
if(fd < 0)
return -1;
d = cdDirstat(mboxDir, box->name);
if(d != nil){
mode = d->mode & 0777;
nulldir(d);
d->mode = mode;
dirfwstat(fd, d);
free(d);
}
if(fqid(fd, qid) < 0){
close(fd);
return -1;
}
return fd;
}
/*
* read or re-read a .imp file.
* this is tricky:
* messages can be deleted by another agent
* we might still have a Msg for an expunged message,
* because we haven't told the client yet.
* we can have a Msg without a .imp entry.
* flag information is added at the end of the .imp by copy & append
* there can be duplicate messages (same digests).
*
* look up existing messages based on uid.
* look up new messages based on in order digest matching.
*
* note: in the face of duplicate messages, one of which is deleted,
* two active servers may decide different ones are valid, and so return
* different uids for the messages. this situation will stablize when the servers exit.
*/
static int
parseImp(Biobuf *b, Box *box)
{
Msg *m, *mm;
char *s, *t, *toks[3];
ulong uid, u;
int match, n;
m = box->msgs;
s = Brdline(b, '\n');
if(s == nil || Blinelen(b) != STRLEN(IMPMAGIC)
|| strncmp(s, IMPMAGIC, STRLEN(IMPMAGIC)) != 0)
return 0;
s = Brdline(b, '\n');
if(s == nil || Blinelen(b) != 2*NUid + 2)
return 0;
s[2*NUid + 1] = '\0';
u = strtoul(s, &t, 10);
if(u != box->uidvalidity && box->uidvalidity != 0)
return 0;
box->uidvalidity = u;
if(*t != ' ' || t != s + NUid)
return 0;
t++;
u = strtoul(t, &t, 10);
if(box->uidnext > u)
return 0;
box->uidnext = u;
if(t != s + 2*NUid+1 || box->uidnext == 0)
return 0;
uid = ~0;
while(m != nil){
s = Brdline(b, '\n');
if(s == nil)
break;
n = Blinelen(b) - 1;
if(n != NDigest + NUid + NFlags + 2
|| s[NDigest] != ' ' || s[NDigest + NUid + 1] != ' ')
return 0;
toks[0] = s;
s[NDigest] = '\0';
toks[1] = s + NDigest + 1;
s[NDigest + NUid + 1] = '\0';
toks[2] = s + NDigest + NUid + 2;
s[n] = '\0';
t = toks[1];
u = strtoul(t, &t, 10);
if(*t != '\0' || uid != ~0 && (uid >= u && u || u && !uid))
return 0;
uid = u;
/*
* zero uid => added by append or copy, only flags valid
* can only match messages without uids, but this message
* may not be the next one, and may have been deleted.
*/
if(!uid){
for(; m != nil && m->uid; m = m->next)
;
for(mm = m; mm != nil; mm = mm->next){
if(mm->info[IDigest] != nil &&
strcmp(mm->info[IDigest], toks[0]) == 0){
if(!mm->uid)
mm->flags = 0;
if(!impFlags(box, mm, toks[2]))
return 0;
m = mm->next;
break;
}
}
continue;
}
/*
* ignore expunged messages,
* and messages already assigned uids which don't match this uid.
* such messages must have been deleted by another imap server,
* which updated the mailbox and .imp file since we read the mailbox,
* or because upas/fs got confused by consecutive duplicate messages,
* the first of which was deleted by another imap server.
*/
for(; m != nil && (m->expunged || m->uid && m->uid < uid); m = m->next)
;
if(m == nil)
break;
/*
* only check for digest match on the next message,
* since it comes before all other messages, and therefore
* must be in the .imp file if they should be.
*/
match = m->info[IDigest] != nil &&
strcmp(m->info[IDigest], toks[0]) == 0;
if(uid && (m->uid == uid || !m->uid && match)){
if(!match)
bye("inconsistent uid");
/*
* wipe out recent flag if some other server saw this new message.
* it will be read from the .imp file if is really should be set,
* ie the message was only seen by a status command.
*/
if(!m->uid)
m->flags = 0;
if(!impFlags(box, m, toks[2]))
return 0;
m->uid = uid;
m = m->next;
}
}
return 1;
}
/*
* parse .imp flags
*/
static int
impFlags(Box *box, Msg *m, char *flags)
{
int i, f;
f = 0;
for(i = 0; i < NFlags; i++){
if(flags[i] == '-')
continue;
if(flags[i] != flagChars[i].name[0])
return 0;
f |= flagChars[i].v;
}
/*
* recent flags are set until the first time message's box is selected or examined.
* it may be stored in the file as a side effect of a status or subscribe command;
* if so, clear it out.
*/
if((f & MRecent) && strcmp(box->fs, "imap") == 0)
box->dirtyImp = 1;
f |= m->flags & MRecent;
/*
* all old messages with changed flags should be reported to the client
*/
if(m->uid && m->flags != f){
box->sendFlags = 1;
m->sendFlags = 1;
}
m->flags = f;
return 1;
}
/*
* assign uids to any new messages
* which aren't already in the .imp file.
* sum up totals for flag values.
*/
static void
boxFlags(Box *box)
{
Msg *m;
box->recent = 0;
for(m = box->msgs; m != nil; m = m->next){
if(m->uid == 0){
box->dirtyImp = 1;
box->uidnext = uidRenumber(m, box->uidnext, 0);
}
if(m->flags & MRecent)
box->recent++;
}
}
static ulong
uidRenumber(Msg *m, ulong uid, int force)
{
for(; m != nil; m = m->next){
if(!force && m->uid != 0)
bye("uid renumbering with a valid uid");
m->uid = uid++;
}
return uid;
}
void
closeBox(Box *box, int opened)
{
Msg *m, *next;
/*
* make sure to leave the mailbox directory so upas/fs can close the mailbox
*/
myChdir(mboxDir);
if(box->writable){
deleteMsgs(box);
if(expungeMsgs(box, 0))
closeImp(box, checkBox(box, 1));
}
if(fprint(fsCtl, "close %s", box->fs) < 0 && opened)
bye("can't talk to mail server");
for(m = box->msgs; m != nil; m = next){
next = m->next;
freeMsg(m);
}
free(box->name);
free(box->fs);
free(box->fsDir);
free(box->imp);
free(box);
}
int
deleteMsgs(Box *box)
{
Msg *m;
char buf[BufSize], *p, *start;
int ok;
if(!box->writable)
return 0;
/*
* first pass: delete messages; gang the writes together for speed.
*/
ok = 1;
start = seprint(buf, buf + sizeof(buf), "delete %s", box->fs);
p = start;
for(m = box->msgs; m != nil; m = m->next){
if((m->flags & MDeleted) && !m->expunged){
m->expunged = 1;
p = seprint(p, buf + sizeof(buf), " %lud", m->id);
if(p + 32 >= buf + sizeof(buf)){
if(write(fsCtl, buf, p - buf) < 0)
bye("can't talk to mail server");
p = start;
}
}
}
if(p != start && write(fsCtl, buf, p - buf) < 0)
bye("can't talk to mail server");
return ok;
}
/*
* second pass: remove the message structure,
* and renumber message sequence numbers.
* update messages counts in mailbox.
* returns true if anything changed.
*/
int
expungeMsgs(Box *box, int send)
{
Msg *m, *next, *last;
ulong n;
n = 0;
last = nil;
for(m = box->msgs; m != nil; m = next){
m->seq -= n;
next = m->next;
if(m->expunged){
if(send)
Bprint(&bout, "* %lud expunge\r\n", m->seq);
if(m->flags & MRecent)
box->recent--;
n++;
if(last == nil)
box->msgs = next;
else
last->next = next;
freeMsg(m);
}else
last = m;
}
if(n){
box->max -= n;
box->dirtyImp = 1;
}
return n;
}
static void
fsInit(void)
{
if(fsCtl >= 0)
return;
fsCtl = open("/mail/fs/ctl", ORDWR);
if(fsCtl < 0)
bye("can't open mail file system");
if(fprint(fsCtl, "close mbox") < 0)
bye("can't initialize mail file system");
}
static char *stoplist[] =
{
"mbox",
"pipeto",
"forward",
"names",
"pipefrom",
"headers",
"imap.ok",
0
};
enum {
Maxokbytes = 4096,
Maxfolders = Maxokbytes / 4,
};
static char *folders[Maxfolders];
static char *folderbuff;
static void
readokfolders(void)
{
int fd, nr;
fd = open("imap.ok", OREAD);
if(fd < 0)
return;
folderbuff = malloc(Maxokbytes);
if(folderbuff == nil) {
close(fd);
return;
}
nr = read(fd, folderbuff, Maxokbytes-1); /* once is ok */
close(fd);
if(nr < 0){
free(folderbuff);
folderbuff = nil;
return;
}
folderbuff[nr] = 0;
tokenize(folderbuff, folders, nelem(folders));
}
/*
* reject bad mailboxes based on mailbox name
*/
int
okMbox(char *path)
{
char *name;
int i;
if(folderbuff == nil && access("imap.ok", AREAD) == 0)
readokfolders();
name = strrchr(path, '/');
if(name == nil)
name = path;
else
name++;
if(folderbuff != nil){
for(i = 0; i < nelem(folders) && folders[i] != nil; i++)
if(cistrcmp(folders[i], name) == 0)
return 1;
return 0;
}
if(strlen(name) + STRLEN(".imp") >= MboxNameLen)
return 0;
for(i = 0; stoplist[i]; i++)
if(strcmp(name, stoplist[i]) == 0)
return 0;
if(isprefix("L.", name) || isprefix("imap-tmp.", name)
|| issuffix(".imp", name)
|| strcmp("imap.subscribed", name) == 0
|| isdotdot(name) || name[0] == '/')
return 0;
return 1;
}