#include "imap4d.h"

static  int     fsctl           = -1;
static  char    Ecanttalk[]     = "can't talk to mail server";

static void
fsinit(void)
{
       if(fsctl != -1)
               return;
       fsctl = open("/mail/fs/ctl", ORDWR);
       if(fsctl == -1)
               bye(Ecanttalk);
}

static void
boxflags(Box *box)
{
       Msg *m;

       box->recent = 0;
       for(m = box->msgs; m != nil; m = m->next){
               if(m->uid == 0){
       //              fprint(2, "unassigned uid %s\n", m->info[Idigest]);
                       box->dirtyimp = 1;
                       m->uid = box->uidnext++;
               }
               if(m->flags & Frecent)
                       box->recent++;
       }
}

/*
* try to match permissions with mbox
*/
static int
createimp(Box *box, Qid *qid)
{
       int fd;
       long mode;
       Dir *d;

       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 in the .imp file, or make one if it doesn't exist.
* make sure all flags and uids are consistent.
* return the mailbox lock.
*/
static Mblock*
openimp(Box *box, int new)
{
       char buf[ERRMAX];
       int fd;
       Biobuf b;
       Mblock *ml;
       Qid qid;

       ml = mblock();
       if(ml == nil)
               return nil;
       fd = cdopen(mboxdir, box->imp, OREAD);
       if(fd < 0 || fqid(fd, &qid) < 0){
               if(fd < 0){
                       errstr(buf, sizeof buf);
                       if(cistrstr(buf, "does not exist") == nil)
                               ilog("imp: %s: %s", box->imp, buf);
                       else
                               debuglog("imp: %s: %s .. creating", box->imp, buf);
               }else{
                       close(fd);
                       ilog("%s: bogus imp: bad qid: recreating", box->imp);
               }
               fd = createimp(box, &qid);
               if(fd < 0){
                       ilog("createimp fails: %r");
                       mbunlock(ml);
                       return nil;
               }
               box->dirtyimp = 1;
               if(box->uidvalidity == 0){
                       ilog("set uidvalidity %lud [new]\n", box->uidvalidity);
                       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) == -1){
                       ilog("%s: bogus imp: parse failure", box->imp);
                       box->dirtyimp = 1;
                       if(box->uidvalidity == 0){
                               ilog("set uidvalidity %lud [parseerr]\n", box->uidvalidity);
                               box->uidvalidity = box->mtime;
                       }
               }
               Bterm(&b);
               box->impqid = qid;
               new = 1;
       }
       if(new)
               boxflags(box);
       close(fd);
       return ml;
}

