shithub: riscv

ref: c06e464ec45dd5880850fa60c850498861c514d3
dir: /sys/src/cmd/ip/imap4d/mbox.c/

View raw version
#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;
}