1 #include "common.h" 2 #include <ctype.h> 3 #include <plumb.h> 4 #include <libsec.h> 5 #include <auth.h> 6 #include "dat.h" 7 8 #pragma varargck argpos imap4cmd 2 9 #pragma varargck type "Z" char* 10 11 int doublequote(Fmt*); 12 13 static char Eio[] = "i/o error"; 14 15 typedef struct Imap Imap; 16 struct Imap { 17 char *freep; // free this to free the strings below 18 19 char *host; 20 char *user; 21 char *mbox; 22 23 int mustssl; 24 int refreshtime; 25 int debug; 26 27 ulong tag; 28 ulong validity; 29 int nmsg; 30 int size; 31 char *base; 32 char *data; 33 34 vlong *uid; 35 int nuid; 36 int muid; 37 38 Thumbprint *thumb; 39 40 // open network connection 41 Biobuf bin; 42 Biobuf bout; 43 int fd; 44 }; 45 46 static char* 47 removecr(char *s) 48 { 49 char *r, *w; 50 51 for(r=w=s; *r; r++) 52 if(*r != '\r') 53 *w++ = *r; 54 *w = '\0'; 55 return s; 56 } 57 58 // 59 // send imap4 command 60 // 61 static void 62 imap4cmd(Imap *imap, char *fmt, ...) 63 { 64 char buf[128], *p; 65 va_list va; 66 67 va_start(va, fmt); 68 p = buf+sprint(buf, "9X%lud ", imap->tag); 69 vseprint(p, buf+sizeof(buf), fmt, va); 70 va_end(va); 71 72 p = buf+strlen(buf); 73 if(p > (buf+sizeof(buf)-3)) 74 sysfatal("imap4 command too long"); 75 76 if(imap->debug) 77 fprint(2, "-> %s\n", buf); 78 strcpy(p, "\r\n"); 79 Bwrite(&imap->bout, buf, strlen(buf)); 80 Bflush(&imap->bout); 81 } 82 83 enum { 84 OK, 85 NO, 86 BAD, 87 BYE, 88 EXISTS, 89 STATUS, 90 FETCH, 91 UNKNOWN, 92 }; 93 94 static char *verblist[] = { 95 [OK] "OK", 96 [NO] "NO", 97 [BAD] "BAD", 98 [BYE] "BYE", 99 [EXISTS] "EXISTS", 100 [STATUS] "STATUS", 101 [FETCH] "FETCH", 102 }; 103 104 static int 105 verbcode(char *verb) 106 { 107 int i; 108 char *q; 109 110 if(q = strchr(verb, ' ')) 111 *q = '\0'; 112 113 for(i=0; i<nelem(verblist); i++) 114 if(verblist[i] && strcmp(verblist[i], verb)==0){ 115 if(q) 116 *q = ' '; 117 return i; 118 } 119 if(q) 120 *q = ' '; 121 return UNKNOWN; 122 } 123 124 static void 125 strupr(char *s) 126 { 127 for(; *s; s++) 128 if('a' <= *s && *s <= 'z') 129 *s += 'A'-'a'; 130 } 131 132 133 // 134 // get imap4 response line. there might be various 135 // data or other informational lines mixed in. 136 // 137 static char* 138 imap4resp(Imap *imap) 139 { 140 char *line, *p, *ep, *op, *q, *r, *en, *verb; 141 int n; 142 143 while(p = Brdline(&imap->bin, '\n')){ 144 ep = p+Blinelen(&imap->bin); 145 while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r')) 146 *--ep = '\0'; 147 148 if(imap->debug) 149 fprint(2, "<- %s\n", p); 150 strupr(p); 151 152 switch(p[0]){ 153 case '+': 154 if(imap->tag == 0) 155 fprint(2, "unexpected: %s\n", p); 156 break; 157 158 // ``unsolicited'' information; everything happens here. 159 case '*': 160 if(p[1]!=' ') 161 continue; 162 p += 2; 163 line = p; 164 n = strtol(p, &p, 10); 165 if(*p==' ') 166 p++; 167 verb = p; 168 169 if(p = strchr(verb, ' ')) 170 p++; 171 else 172 p = verb+strlen(verb); 173 174 switch(verbcode(verb)){ 175 case OK: 176 case NO: 177 case BAD: 178 // human readable text at p; 179 break; 180 case BYE: 181 // early disconnect 182 // human readable text at p; 183 break; 184 185 // * 32 EXISTS 186 case EXISTS: 187 imap->nmsg = n; 188 break; 189 190 // * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964) 191 case STATUS: 192 if(q = strstr(p, "MESSAGES")) 193 imap->nmsg = atoi(q+8); 194 if(q = strstr(p, "UIDVALIDITY")) 195 imap->validity = strtoul(q+11, 0, 10); 196 break; 197 198 case FETCH: 199 // * 1 FETCH (UID 1 RFC822.SIZE 511) 200 if(q=strstr(p, "RFC822.SIZE")){ 201 imap->size = atoi(q+11); 202 break; 203 } 204 205 // * 1 FETCH (UID 1 RFC822.HEADER {496} 206 // <496 bytes of data> 207 // ) 208 // * 1 FETCH (UID 1 RFC822.HEADER "data") 209 if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){ 210 if((q = strchr(p, '{')) 211 && (n=strtol(q+1, &en, 0), *en=='}')){ 212 if(imap->data == nil){ 213 imap->base = emalloc(n+1); 214 imap->data = imap->base; 215 imap->size = n+1; 216 } 217 if(n >= imap->size) 218 return Eio; 219 if(Bread(&imap->bin, imap->data, n) != n) 220 return Eio; 221 imap->data[n] = '\0'; 222 imap->data += n; 223 imap->size -= n; 224 Brdline(&imap->bin, '\n'); 225 }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){ 226 *r = '\0'; 227 q++; 228 n = r-q; 229 if(imap->data == nil){ 230 imap->base = emalloc(n+1); 231 imap->data = imap->base; 232 imap->size = n+1; 233 } 234 if(n >= imap->size) 235 return Eio; 236 memmove(imap->data, q, n); 237 imap->data[n] = '\0'; 238 imap->data += n; 239 imap->size -= n; 240 }else 241 return "confused about FETCH response"; 242 break; 243 } 244 245 // * 1 FETCH (UID 1) 246 // * 2 FETCH (UID 6) 247 if(q = strstr(p, "UID")){ 248 if(imap->nuid < imap->muid) 249 imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10); 250 break; 251 } 252 } 253 254 if(imap->tag == 0) 255 return line; 256 break; 257 258 case '9': // response to our message 259 op = p; 260 if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){ 261 while(*p==' ') 262 p++; 263 imap->tag++; 264 return p; 265 } 266 fprint(2, "expected %lud; got %s\n", imap->tag, op); 267 break; 268 269 default: 270 fprint(2, "unexpected line: %s\n", p); 271 } 272 } 273 return Eio; 274 } 275 276 static int 277 isokay(char *resp) 278 { 279 return strncmp(resp, "OK", 2)==0; 280 } 281 282 // 283 // log in to IMAP4 server, select mailbox, no SSL at the moment 284 // 285 static char* 286 imap4login(Imap *imap) 287 { 288 char *s; 289 UserPasswd *up; 290 291 imap->tag = 0; 292 s = imap4resp(imap); 293 if(!isokay(s)) 294 return "error in initial IMAP handshake"; 295 296 if(imap->user != nil) 297 up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user); 298 else 299 up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host); 300 if(up == nil) 301 return "cannot find IMAP password"; 302 303 imap->tag = 1; 304 imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd); 305 free(up); 306 if(!isokay(s = imap4resp(imap))) 307 return s; 308 309 imap4cmd(imap, "SELECT %Z", imap->mbox); 310 if(!isokay(s = imap4resp(imap))) 311 return s; 312 313 return nil; 314 } 315 316 // 317 // push tls onto a connection 318 // 319 int 320 mypushtls(int fd) 321 { 322 int p[2]; 323 char buf[10]; 324 325 if(pipe(p) < 0) 326 return -1; 327 328 switch(fork()){ 329 case -1: 330 close(p[0]); 331 close(p[1]); 332 return -1; 333 case 0: 334 close(p[1]); 335 dup(p[0], 0); 336 dup(p[0], 1); 337 sprint(buf, "/fd/%d", fd); 338 execl("/bin/tlsrelay", "tlsrelay", "-f", buf, nil); 339 _exits(nil); 340 default: 341 break; 342 } 343 close(fd); 344 close(p[0]); 345 return p[1]; 346 } 347 348 // 349 // dial and handshake with the imap server 350 // 351 static char* 352 imap4dial(Imap *imap) 353 { 354 char *err, *port; 355 uchar digest[SHA1dlen]; 356 int sfd; 357 TLSconn conn; 358 359 if(imap->fd >= 0){ 360 imap4cmd(imap, "noop"); 361 if(isokay(imap4resp(imap))) 362 return nil; 363 close(imap->fd); 364 imap->fd = -1; 365 } 366 367 if(imap->mustssl) 368 port = "imaps"; 369 else 370 port = "imap4"; 371 372 if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0) 373 return geterrstr(); 374 375 if(imap->mustssl){ 376 memset(&conn, 0, sizeof conn); 377 sfd = tlsClient(imap->fd, &conn); 378 if(sfd < 0) 379 sysfatal("tlsClient: %r"); 380 if(conn.cert==nil || conn.certlen <= 0) 381 sysfatal("server did not provide TLS certificate"); 382 sha1(conn.cert, conn.certlen, digest, nil); 383 if(!imap->thumb || !okThumbprint(digest, imap->thumb)){ 384 fmtinstall('H', encodefmt); 385 sysfatal("server certificate %.*H not recognized", SHA1dlen, digest); 386 } 387 free(conn.cert); 388 close(imap->fd); 389 imap->fd = sfd; 390 } 391 Binit(&imap->bin, imap->fd, OREAD); 392 Binit(&imap->bout, imap->fd, OWRITE); 393 394 if(err = imap4login(imap)) { 395 close(imap->fd); 396 return err; 397 } 398 399 return nil; 400 } 401 402 // 403 // close connection 404 // 405 static void 406 imap4hangup(Imap *imap) 407 { 408 imap4cmd(imap, "LOGOUT"); 409 imap4resp(imap); 410 close(imap->fd); 411 } 412 413 // 414 // download just the header of a message 415 // because we do this, the sha1 digests are only of 416 // the headers. hopefully this won't cause problems. 417 // 418 // this doesn't work (and isn't used). the downloading 419 // itself works, but we need to have the full mime headers 420 // to get the right recursive structures into the file system, 421 // which means having the body. i've also had other weird 422 // problems with bodies being paged in incorrectly. 423 // 424 // this is here as a start in case someone else wants a crack 425 // at it. note that if you start using this you'll have to 426 // change the digest in imap4fetch() to digest just the header. 427 // 428 static char* 429 imap4fetchheader(Imap *imap, Mailbox *mb, Message *m) 430 { 431 int i; 432 char *p, *s, sdigest[2*SHA1dlen+1]; 433 434 imap->size = 0; 435 free(imap->base); 436 imap->base = nil; 437 imap->data = nil; 438 439 imap4cmd(imap, "UID FETCH %lud RFC822.HEADER", (ulong)m->imapuid); 440 if(!isokay(s = imap4resp(imap))) 441 return s; 442 if(imap->base == nil) 443 return "didn't get header"; 444 445 p = imap->base; 446 removecr(p); 447 free(m->start); 448 m->start = p; 449 m->end = p+strlen(p); 450 m->bend = m->rbend = m->end; 451 m->header = m->start; 452 //m->fetched = 0; 453 454 imap->base = nil; 455 imap->data = nil; 456 457 parseheaders(m, 0, mb); 458 459 // digest headers 460 sha1((uchar*)m->start, m->hend - m->start, m->digest, nil); 461 for(i = 0; i < SHA1dlen; i++) 462 sprint(sdigest+2*i, "%2.2ux", m->digest[i]); 463 m->sdigest = s_copy(sdigest); 464 465 return nil; 466 } 467 468 // 469 // download a single message 470 // 471 static char* 472 imap4fetch(Mailbox *mb, Message *m) 473 { 474 int i, sz; 475 char *p, *s, sdigest[2*SHA1dlen+1]; 476 Imap *imap; 477 478 imap = mb->aux; 479 480 // if(s = imap4dial(imap)) 481 // return s; 482 // 483 // imap4cmd(imap, "STATUS %s (MESSAGES UIDVALIDITY)", imap->mbox); 484 // if(!isokay(s = imap4resp(imap))) 485 // return s; 486 // if((ulong)(m->imapuid>>32) != imap->validity) 487 // return "uids changed underfoot"; 488 489 imap->size = 0; 490 /* SIZE */ 491 if(!isokay(s = imap4resp(imap))) 492 return s; 493 if(imap->size == 0) 494 return "didn't get size from size command"; 495 496 sz = imap->size+200; /* 200: slop */ 497 p = emalloc(sz+1); 498 free(imap->base); 499 imap->base = p; 500 imap->data = p; 501 imap->size = sz; 502 503 /* HEADER */ 504 if(!isokay(s = imap4resp(imap))) 505 return s; 506 if(imap->size == sz){ 507 free(p); 508 imap->data = nil; 509 return "didn't get header"; 510 } 511 512 /* TEXT */ 513 if(!isokay(s = imap4resp(imap))) 514 return s; 515 516 removecr(p); 517 free(m->start); 518 m->start = p; 519 m->end = p+strlen(p); 520 m->bend = m->rbend = m->end; 521 m->header = m->start; 522 //m->fetched = 1; 523 524 imap->base = nil; 525 imap->data = nil; 526 527 parse(m, 0, mb); 528 529 // digest headers 530 sha1((uchar*)m->start, m->end - m->start, m->digest, nil); 531 for(i = 0; i < SHA1dlen; i++) 532 sprint(sdigest+2*i, "%2.2ux", m->digest[i]); 533 m->sdigest = s_copy(sdigest); 534 535 return nil; 536 } 537 538 // 539 // check for new messages on imap4 server 540 // download new messages, mark deleted messages 541 // 542 static char* 543 imap4read(Imap *imap, Mailbox *mb, int doplumb) 544 { 545 char *s; 546 int i, ignore, nnew, t; 547 Message *m, *next, **l; 548 549 imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox); 550 if(!isokay(s = imap4resp(imap))) 551 return s; 552 553 imap->nuid = 0; 554 imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0])); 555 imap->muid = imap->nmsg; 556 557 if(imap->nmsg > 0){ 558 imap4cmd(imap, "UID FETCH 1:* UID"); 559 if(!isokay(s = imap4resp(imap))) 560 return s; 561 } 562 563 l = &mb->root->part; 564 for(i=0; i<imap->nuid; i++){ 565 ignore = 0; 566 while(*l != nil){ 567 if((*l)->imapuid == imap->uid[i]){ 568 ignore = 1; 569 l = &(*l)->next; 570 break; 571 }else{ 572 // old mail, we don't have it anymore 573 if(doplumb) 574 mailplumb(mb, *l, 1); 575 (*l)->inmbox = 0; 576 (*l)->deleted = 1; 577 l = &(*l)->next; 578 } 579 } 580 if(ignore) 581 continue; 582 583 // new message 584 m = newmessage(mb->root); 585 m->mallocd = 1; 586 m->inmbox = 1; 587 m->imapuid = imap->uid[i]; 588 589 // add to chain, will download soon 590 *l = m; 591 l = &m->next; 592 } 593 594 // whatever is left at the end of the chain is gone 595 while(*l != nil){ 596 if(doplumb) 597 mailplumb(mb, *l, 1); 598 (*l)->inmbox = 0; 599 (*l)->deleted = 1; 600 l = &(*l)->next; 601 } 602 603 // download new messages 604 t = imap->tag; 605 switch(rfork(RFPROC|RFMEM)){ 606 case -1: 607 sysfatal("rfork: %r"); 608 default: 609 break; 610 case 0: 611 for(m = mb->root->part; m != nil; m = m->next){ 612 if(m->start != nil) 613 continue; 614 Bprint(&imap->bout, "9X%d UID FETCH %lud RFC822.SIZE\r\n", t++, (ulong)m->imapuid); 615 Bprint(&imap->bout, "9X%d UID FETCH %lud RFC822.HEADER\r\n", t++, (ulong)m->imapuid); 616 Bprint(&imap->bout, "9X%d UID FETCH %lud RFC822.TEXT\r\n", t++, (ulong)m->imapuid); 617 } 618 Bflush(&imap->bout); 619 _exits(nil); 620 } 621 622 nnew = 0; 623 for(m=mb->root->part; m!=nil; m=next){ 624 next = m->next; 625 if(m->start != nil) 626 continue; 627 628 if(s = imap4fetch(mb, m)){ 629 // message disappeared? unchain 630 fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s); 631 delmessage(mb, m); 632 mb->root->subname--; 633 continue; 634 } 635 nnew++; 636 if(doplumb) 637 mailplumb(mb, m, 0); 638 } 639 waitpid(); 640 641 if(nnew){ 642 mb->vers++; 643 henter(PATH(0, Qtop), mb->name, 644 (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb); 645 } 646 return nil; 647 } 648 649 // 650 // sync mailbox 651 // 652 static void 653 imap4purge(Imap *imap, Mailbox *mb) 654 { 655 int ndel; 656 Message *m, *next; 657 658 ndel = 0; 659 for(m=mb->root->part; m!=nil; m=next){ 660 next = m->next; 661 if(m->deleted && m->refs==0){ 662 if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){ 663 imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid); 664 if(isokay(imap4resp(imap))){ 665 ndel++; 666 delmessage(mb, m); 667 } 668 }else 669 delmessage(mb, m); 670 } 671 } 672 673 if(ndel){ 674 imap4cmd(imap, "EXPUNGE"); 675 imap4resp(imap); 676 } 677 } 678 679 // 680 // connect to imap4 server, sync mailbox 681 // 682 static char* 683 imap4sync(Mailbox *mb, int doplumb) 684 { 685 char *err; 686 Imap *imap; 687 688 imap = mb->aux; 689 690 if(err = imap4dial(imap)){ 691 mb->waketime = time(0) + imap->refreshtime; 692 return err; 693 } 694 695 if((err = imap4read(imap, mb, doplumb)) == nil){ 696 imap4purge(imap, mb); 697 mb->d->atime = mb->d->mtime = time(0); 698 } 699 /* 700 * don't hang up; leave connection open for next time. 701 */ 702 // imap4hangup(imap); 703 mb->waketime = time(0) + imap->refreshtime; 704 return err; 705 } 706 707 static char Eimap4ctl[] = "bad imap4 control message"; 708 709 static char* 710 imap4ctl(Mailbox *mb, int argc, char **argv) 711 { 712 int n; 713 Imap *imap; 714 715 imap = mb->aux; 716 if(argc < 1) 717 return Eimap4ctl; 718 719 if(argc==1 && strcmp(argv[0], "debug")==0){ 720 imap->debug = 1; 721 return nil; 722 } 723 724 if(argc==1 && strcmp(argv[0], "nodebug")==0){ 725 imap->debug = 0; 726 return nil; 727 } 728 729 if(argc==1 && strcmp(argv[0], "thumbprint")==0){ 730 if(imap->thumb) 731 freeThumbprints(imap->thumb); 732 imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); 733 } 734 if(strcmp(argv[0], "refresh")==0){ 735 if(argc==1){ 736 imap->refreshtime = 60; 737 return nil; 738 } 739 if(argc==2){ 740 n = atoi(argv[1]); 741 if(n < 15) 742 return Eimap4ctl; 743 imap->refreshtime = n; 744 return nil; 745 } 746 } 747 748 return Eimap4ctl; 749 } 750 751 // 752 // free extra memory associated with mb 753 // 754 static void 755 imap4close(Mailbox *mb) 756 { 757 Imap *imap; 758 759 imap = mb->aux; 760 free(imap->freep); 761 free(imap->base); 762 free(imap->uid); 763 free(imap); 764 } 765 766 // 767 // open mailboxes of the form /imap/host/user 768 // 769 char* 770 imap4mbox(Mailbox *mb, char *path) 771 { 772 char *f[10]; 773 int mustssl, nf; 774 Imap *imap; 775 776 quotefmtinstall(); 777 fmtinstall('Z', doublequote); 778 if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0) 779 return Enotme; 780 mustssl = (strncmp(path, "/imaps/", 7) == 0); 781 782 path = strdup(path); 783 if(path == nil) 784 return "out of memory"; 785 786 nf = getfields(path, f, 5, 0, "/"); 787 if(nf < 3){ 788 free(path); 789 return "bad imap path syntax /imap[s]/system[/user[/mailbox]]"; 790 } 791 792 imap = emalloc(sizeof(*imap)); 793 imap->fd = -1; 794 imap->debug = 0; 795 imap->freep = path; 796 imap->mustssl = mustssl; 797 imap->host = f[2]; 798 if(nf < 4) 799 imap->user = nil; 800 else 801 imap->user = f[3]; 802 if(nf < 5) 803 imap->mbox = "Inbox"; 804 else 805 imap->mbox = f[4]; 806 imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); 807 808 mb->aux = imap; 809 mb->sync = imap4sync; 810 mb->close = imap4close; 811 mb->ctl = imap4ctl; 812 mb->d = emalloc(sizeof(*mb->d)); 813 //mb->fetch = imap4fetch; 814 815 return nil; 816 } 817 818 // 819 // Formatter for %" 820 // Use double quotes to protect white space, frogs, \ and " 821 // 822 enum 823 { 824 Qok = 0, 825 Qquote, 826 Qbackslash, 827 }; 828 829 static int 830 needtoquote(Rune r) 831 { 832 if(r >= Runeself) 833 return Qquote; 834 if(r <= ' ') 835 return Qquote; 836 if(r=='\\' || r=='"') 837 return Qbackslash; 838 return Qok; 839 } 840 841 int 842 doublequote(Fmt *f) 843 { 844 char *s, *t; 845 int w, quotes; 846 Rune r; 847 848 s = va_arg(f->args, char*); 849 if(s == nil || *s == '\0') 850 return fmtstrcpy(f, "\"\""); 851 852 quotes = 0; 853 for(t=s; *t; t+=w){ 854 w = chartorune(&r, t); 855 quotes |= needtoquote(r); 856 } 857 if(quotes == 0) 858 return fmtstrcpy(f, s); 859 860 fmtrune(f, '"'); 861 for(t=s; *t; t+=w){ 862 w = chartorune(&r, t); 863 if(needtoquote(r) == Qbackslash) 864 fmtrune(f, '\\'); 865 fmtrune(f, r); 866 } 867 return fmtrune(f, '"'); 868 } 869