/*
* mailbox is unreachable, so mark all messages expunged
* clean up .imp files as well.
*/
static void
mboxgone(Box *box)
{
       char buf[ERRMAX];
       Msg *m;

       rerrstr(buf, ERRMAX);
       if(strstr(buf, "hungup channel"))
               bye(Ecanttalk);
//      too smart.
//      if(cdexists(mboxdir, box->name) < 0)
//              cdremove(mboxdir, box->imp);
       for(m = box->msgs; m != nil; m = m->next)
               m->expunged = 1;
       ilog("mboxgone");
       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.
*/
enum {
       Gone    = 2,            /* don't unexpunge messages */
};

static int
readbox(Box *box)
{
       char buf[ERRMAX];
       int i, n, fd, new, id;
       Dir *d;
       Msg *m, *last;

       fd = cdopen(box->fsdir, ".", OREAD);
       if(fd == -1){
goinggoinggone:
               rerrstr(buf, ERRMAX);
               ilog("upas/fs stat of %s/%s aka %s failed: %r",
                       username, box->name, box->fsdir);
               mboxgone(box);
               return -1;
       }

       if((d = dirfstat(fd)) == nil){
               close(fd);
               goto goinggoinggone;
       }
       box->mtime = d->mtime;
       box->qid = d->qid;
       last = nil;
       for(m = box->msgs; m != nil; m = m->next){
               last = m;
               m->expunged |= Gone;
       }
       new = 0;
       free(d);

       for(;;){
               n = dirread(fd, &d);
               if(n <= 0){
                       close(fd);
                       if(n == -1)
                               goto goinggoinggone;
                       break;
               }
               for(i = 0; i < n; i++){
                       if((d[i].qid.type & QTDIR) == 0)
                               continue;
                       id = atoi(d[i].name);
                       if(m = fstreefind(box, id)){
                               m->expunged &= ~Gone;
                               continue;
                       }
                       new = 1;
                       m = MKZ(Msg);
                       m->id = id;
                       m->fsdir = box->fsdir;
                       m->fs = emalloc(2 * (Filelen + 1));
                       m->efs = seprint(m->fs, m->fs + (Filelen + 1), "%ud/", id);
                       m->size = ~0UL;
                       m->lines = ~0UL;
                       m->flags = Frecent;
                       if(!msginfo(m) || m->info[Idigest] == 0)
                               freemsg(0, m);
                       else{
                               fstreeadd(box, m);
                               if(last == nil)
                                       box->msgs = m;
                               else
                                       last->next = m;
                               last = m;
                       }
               }
               free(d);
       }

       /* box->max is invalid here */
       return new;
}

int
uidcmp(void *va, void *vb)
{
       Msg **a, **b;

       a = va;
       b = vb;
       return (*a)->uid - (*b)->uid;
}

static void
sequence(Box *box)
{
       Msg **a, *m;
       int n, i;

       n = 0;
       for(m = box->msgs; m; m = m->next)
               n++;
       a = ezmalloc(n * sizeof *a);
       i = 0;
       for(m = box->msgs; m; m = m->next)
               a[i++] = m;
       qsort(a, n, sizeof *a, uidcmp);
       for(i = 0; i < n - 1; i++)
               a[i]->next = a[i + 1];
       for(i = 0; i < n; i++)
               if(a[i]->seq && a[i]->seq != i + 1)
                       bye("internal error assigning message numbers");
               else
                       a[i]->seq = i + 1;
       box->msgs = nil;
       if(n > 0){
               a[n - 1]->next = nil;
               box->msgs = a[0];
       }
       box->max = n;
       memset(a, 0, n*sizeof *a);
       free(a);
}

/*
* 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:
*      Fseen           s
*      Fanswered       a
*      Fflagged        f
*      Fdeleted        D
*      Fdraft          d
*/

Box*
openbox(char *name, char *fsname, int writable)
{
       char err[ERRMAX];
       int new;
       Box *box;
       Mblock *ml;

       fsinit();
if(!strcmp(name, "mbox"))ilog("open %F %q", name, fsname);
       if(fprint(fsctl, "open %F %q", name, fsname) < 0){
               rerrstr(err, sizeof err);
               if(strstr(err, "file does not exist") == nil)
                       ilog("fs open %F as %s: %s", name, fsname, err);
               if(strstr(err, "hungup channel"))
                       bye(Ecanttalk);
               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;
       box->name = smprint("%s", name);
       box->imp = smprint("%s.imp", name);
       box->fs = smprint("%s", fsname);
       box->fsdir = smprint("/mail/fs/%s", fsname);
       box->uidnext = 1;
       box->fstree = avlcreate(fstreecmp);
       new = readbox(box);
       if(new >= 0 && (ml = openimp(box, new))){
               closeimp(box, ml);
               sequence(box);
               return box;
       }
       closebox(box, 0);
       return nil;
}

/*
* careful: called by idle polling proc
*/
Mblock*
checkbox(Box *box, int imped)
{
       int new;
       Dir *d;
       Mblock *ml;

       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){
               ilog("openimp fails; box->writable = 0: %r");
               box->writable = 0;
       }else if(!imped){
               closeimp(box, ml);
               ml = nil;
       }
       if(new || box->dirtyimp)
               sequence(box);
       return ml;
}

/*
* close the .imp file, after writing out any changes
*/
void
closeimp(Box *box, Mblock *ml)
{
       int fd;
       Biobuf b;
       Qid qid;

       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;
       wrimp(&b, box);
       Bterm(&b);

       if(fqid(fd, &qid) == 0)
               box->impqid = qid;
       close(fd);
       mbunlock(ml);
}

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){
               deletemsg(box, 0);
               if(expungemsgs(box, 0))
                       closeimp(box, checkbox(box, 1));
       }

       if(fprint(fsctl, "close %s", box->fs) < 0 && opened)
               bye(Ecanttalk);
       for(m = box->msgs; m != nil; m = next){
               next = m->next;
               freemsg(box, m);
       }
       free(box->name);
       free(box->fs);
       free(box->fsdir);
       free(box->imp);
       free(box->fstree);
       free(box);
}

