1 #include <u.h> 2 #include <libc.h> 3 #include <bio.h> 4 #include <auth.h> 5 #include "imap4d.h" 6 7 static NamedInt flagChars[NFlags] = 8 { 9 {"s", MSeen}, 10 {"a", MAnswered}, 11 {"f", MFlagged}, 12 {"D", MDeleted}, 13 {"d", MDraft}, 14 {"r", MRecent}, 15 }; 16 17 static int fsCtl = -1; 18 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); 28 29 /* 30 * strategy: 31 * every mailbox file has an associated .imp file 32 * which maps upas/fs message digests to uids & message flags. 33 * 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. 42 * 43 * if no .imp file exists, a null one must be created before 44 * assigning uids. 45 * 46 * the .imp file has the following format 47 * imp : "imap internal mailbox description\n" 48 * uidvalidity " " uidnext "\n" 49 * messageLines 50 * 51 * messageLines : 52 * | messageLines digest " " uid " " flags "\n" 53 * 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. 59 * 60 * digest is an ascii hex string NDigest characters long. 61 * 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: 66 * MSeen s 67 * MAnswered a 68 * MFlagged f 69 * MDeleted D 70 * MDraft d 71 */ 72 Box* 73 openBox(char *name, char *fsname, int writable) 74 { 75 Box *box; 76 MbLock *ml; 77 int n, new; 78 79 if(cistrcmp(name, "inbox") == 0) 80 name = "mbox"; 81 82 fsInit(); 83 if(fprint(fsCtl, "open /mail/box/%s/%s %s", username, name, fsname) < 0){ 84 //ZZZ 85 char err[ERRMAX]; 86 errstr(err, sizeof err); 87 if(strstr(err, "file does not exist") == nil) 88 fprint(2, "imap4d at %lud: upas/fs open %s/%s as %s failed: '%s' %s\n", time(nil), username, name, fsname, err, ctime(time(nil))); 89 fprint(fsCtl, "close %s", fsname); 90 return nil; 91 } 92 93 /* 94 * read box to find all messages 95 * each one has a directory, and is in numerical order 96 */ 97 box = MKZ(Box); 98 box->writable = writable; 99 100 n = strlen(name) + 1; 101 box->name = emalloc(n); 102 strcpy(box->name, name); 103 104 n += STRLEN(".imp"); 105 box->imp = emalloc(n); 106 snprint(box->imp, n, "%s.imp", name); 107 108 n = strlen(fsname) + 1; 109 box->fs = emalloc(n); 110 strcpy(box->fs, fsname); 111 112 n = STRLEN("/mail/fs/") + strlen(fsname) + 1; 113 box->fsDir = emalloc(n); 114 snprint(box->fsDir, n, "/mail/fs/%s", fsname); 115 116 box->uidnext = 1; 117 new = readBox(box); 118 if(new >= 0){ 119 ml = openImp(box, new); 120 if(ml != nil){ 121 closeImp(box, ml); 122 return box; 123 } 124 } 125 closeBox(box, 0); 126 return nil; 127 } 128 129 /* 130 * check mailbox 131 * returns fd of open .imp file if imped. 132 * otherwise, return value is insignificant 133 * 134 * careful: called by idle polling proc 135 */ 136 MbLock* 137 checkBox(Box *box, int imped) 138 { 139 MbLock *ml; 140 Dir *d; 141 int new; 142 143 if(box == nil) 144 return nil; 145 146 /* 147 * if stat fails, mailbox must be gone 148 */ 149 d = cdDirstat(box->fsDir, "."); 150 if(d == nil){ 151 mboxGone(box); 152 return nil; 153 } 154 new = 0; 155 if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers 156 || box->mtime != d->mtime){ 157 new = readBox(box); 158 if(new < 0){ 159 free(d); 160 return nil; 161 } 162 } 163 free(d); 164 ml = openImp(box, new); 165 if(ml == nil) 166 box->writable = 0; 167 else if(!imped){ 168 closeImp(box, ml); 169 ml = nil; 170 } 171 return ml; 172 } 173 174 /* 175 * mailbox is unreachable, so mark all messages expunged 176 * clean up .imp files as well. 177 */ 178 static void 179 mboxGone(Box *box) 180 { 181 Msg *m; 182 183 if(cdExists(mboxDir, box->name) < 0) 184 cdRemove(mboxDir, box->imp); 185 for(m = box->msgs; m != nil; m = m->next) 186 m->expunged = 1; 187 box->writable = 0; 188 } 189 190 /* 191 * read messages in the mailbox 192 * mark message that no longer exist as expunged 193 * returns -1 for failure, 0 if no new messages, 1 if new messages. 194 */ 195 static int 196 readBox(Box *box) 197 { 198 Msg *msgs, *m, *last; 199 Dir *d; 200 char *s; 201 long max, id; 202 int i, nd, fd, new; 203 204 fd = cdOpen(box->fsDir, ".", OREAD); 205 if(fd < 0){ 206 syslog(0, "mail", "imap4d at %lud: upas/fs stat of %s/%s aka %s failed: %r\n", time(nil), 207 username, box->name, box->fsDir); 208 mboxGone(box); 209 return -1; 210 } 211 212 /* 213 * read box to find all messages 214 * each one has a directory, and is in numerical order 215 */ 216 d = dirfstat(fd); 217 if(d == nil){ 218 close(fd); 219 return -1; 220 } 221 box->mtime = d->mtime; 222 box->qid = d->qid; 223 last = nil; 224 msgs = box->msgs; 225 max = 0; 226 new = 0; 227 free(d); 228 while((nd = dirread(fd, &d)) > 0){ 229 for(i = 0; i < nd; i++){ 230 s = d[i].name; 231 id = strtol(s, &s, 10); 232 if(id <= max || *s != '\0' 233 || (d[i].mode & DMDIR) != DMDIR) 234 continue; 235 236 max = id; 237 238 while(msgs != nil){ 239 last = msgs; 240 msgs = msgs->next; 241 if(last->id == id) 242 goto continueDir; 243 last->expunged = 1; 244 } 245 246 new = 1; 247 m = MKZ(Msg); 248 m->id = id; 249 m->fsDir = box->fsDir; 250 m->fs = emalloc(2 * (MsgNameLen + 1)); 251 m->efs = seprint(m->fs, m->fs + (MsgNameLen + 1), "%lud/", id); 252 m->size = ~0UL; 253 m->lines = ~0UL; 254 m->prev = last; 255 m->flags = MRecent; 256 if(!msgInfo(m)) 257 freeMsg(m); 258 else{ 259 if(last == nil) 260 box->msgs = m; 261 else 262 last->next = m; 263 last = m; 264 } 265 continueDir:; 266 } 267 free(d); 268 } 269 close(fd); 270 for(; msgs != nil; msgs = msgs->next) 271 msgs->expunged = 1; 272 273 /* 274 * make up the imap message sequence numbers 275 */ 276 id = 1; 277 for(m = box->msgs; m != nil; m = m->next){ 278 if(m->seq && m->seq != id) 279 bye("internal error assigning message numbers"); 280 m->seq = id++; 281 } 282 box->max = id - 1; 283 284 return new; 285 } 286 287 /* 288 * read in the .imp file, or make one if it doesn't exist. 289 * make sure all flags and uids are consistent. 290 * return the mailbox lock. 291 */ 292 #define IMPMAGIC "imap internal mailbox description\n" 293 static MbLock* 294 openImp(Box *box, int new) 295 { 296 Qid qid; 297 Biobuf b; 298 MbLock *ml; 299 int fd; 300 //ZZZZ 301 int once; 302 303 ml = mbLock(); 304 if(ml == nil) 305 return nil; 306 fd = cdOpen(mboxDir, box->imp, OREAD); 307 once = 0; 308 ZZZhack: 309 if(fd < 0 || fqid(fd, &qid) < 0){ 310 if(fd < 0){ 311 char buf[ERRMAX]; 312 313 errstr(buf, sizeof buf); 314 if(cistrstr(buf, "does not exist") == nil) 315 fprint(2, "imap4d at %lud: imp open failed: %s\n", time(nil), buf); 316 if(!once && cistrstr(buf, "locked") != nil){ 317 once = 1; 318 fprint(2, "imap4d at %lud: imp %s/%s %s locked when it shouldn't be; spinning\n", time(nil), username, box->name, box->imp); 319 fd = openLocked(mboxDir, box->imp, OREAD); 320 goto ZZZhack; 321 } 322 } 323 if(fd >= 0) 324 close(fd); 325 fd = createImp(box, &qid); 326 if(fd < 0){ 327 mbUnlock(ml); 328 return nil; 329 } 330 box->dirtyImp = 1; 331 if(box->uidvalidity == 0) 332 box->uidvalidity = box->mtime; 333 box->impQid = qid; 334 new = 1; 335 }else if(qid.path != box->impQid.path || qid.vers != box->impQid.vers){ 336 Binit(&b, fd, OREAD); 337 if(!parseImp(&b, box)){ 338 box->dirtyImp = 1; 339 if(box->uidvalidity == 0) 340 box->uidvalidity = box->mtime; 341 } 342 Bterm(&b); 343 box->impQid = qid; 344 new = 1; 345 } 346 if(new) 347 boxFlags(box); 348 close(fd); 349 return ml; 350 } 351 352 /* 353 * close the .imp file, after writing out any changes 354 */ 355 void 356 closeImp(Box *box, MbLock *ml) 357 { 358 Msg *m; 359 Qid qid; 360 Biobuf b; 361 char buf[NFlags+1]; 362 int fd; 363 364 if(ml == nil) 365 return; 366 if(!box->dirtyImp){ 367 mbUnlock(ml); 368 return; 369 } 370 371 fd = cdCreate(mboxDir, box->imp, OWRITE, 0664); 372 if(fd < 0){ 373 mbUnlock(ml); 374 return; 375 } 376 Binit(&b, fd, OWRITE); 377 378 box->dirtyImp = 0; 379 Bprint(&b, "%s", IMPMAGIC); 380 Bprint(&b, "%.*lud %.*lud\n", NUid, box->uidvalidity, NUid, box->uidnext); 381 for(m = box->msgs; m != nil; m = m->next){ 382 if(m->expunged) 383 continue; 384 wrImpFlags(buf, m->flags, strcmp(box->fs, "imap") == 0); 385 Bprint(&b, "%.*s %.*lud %s\n", NDigest, m->info[IDigest], NUid, m->uid, buf); 386 } 387 Bterm(&b); 388 389 if(fqid(fd, &qid) >= 0) 390 box->impQid = qid; 391 close(fd); 392 mbUnlock(ml); 393 } 394 395 void 396 wrImpFlags(char *buf, int flags, int killRecent) 397 { 398 int i; 399 400 for(i = 0; i < NFlags; i++){ 401 if((flags & flagChars[i].v) 402 && (flagChars[i].v != MRecent || !killRecent)) 403 buf[i] = flagChars[i].name[0]; 404 else 405 buf[i] = '-'; 406 } 407 buf[i] = '\0'; 408 } 409 410 int 411 emptyImp(char *mbox) 412 { 413 Dir *d; 414 long mode; 415 int fd; 416 417 fd = cdCreate(mboxDir, impName(mbox), OWRITE, 0664); 418 if(fd < 0) 419 return -1; 420 d = cdDirstat(mboxDir, mbox); 421 if(d == nil){ 422 close(fd); 423 return -1; 424 } 425 fprint(fd, "%s%.*lud %.*lud\n", IMPMAGIC, NUid, d->mtime, NUid, 1UL); 426 mode = d->mode & 0777; 427 nulldir(d); 428 d->mode = mode; 429 dirfwstat(fd, d); 430 free(d); 431 return fd; 432 } 433 434 /* 435 * try to match permissions with mbox 436 */ 437 static int 438 createImp(Box *box, Qid *qid) 439 { 440 Dir *d; 441 long mode; 442 int fd; 443 444 fd = cdCreate(mboxDir, box->imp, OREAD, 0664); 445 if(fd < 0) 446 return -1; 447 d = cdDirstat(mboxDir, box->name); 448 if(d != nil){ 449 mode = d->mode & 0777; 450 nulldir(d); 451 d->mode = mode; 452 dirfwstat(fd, d); 453 free(d); 454 } 455 if(fqid(fd, qid) < 0){ 456 close(fd); 457 return -1; 458 } 459 460 return fd; 461 } 462 463 /* 464 * read or re-read a .imp file. 465 * this is tricky: 466 * messages can be deleted by another agent 467 * we might still have a Msg for an expunged message, 468 * because we haven't told the client yet. 469 * we can have a Msg without a .imp entry. 470 * flag information is added at the end of the .imp by copy & append 471 * there can be duplicate messages (same digests). 472 * 473 * look up existing messages based on uid. 474 * look up new messages based on in order digest matching. 475 * 476 * note: in the face of duplicate messages, one of which is deleted, 477 * two active servers may decide different ones are valid, and so return 478 * different uids for the messages. this situation will stablize when the servers exit. 479 */ 480 static int 481 parseImp(Biobuf *b, Box *box) 482 { 483 Msg *m, *mm; 484 char *s, *t, *toks[3]; 485 ulong uid, u; 486 int match, n; 487 488 m = box->msgs; 489 s = Brdline(b, '\n'); 490 if(s == nil || Blinelen(b) != STRLEN(IMPMAGIC) 491 || strncmp(s, IMPMAGIC, STRLEN(IMPMAGIC)) != 0) 492 return 0; 493 494 s = Brdline(b, '\n'); 495 if(s == nil || Blinelen(b) != 2*NUid + 2) 496 return 0; 497 s[2*NUid + 1] = '\0'; 498 u = strtoul(s, &t, 10); 499 if(u != box->uidvalidity && box->uidvalidity != 0) 500 return 0; 501 box->uidvalidity = u; 502 if(*t != ' ' || t != s + NUid) 503 return 0; 504 t++; 505 u = strtoul(t, &t, 10); 506 if(box->uidnext > u) 507 return 0; 508 box->uidnext = u; 509 if(t != s + 2*NUid+1 || box->uidnext == 0) 510 return 0; 511 512 uid = ~0; 513 while(m != nil){ 514 s = Brdline(b, '\n'); 515 if(s == nil) 516 break; 517 n = Blinelen(b) - 1; 518 if(n != NDigest + NUid + NFlags + 2 519 || s[NDigest] != ' ' || s[NDigest + NUid + 1] != ' ') 520 return 0; 521 toks[0] = s; 522 s[NDigest] = '\0'; 523 toks[1] = s + NDigest + 1; 524 s[NDigest + NUid + 1] = '\0'; 525 toks[2] = s + NDigest + NUid + 2; 526 s[n] = '\0'; 527 t = toks[1]; 528 u = strtoul(t, &t, 10); 529 if(*t != '\0' || uid != ~0 && (uid >= u && u || u && !uid)) 530 return 0; 531 uid = u; 532 533 /* 534 * zero uid => added by append or copy, only flags valid 535 * can only match messages without uids, but this message 536 * may not be the next one, and may have been deleted. 537 */ 538 if(!uid){ 539 for(; m != nil && m->uid; m = m->next) 540 ; 541 for(mm = m; mm != nil; mm = mm->next){ 542 if(strcmp(mm->info[IDigest], toks[0]) == 0){ 543 if(!mm->uid) 544 mm->flags = 0; 545 if(!impFlags(box, mm, toks[2])) 546 return 0; 547 m = mm->next; 548 break; 549 } 550 } 551 continue; 552 } 553 554 /* 555 * ignore expunged messages, 556 * and messages already assigned uids which don't match this uid. 557 * such messages must have been deleted by another imap server, 558 * which updated the mailbox and .imp file since we read the mailbox, 559 * or because upas/fs got confused by consecutive duplicate messages, 560 * the first of which was deleted by another imap server. 561 */ 562 for(; m != nil && (m->expunged || m->uid && m->uid < uid); m = m->next) 563 ; 564 if(m == nil) 565 break; 566 567 /* 568 * only check for digest match on the next message, 569 * since it comes before all other messages, and therefore 570 * must be in the .imp file if they should be. 571 */ 572 match = strcmp(m->info[IDigest], toks[0]) == 0; 573 if(uid && (m->uid == uid || !m->uid && match)){ 574 if(!match) 575 bye("inconsistent uid"); 576 577 /* 578 * wipe out recent flag if some other server saw this new message. 579 * it will be read from the .imp file if is really should be set, 580 * ie the message was only seen by a status command. 581 */ 582 if(!m->uid) 583 m->flags = 0; 584 585 if(!impFlags(box, m, toks[2])) 586 return 0; 587 m->uid = uid; 588 m = m->next; 589 } 590 } 591 return 1; 592 } 593 594 /* 595 * parse .imp flags 596 */ 597 static int 598 impFlags(Box *box, Msg *m, char *flags) 599 { 600 int i, f; 601 602 f = 0; 603 for(i = 0; i < NFlags; i++){ 604 if(flags[i] == '-') 605 continue; 606 if(flags[i] != flagChars[i].name[0]) 607 return 0; 608 f |= flagChars[i].v; 609 } 610 611 /* 612 * recent flags are set until the first time message's box is selected or examined. 613 * it may be stored in the file as a side effect of a status or subscribe command; 614 * if so, clear it out. 615 */ 616 if((f & MRecent) && strcmp(box->fs, "imap") == 0) 617 box->dirtyImp = 1; 618 f |= m->flags & MRecent; 619 620 /* 621 * all old messages with changed flags should be reported to the client 622 */ 623 if(m->uid && m->flags != f){ 624 box->sendFlags = 1; 625 m->sendFlags = 1; 626 } 627 m->flags = f; 628 return 1; 629 } 630 631 /* 632 * assign uids to any new messages 633 * which aren't already in the .imp file. 634 * sum up totals for flag values. 635 */ 636 static void 637 boxFlags(Box *box) 638 { 639 Msg *m; 640 641 box->recent = 0; 642 for(m = box->msgs; m != nil; m = m->next){ 643 if(m->uid == 0){ 644 box->dirtyImp = 1; 645 box->uidnext = uidRenumber(m, box->uidnext, 0); 646 } 647 if(m->flags & MRecent) 648 box->recent++; 649 } 650 } 651 652 static ulong 653 uidRenumber(Msg *m, ulong uid, int force) 654 { 655 for(; m != nil; m = m->next){ 656 if(!force && m->uid != 0) 657 bye("uid renumbering with a valid uid"); 658 m->uid = uid++; 659 } 660 return uid; 661 } 662 663 void 664 closeBox(Box *box, int opened) 665 { 666 Msg *m, *next; 667 668 /* 669 * make sure to leave the mailbox directory so upas/fs can close the mailbox 670 */ 671 myChdir(mboxDir); 672 673 if(box->writable){ 674 deleteMsgs(box); 675 if(expungeMsgs(box, 0)) 676 closeImp(box, checkBox(box, 1)); 677 } 678 679 if(fprint(fsCtl, "close %s", box->fs) < 0 && opened) 680 bye("can't talk to mail server"); 681 for(m = box->msgs; m != nil; m = next){ 682 next = m->next; 683 freeMsg(m); 684 } 685 free(box->name); 686 free(box->fs); 687 free(box->fsDir); 688 free(box->imp); 689 free(box); 690 } 691 692 int 693 deleteMsgs(Box *box) 694 { 695 Msg *m; 696 char buf[BufSize], *p, *start; 697 int ok; 698 699 if(!box->writable) 700 return 0; 701 702 /* 703 * first pass: delete messages; gang the writes together for speed. 704 */ 705 ok = 1; 706 start = seprint(buf, buf + sizeof(buf), "delete %s", box->fs); 707 p = start; 708 for(m = box->msgs; m != nil; m = m->next){ 709 if((m->flags & MDeleted) && !m->expunged){ 710 m->expunged = 1; 711 p = seprint(p, buf + sizeof(buf), " %lud", m->id); 712 if(p + 32 >= buf + sizeof(buf)){ 713 if(write(fsCtl, buf, p - buf) < 0) 714 bye("can't talk to mail server"); 715 p = start; 716 } 717 } 718 } 719 if(p != start && write(fsCtl, buf, p - buf) < 0) 720 bye("can't talk to mail server"); 721 722 return ok; 723 } 724 725 /* 726 * second pass: remove the message structure, 727 * and renumber message sequence numbers. 728 * update messages counts in mailbox. 729 * returns true if anything changed. 730 */ 731 int 732 expungeMsgs(Box *box, int send) 733 { 734 Msg *m, *next, *last; 735 ulong n; 736 737 n = 0; 738 last = nil; 739 for(m = box->msgs; m != nil; m = next){ 740 m->seq -= n; 741 next = m->next; 742 if(m->expunged){ 743 if(send) 744 Bprint(&bout, "* %lud expunge\r\n", m->seq); 745 if(m->flags & MRecent) 746 box->recent--; 747 n++; 748 if(last == nil) 749 box->msgs = next; 750 else 751 last->next = next; 752 freeMsg(m); 753 }else 754 last = m; 755 } 756 if(n){ 757 box->max -= n; 758 box->dirtyImp = 1; 759 } 760 return n; 761 } 762 763 static void 764 fsInit(void) 765 { 766 if(fsCtl >= 0) 767 return; 768 fsCtl = open("/mail/fs/ctl", ORDWR); 769 if(fsCtl < 0) 770 bye("can't open mail file system"); 771 if(fprint(fsCtl, "close mbox") < 0) 772 bye("can't initialize mail file system"); 773 } 774 775 static char *stoplist[] = 776 { 777 "mbox", 778 "pipeto", 779 "forward", 780 "names", 781 0 782 }; 783 784 /* 785 * reject bad mailboxes based on mailbox name 786 */ 787 int 788 okMbox(char *path) 789 { 790 char *name; 791 int i; 792 793 name = strrchr(path, '/'); 794 if(name == nil) 795 name = path; 796 else 797 name++; 798 if(strlen(name) + STRLEN(".imp") >= MboxNameLen) 799 return 0; 800 for(i = 0; stoplist[i]; i++) 801 if(strcmp(name, stoplist[i]) == 0) 802 return 0; 803 if(isprefix("L.", name) || isprefix("imap-tmp.", name) 804 || issuffix(".imp", name) 805 || strcmp("imap.subscribed", name) == 0 806 || isdotdot(name) || name[0] == '/') 807 return 0; 808 809 return 1; 810 811 } 812