7 static NamedInt flagChars[NFlags] =
17 static int fsCtl = -1;
19 static void boxFlags(Box *box);
20 static int createImp(Box *box, Qid *qid);
21 static void fsInit(void);
22 static void mboxGone(Box *box);
23 static MbLock *openImp(Box *box, int new);
24 static int parseImp(Biobuf *b, Box *box);
25 static int readBox(Box *box);
26 static ulong uidRenumber(Msg *m, ulong uid, int force);
27 static int impFlags(Box *box, Msg *m, char *flags);
31 * every mailbox file has an associated .imp file
32 * which maps upas/fs message digests to uids & message flags.
34 * the .imp files are locked by /mail/fs/usename/L.mbox.
35 * whenever the flags can be modified, the lock file
36 * should be opened, thereby locking the uid & flag state.
37 * for example, whenever new uids are assigned to messages,
38 * and whenever flags are changed internally, the lock file
39 * should be open and locked. this means the file must be
40 * opened during store command, and when changing the \seen
41 * flag for the fetch command.
43 * if no .imp file exists, a null one must be created before
46 * the .imp file has the following format
47 * imp : "imap internal mailbox description\n"
48 * uidvalidity " " uidnext "\n"
52 * | messageLines digest " " uid " " flags "\n"
54 * uid, uidnext, and uidvalidity are 32 bit decimal numbers
55 * printed right justified in a field NUid characters long.
56 * the 0 uid implies that no uid has been assigned to the message,
57 * but the flags are valid. note that message lines are in mailbox
58 * order, except possibly for 0 uid messages.
60 * digest is an ascii hex string NDigest characters long.
62 * flags has a character for each of NFlag flag fields.
63 * if the flag is clear, it is represented by a "-".
64 * set flags are represented as a unique single ascii character.
65 * the currently assigned flags are, in order:
73 openBox(char *name, char *fsname, int writable)
79 if(cistrcmp(name, "inbox") == 0)
80 if(access("msgs", AEXIST) == 0)
85 debuglog("imap4d open %s %s\n", name, fsname);
87 if(fprint(fsCtl, "open '/mail/box/%s/%s' %s", username, name, fsname) < 0){
91 rerrstr(err, sizeof err);
92 if(strstr(err, "file does not exist") == nil)
94 "imap4d at %lud: upas/fs open %s/%s as %s failed: '%s' %s",
95 time(nil), username, name, fsname, err,
96 ctime(time(nil))); /* NB: ctime result ends with \n */
97 fprint(fsCtl, "close %s", fsname);
102 * read box to find all messages
103 * each one has a directory, and is in numerical order
106 box->writable = writable;
108 n = strlen(name) + 1;
109 box->name = emalloc(n);
110 strcpy(box->name, name);
113 box->imp = emalloc(n);
114 snprint(box->imp, n, "%s.imp", name);
116 n = strlen(fsname) + 1;
117 box->fs = emalloc(n);
118 strcpy(box->fs, fsname);
120 n = STRLEN("/mail/fs/") + strlen(fsname) + 1;
121 box->fsDir = emalloc(n);
122 snprint(box->fsDir, n, "/mail/fs/%s", fsname);
127 ml = openImp(box, new);
139 * returns fd of open .imp file if imped.
140 * otherwise, return value is insignificant
142 * careful: called by idle polling proc
145 checkBox(Box *box, int imped)
155 * if stat fails, mailbox must be gone
157 d = cdDirstat(box->fsDir, ".");
163 if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers
164 || box->mtime != d->mtime){
172 ml = openImp(box, new);
183 * mailbox is unreachable, so mark all messages expunged
184 * clean up .imp files as well.
191 if(cdExists(mboxDir, box->name) < 0)
192 cdRemove(mboxDir, box->imp);
193 for(m = box->msgs; m != nil; m = m->next)
199 * read messages in the mailbox
200 * mark message that no longer exist as expunged
201 * returns -1 for failure, 0 if no new messages, 1 if new messages.
206 Msg *msgs, *m, *last;
212 fd = cdOpen(box->fsDir, ".", OREAD);
215 "imap4d at %lud: upas/fs stat of %s/%s aka %s failed: %r",
216 time(nil), username, box->name, box->fsDir);
222 * read box to find all messages
223 * each one has a directory, and is in numerical order
230 box->mtime = d->mtime;
237 while((nd = dirread(fd, &d)) > 0){
238 for(i = 0; i < nd; i++){
240 id = strtol(s, &s, 10);
241 if(id <= max || *s != '\0'
242 || (d[i].mode & DMDIR) != DMDIR)
258 m->fsDir = box->fsDir;
259 m->fs = emalloc(2 * (MsgNameLen + 1));
260 m->efs = seprint(m->fs, m->fs + (MsgNameLen + 1), "%lud/", id);
279 for(; msgs != nil; msgs = msgs->next)
283 * make up the imap message sequence numbers
286 for(m = box->msgs; m != nil; m = m->next){
287 if(m->seq && m->seq != id)
288 bye("internal error assigning message numbers");
297 * read in the .imp file, or make one if it doesn't exist.
298 * make sure all flags and uids are consistent.
299 * return the mailbox lock.
301 #define IMPMAGIC "imap internal mailbox description\n"
303 openImp(Box *box, int new)
315 fd = cdOpen(mboxDir, box->imp, OREAD);
318 if(fd < 0 || fqid(fd, &qid) < 0){
322 errstr(buf, sizeof buf);
323 if(cistrstr(buf, "does not exist") == nil)
324 fprint(2, "imap4d at %lud: imp open failed: %s\n", time(nil), buf);
325 if(!once && cistrstr(buf, "locked") != nil){
327 fprint(2, "imap4d at %lud: imp %s/%s %s locked when it shouldn't be; spinning\n", time(nil), username, box->name, box->imp);
328 fd = openLocked(mboxDir, box->imp, OREAD);
334 fd = createImp(box, &qid);
340 if(box->uidvalidity == 0)
341 box->uidvalidity = box->mtime;
344 }else if(qid.path != box->impQid.path || qid.vers != box->impQid.vers){
345 Binit(&b, fd, OREAD);
346 if(!parseImp(&b, box)){
348 if(box->uidvalidity == 0)
349 box->uidvalidity = box->mtime;
362 * close the .imp file, after writing out any changes
365 closeImp(Box *box, MbLock *ml)
380 fd = cdCreate(mboxDir, box->imp, OWRITE, 0664);
385 Binit(&b, fd, OWRITE);
388 Bprint(&b, "%s", IMPMAGIC);
389 Bprint(&b, "%.*lud %.*lud\n", NUid, box->uidvalidity, NUid, box->uidnext);
390 for(m = box->msgs; m != nil; m = m->next){
393 wrImpFlags(buf, m->flags, strcmp(box->fs, "imap") == 0);
394 Bprint(&b, "%.*s %.*lud %s\n", NDigest, m->info[IDigest], NUid, m->uid, buf);
398 if(fqid(fd, &qid) >= 0)
405 wrImpFlags(char *buf, int flags, int killRecent)
409 for(i = 0; i < NFlags; i++){
410 if((flags & flagChars[i].v)
411 && (flagChars[i].v != MRecent || !killRecent))
412 buf[i] = flagChars[i].name[0];
426 fd = cdCreate(mboxDir, impName(mbox), OWRITE, 0664);
429 d = cdDirstat(mboxDir, mbox);
434 fprint(fd, "%s%.*lud %.*lud\n", IMPMAGIC, NUid, d->mtime, NUid, 1UL);
435 mode = d->mode & 0777;
444 * try to match permissions with mbox
447 createImp(Box *box, Qid *qid)
453 fd = cdCreate(mboxDir, box->imp, OREAD, 0664);
456 d = cdDirstat(mboxDir, box->name);
458 mode = d->mode & 0777;
464 if(fqid(fd, qid) < 0){
473 * read or re-read a .imp file.
475 * messages can be deleted by another agent
476 * we might still have a Msg for an expunged message,
477 * because we haven't told the client yet.
478 * we can have a Msg without a .imp entry.
479 * flag information is added at the end of the .imp by copy & append
480 * there can be duplicate messages (same digests).
482 * look up existing messages based on uid.
483 * look up new messages based on in order digest matching.
485 * note: in the face of duplicate messages, one of which is deleted,
486 * two active servers may decide different ones are valid, and so return
487 * different uids for the messages. this situation will stablize when the servers exit.
490 parseImp(Biobuf *b, Box *box)
493 char *s, *t, *toks[3];
498 s = Brdline(b, '\n');
499 if(s == nil || Blinelen(b) != STRLEN(IMPMAGIC)
500 || strncmp(s, IMPMAGIC, STRLEN(IMPMAGIC)) != 0)
503 s = Brdline(b, '\n');
504 if(s == nil || Blinelen(b) != 2*NUid + 2)
506 s[2*NUid + 1] = '\0';
507 u = strtoul(s, &t, 10);
508 if(u != box->uidvalidity && box->uidvalidity != 0)
510 box->uidvalidity = u;
511 if(*t != ' ' || t != s + NUid)
514 u = strtoul(t, &t, 10);
518 if(t != s + 2*NUid+1 || box->uidnext == 0)
523 s = Brdline(b, '\n');
527 if(n != NDigest + NUid + NFlags + 2
528 || s[NDigest] != ' ' || s[NDigest + NUid + 1] != ' ')
532 toks[1] = s + NDigest + 1;
533 s[NDigest + NUid + 1] = '\0';
534 toks[2] = s + NDigest + NUid + 2;
537 u = strtoul(t, &t, 10);
538 if(*t != '\0' || uid != ~0 && (uid >= u && u || u && !uid))
543 * zero uid => added by append or copy, only flags valid
544 * can only match messages without uids, but this message
545 * may not be the next one, and may have been deleted.
548 for(; m != nil && m->uid; m = m->next)
550 for(mm = m; mm != nil; mm = mm->next){
551 if(mm->info[IDigest] != nil &&
552 strcmp(mm->info[IDigest], toks[0]) == 0){
555 if(!impFlags(box, mm, toks[2]))
565 * ignore expunged messages,
566 * and messages already assigned uids which don't match this uid.
567 * such messages must have been deleted by another imap server,
568 * which updated the mailbox and .imp file since we read the mailbox,
569 * or because upas/fs got confused by consecutive duplicate messages,
570 * the first of which was deleted by another imap server.
572 for(; m != nil && (m->expunged || m->uid && m->uid < uid); m = m->next)
578 * only check for digest match on the next message,
579 * since it comes before all other messages, and therefore
580 * must be in the .imp file if they should be.
582 match = m->info[IDigest] != nil &&
583 strcmp(m->info[IDigest], toks[0]) == 0;
584 if(uid && (m->uid == uid || !m->uid && match)){
586 bye("inconsistent uid");
589 * wipe out recent flag if some other server saw this new message.
590 * it will be read from the .imp file if is really should be set,
591 * ie the message was only seen by a status command.
596 if(!impFlags(box, m, toks[2]))
609 impFlags(Box *box, Msg *m, char *flags)
614 for(i = 0; i < NFlags; i++){
617 if(flags[i] != flagChars[i].name[0])
623 * recent flags are set until the first time message's box is selected or examined.
624 * it may be stored in the file as a side effect of a status or subscribe command;
625 * if so, clear it out.
627 if((f & MRecent) && strcmp(box->fs, "imap") == 0)
629 f |= m->flags & MRecent;
632 * all old messages with changed flags should be reported to the client
634 if(m->uid && m->flags != f){
643 * assign uids to any new messages
644 * which aren't already in the .imp file.
645 * sum up totals for flag values.
653 for(m = box->msgs; m != nil; m = m->next){
656 box->uidnext = uidRenumber(m, box->uidnext, 0);
658 if(m->flags & MRecent)
664 uidRenumber(Msg *m, ulong uid, int force)
666 for(; m != nil; m = m->next){
667 if(!force && m->uid != 0)
668 bye("uid renumbering with a valid uid");
675 closeBox(Box *box, int opened)
680 * make sure to leave the mailbox directory so upas/fs can close the mailbox
686 if(expungeMsgs(box, 0))
687 closeImp(box, checkBox(box, 1));
690 if(fprint(fsCtl, "close %s", box->fs) < 0 && opened)
691 bye("can't talk to mail server");
692 for(m = box->msgs; m != nil; m = next){
707 char buf[BufSize], *p, *start;
714 * first pass: delete messages; gang the writes together for speed.
717 start = seprint(buf, buf + sizeof(buf), "delete %s", box->fs);
719 for(m = box->msgs; m != nil; m = m->next){
720 if((m->flags & MDeleted) && !m->expunged){
722 p = seprint(p, buf + sizeof(buf), " %lud", m->id);
723 if(p + 32 >= buf + sizeof(buf)){
724 if(write(fsCtl, buf, p - buf) < 0)
725 bye("can't talk to mail server");
730 if(p != start && write(fsCtl, buf, p - buf) < 0)
731 bye("can't talk to mail server");
737 * second pass: remove the message structure,
738 * and renumber message sequence numbers.
739 * update messages counts in mailbox.
740 * returns true if anything changed.
743 expungeMsgs(Box *box, int send)
745 Msg *m, *next, *last;
750 for(m = box->msgs; m != nil; m = next){
755 Bprint(&bout, "* %lud expunge\r\n", m->seq);
756 if(m->flags & MRecent)
779 fsCtl = open("/mail/fs/ctl", ORDWR);
781 bye("can't open mail file system");
782 if(fprint(fsCtl, "close mbox") < 0)
783 bye("can't initialize mail file system");
786 static char *stoplist[] =
800 Maxfolders = Maxokbytes / 4,
803 static char *folders[Maxfolders];
804 static char *folderbuff;
811 fd = open("imap.ok", OREAD);
814 folderbuff = malloc(Maxokbytes);
815 if(folderbuff == nil) {
819 nr = read(fd, folderbuff, Maxokbytes-1); /* once is ok */
827 tokenize(folderbuff, folders, nelem(folders));
831 * reject bad mailboxes based on mailbox name
839 if(folderbuff == nil && access("imap.ok", AREAD) == 0)
841 name = strrchr(path, '/');
846 if(folderbuff != nil){
847 for(i = 0; i < nelem(folders) && folders[i] != nil; i++)
848 if(cistrcmp(folders[i], name) == 0)
852 if(strlen(name) + STRLEN(".imp") >= MboxNameLen)
854 for(i = 0; stoplist[i]; i++)
855 if(strcmp(name, stoplist[i]) == 0)
857 if(isprefix("L.", name) || isprefix("imap-tmp.", name)
858 || issuffix(".imp", name)
859 || strcmp("imap.subscribed", name) == 0
860 || isdotdot(name) || name[0] == '/')