int
deletemsg(Box *box, Msgset *ms)
{
       char buf[Bufsize], *p, *start;
       int ok;
       Msg *m;

       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(ms == 0 || ms && inmsgset(ms, m->uid))
               if((m->flags & Fdeleted) && !m->expunged){
                       m->expunged = 1;
                       p = seprint(p, buf + sizeof buf, " %ud", m->id);
                       if(p + 32 >= buf + sizeof buf){
                               if(write(fsctl, buf, p - buf) == -1)
                                       bye(Ecanttalk);
                               p = start;
                       }
               }
       if(p != start && write(fsctl, buf, p - buf) == -1)
               bye(Ecanttalk);
       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)
{
       uint n;
       Msg *m, *next, *last;

       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, "* %ud expunge\r\n", m->seq);
                       if(m->flags & Frecent)
                               box->recent--;
                       n++;
                       if(last == nil)
                               box->msgs = next;
                       else
                               last->next = next;
                       freemsg(box, m);
               }else
                       last = m;
       }
       if(n){
               box->max -= n;
               box->dirtyimp = 1;
       }
       return n;
}

static char *stoplist[] =
{
       ".",
       "dead.letter",
       "forward",
       "headers",
       "imap.subscribed",
       "mbox",
       "names",
       "pipefrom",
       "pipeto",
       0
};

/*
* reject bad mailboxes based on mailbox name
*/
int
okmbox(char *path)
{
       char *name;
       int i, c;

       name = strrchr(path, '/');
       if(name == nil)
               name = path;
       else
               name++;
       if(strlen(name) + STRLEN(".imp") >= Pathlen)
               return 0;
       for(i = 0; stoplist[i]; i++)
               if(strcmp(name, stoplist[i]) == 0)
                       return 0;
       c = name[0];
       if(c == 0 || c == '-' || c == '/'
       || isdotdot(name)
       || isprefix("L.", name)
       || isprefix("imap-tmp.", name)
       || issuffix("-", name)
       || issuffix(".00", name)
       || issuffix(".imp", name)
       || issuffix(".idx", name))
               return 0;

       return 1;
}

int
creatembox(char *mbox)
{
       fsinit();
       if(fprint(fsctl, "create %q", mbox) > 0){
               fprint(fsctl, "close %s", mbox);
               return 0;
       }
       return -1;
}

/*
* rename mailbox.  truncaes or removes the source.
* bug? is the lock required
* upas/fs helpfully moves our .imp file.
*/
int
renamebox(char *from, char *to, int doremove)
{
       char *p;
       int r;
       Mblock *ml;

       fsinit();
       ml = mblock();
       if(ml == nil)
               return 0;
       if(doremove)
               r = fprint(fsctl, "rename %F %F", from, to);
       else
               r = fprint(fsctl, "rename -t %F %F", from, to);
       if(r > 0){
               if(p = strrchr(to, '/'))
                       p++;
               else
                       p = to;
               fprint(fsctl, "close %s", p);
       }
       mbunlock(ml);
       return r > 0;
}

/*
* upas/fs likes us; he removes the .imp file
*/
int
removembox(char *path)
{
       fsinit();
       return fprint(fsctl, "remove %s", path) > 0;
}