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