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