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