Blob


1 #include "common.h"
2 #include <ctype.h>
3 #include <plumb.h>
4 #include <libsec.h>
5 #include <auth.h>
6 #include "dat.h"
8 #pragma varargck argpos imap4cmd 2
9 #pragma varargck type "Z" char*
11 int doublequote(Fmt*);
12 int pipeline = 1;
14 /* static char Eio[] = "i/o error"; jpc */
16 typedef struct Imap Imap;
17 struct Imap {
18 char *freep; /* free this to free the strings below */
20 char *host;
21 char *user;
22 char *mbox;
24 int mustssl;
25 int refreshtime;
26 int debug;
28 ulong tag;
29 ulong validity;
30 int nmsg;
31 int size;
32 char *base;
33 char *data;
35 vlong *uid;
36 int nuid;
37 int muid;
39 Thumbprint *thumb;
41 /* open network connection */
42 Biobuf bin;
43 Biobuf bout;
44 int fd;
45 };
47 static char*
48 removecr(char *s)
49 {
50 char *r, *w;
52 for(r=w=s; *r; r++)
53 if(*r != '\r')
54 *w++ = *r;
55 *w = '\0';
56 return s;
57 }
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;
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);
73 p = buf+strlen(buf);
74 if(p > (buf+sizeof(buf)-3))
75 sysfatal("imap4 command too long");
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 }
84 enum {
85 OK,
86 NO,
87 BAD,
88 BYE,
89 EXISTS,
90 STATUS,
91 FETCH,
92 UNKNOWN
93 };
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 };
105 static int
106 verbcode(char *verb)
108 int i;
109 char *q;
111 if(q = strchr(verb, ' '))
112 *q = '\0';
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;
120 if(q)
121 *q = ' ';
122 return UNKNOWN;
125 static void
126 strupr(char *s)
128 for(; *s; s++)
129 if('a' <= *s && *s <= 'z')
130 *s += 'A'-'a';
133 static void
134 imapgrow(Imap *imap, int n)
136 int i;
138 if(imap->data == nil){
139 imap->base = emalloc(n+1);
140 imap->data = imap->base;
141 imap->size = n+1;
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;
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)
160 char *line, *p, *ep, *op, *q, *r, *en, *verb;
161 int i, n;
162 static char error[256];
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';
169 if(imap->debug)
170 fprint(2, "<- %s\n", p);
171 strupr(p);
173 switch(p[0]){
174 case '+':
175 if(imap->tag == 0)
176 fprint(2, "unexpected: %s\n", p);
177 break;
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;
190 if(p = strchr(verb, ' '))
191 p++;
192 else
193 p = verb+strlen(verb);
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;
206 /* * 32 EXISTS */
207 case EXISTS:
208 imap->nmsg = n;
209 break;
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;
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;
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;
260 /* * 1 FETCH (UID 1 RFC822.SIZE 511) */
261 if(q=strstr(p, "RFC822.SIZE")){
262 imap->size = atoi(q+11);
263 break;
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;
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;
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;
316 if(imap->tag == 0)
317 return line;
318 break;
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;
328 fprint(2, "expected %lud; got %s\n", imap->tag, op);
329 break;
331 default:
332 if(imap->debug || *p)
333 fprint(2, "unexpected line: %s\n", p);
336 snprint(error, sizeof error, "i/o error: %r\n");
337 return error;
340 static int
341 isokay(char *resp)
343 return strncmp(resp, "OK", 2)==0;
346 /* */
347 /* log in to IMAP4 server, select mailbox, no SSL at the moment */
348 /* */
349 static char*
350 imap4login(Imap *imap)
352 char *s;
353 UserPasswd *up;
355 imap->tag = 0;
356 s = imap4resp(imap);
357 if(!isokay(s))
358 return "error in initial IMAP handshake";
360 if(imap->user != nil)
361 up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q user=%q", imap->host, imap->user);
362 else
363 up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", imap->host);
364 if(up == nil)
365 return "cannot find IMAP password";
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;
373 imap4cmd(imap, "SELECT %Z", imap->mbox);
374 if(!isokay(s = imap4resp(imap)))
375 return s;
377 return nil;
380 /* */
381 /* push tls onto a connection */
382 /* */
383 int
384 mypushtls(int fd)
386 int p[2];
387 char buf[10];
389 if(pipe(p) < 0)
390 return -1;
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;
407 close(fd);
408 close(p[0]);
409 return p[1];
412 /* */
413 /* dial and handshake with the imap server */
414 /* */
415 static char*
416 imap4dial(Imap *imap)
418 char *err, *port;
419 uchar digest[SHA1dlen];
420 int sfd;
421 TLSconn conn;
423 if(imap->fd >= 0){
424 imap4cmd(imap, "noop");
425 if(isokay(imap4resp(imap)))
426 return nil;
427 close(imap->fd);
428 imap->fd = -1;
431 if(imap->mustssl)
432 port = "imaps";
433 else
434 port = "imap";
436 if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
437 return geterrstr();
439 if(imap->mustssl){
440 memset(&conn, 0, sizeof conn);
441 sfd = tlsClient(imap->fd, &conn);
442 if(sfd < 0)
443 sysfatal("tlsClient: %r");
444 if(conn.cert==nil || conn.certlen <= 0)
445 sysfatal("server did not provide TLS certificate");
446 sha1(conn.cert, conn.certlen, digest, nil);
447 if(!imap->thumb || !okThumbprint(digest, imap->thumb)){
448 fmtinstall('H', encodefmt);
449 sysfatal("server certificate %.*H not recognized", SHA1dlen, digest);
451 free(conn.cert);
452 close(imap->fd);
453 imap->fd = sfd;
455 if(imap->debug){
456 char fn[128];
457 int fd;
459 snprint(fn, sizeof fn, "%s/ctl", conn.dir);
460 fd = open(fn, ORDWR);
461 if(fd < 0)
462 fprint(2, "opening ctl: %r\n");
463 if(fprint(fd, "debug") < 0)
464 fprint(2, "writing ctl: %r\n");
465 close(fd);
468 Binit(&imap->bin, imap->fd, OREAD);
469 Binit(&imap->bout, imap->fd, OWRITE);
471 if(err = imap4login(imap)) {
472 close(imap->fd);
473 return err;
476 return nil;
479 /* */
480 /* close connection */
481 /* */
482 #if 0 /* jpc */
483 static void
484 imap4hangup(Imap *imap)
486 imap4cmd(imap, "LOGOUT");
487 imap4resp(imap);
488 close(imap->fd);
490 #endif
492 /* */
493 /* download a single message */
494 /* */
495 static char*
496 imap4fetch(Mailbox *mb, Message *m)
498 int i;
499 char *p, *s, sdigest[2*SHA1dlen+1];
500 Imap *imap;
502 imap = mb->aux;
504 imap->size = 0;
506 if(!isokay(s = imap4resp(imap)))
507 return s;
509 p = imap->base;
510 if(p == nil)
511 return "did not get message body";
513 removecr(p);
514 free(m->start);
515 m->start = p;
516 m->end = p+strlen(p);
517 m->bend = m->rbend = m->end;
518 m->header = m->start;
520 imap->base = nil;
521 imap->data = nil;
523 parse(m, 0, mb, 1);
525 /* digest headers */
526 sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
527 for(i = 0; i < SHA1dlen; i++)
528 sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
529 m->sdigest = s_copy(sdigest);
531 return nil;
534 /* */
535 /* check for new messages on imap4 server */
536 /* download new messages, mark deleted messages */
537 /* */
538 static char*
539 imap4read(Imap *imap, Mailbox *mb, int doplumb)
541 char *s;
542 int i, ignore, nnew, t;
543 Message *m, *next, **l;
545 imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox);
546 if(!isokay(s = imap4resp(imap)))
547 return s;
549 imap->nuid = 0;
550 imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0]));
551 imap->muid = imap->nmsg;
553 if(imap->nmsg > 0){
554 imap4cmd(imap, "UID FETCH 1:* UID");
555 if(!isokay(s = imap4resp(imap)))
556 return s;
559 l = &mb->root->part;
560 for(i=0; i<imap->nuid; i++){
561 ignore = 0;
562 while(*l != nil){
563 if((*l)->imapuid == imap->uid[i]){
564 ignore = 1;
565 l = &(*l)->next;
566 break;
567 }else{
568 /* old mail, we don't have it anymore */
569 if(doplumb)
570 mailplumb(mb, *l, 1);
571 (*l)->inmbox = 0;
572 (*l)->deleted = 1;
573 l = &(*l)->next;
576 if(ignore)
577 continue;
579 /* new message */
580 m = newmessage(mb->root);
581 m->mallocd = 1;
582 m->inmbox = 1;
583 m->imapuid = imap->uid[i];
585 /* add to chain, will download soon */
586 *l = m;
587 l = &m->next;
590 /* whatever is left at the end of the chain is gone */
591 while(*l != nil){
592 if(doplumb)
593 mailplumb(mb, *l, 1);
594 (*l)->inmbox = 0;
595 (*l)->deleted = 1;
596 l = &(*l)->next;
599 /* download new messages */
600 t = imap->tag;
601 if(pipeline)
602 switch(rfork(RFPROC|RFMEM)){
603 case -1:
604 sysfatal("rfork: %r");
605 default:
606 break;
607 case 0:
608 for(m = mb->root->part; m != nil; m = m->next){
609 if(m->start != nil)
610 continue;
611 if(imap->debug)
612 fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
613 t, (ulong)m->imapuid);
614 Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
615 t++, (ulong)m->imapuid);
617 Bflush(&imap->bout);
618 _exits(nil);
621 nnew = 0;
622 for(m=mb->root->part; m!=nil; m=next){
623 next = m->next;
624 if(m->start != nil)
625 continue;
627 if(!pipeline){
628 Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
629 (ulong)imap->tag, (ulong)m->imapuid);
630 Bflush(&imap->bout);
633 if(s = imap4fetch(mb, m)){
634 /* message disappeared? unchain */
635 fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s);
636 delmessage(mb, m);
637 mb->root->subname--;
638 continue;
640 nnew++;
641 if(doplumb)
642 mailplumb(mb, m, 0);
644 if(pipeline)
645 waitpid();
647 if(nnew || mb->vers == 0){
648 mb->vers++;
649 henter(PATH(0, Qtop), mb->name,
650 (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
652 return nil;
655 /* */
656 /* sync mailbox */
657 /* */
658 static void
659 imap4purge(Imap *imap, Mailbox *mb)
661 int ndel;
662 Message *m, *next;
664 ndel = 0;
665 for(m=mb->root->part; m!=nil; m=next){
666 next = m->next;
667 if(m->deleted && m->refs==0){
668 if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){
669 imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid);
670 if(isokay(imap4resp(imap))){
671 ndel++;
672 delmessage(mb, m);
674 }else
675 delmessage(mb, m);
679 if(ndel){
680 imap4cmd(imap, "EXPUNGE");
681 imap4resp(imap);
685 /* */
686 /* connect to imap4 server, sync mailbox */
687 /* */
688 static char*
689 imap4sync(Mailbox *mb, int doplumb)
691 char *err;
692 Imap *imap;
694 imap = mb->aux;
696 if(err = imap4dial(imap)){
697 mb->waketime = time(0) + imap->refreshtime;
698 return err;
701 if((err = imap4read(imap, mb, doplumb)) == nil){
702 imap4purge(imap, mb);
703 mb->d->atime = mb->d->mtime = time(0);
705 /*
706 * don't hang up; leave connection open for next time.
707 */
708 /* imap4hangup(imap); */
709 mb->waketime = time(0) + imap->refreshtime;
710 return err;
713 static char Eimap4ctl[] = "bad imap4 control message";
715 static char*
716 imap4ctl(Mailbox *mb, int argc, char **argv)
718 int n;
719 Imap *imap;
721 imap = mb->aux;
722 if(argc < 1)
723 return Eimap4ctl;
725 if(argc==1 && strcmp(argv[0], "debug")==0){
726 imap->debug = 1;
727 return nil;
730 if(argc==1 && strcmp(argv[0], "nodebug")==0){
731 imap->debug = 0;
732 return nil;
735 if(argc==1 && strcmp(argv[0], "thumbprint")==0){
736 if(imap->thumb)
737 freeThumbprints(imap->thumb);
738 imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
740 if(strcmp(argv[0], "refresh")==0){
741 if(argc==1){
742 imap->refreshtime = 60;
743 return nil;
745 if(argc==2){
746 n = atoi(argv[1]);
747 if(n < 15)
748 return Eimap4ctl;
749 imap->refreshtime = n;
750 return nil;
754 return Eimap4ctl;
757 /* */
758 /* free extra memory associated with mb */
759 /* */
760 static void
761 imap4close(Mailbox *mb)
763 Imap *imap;
765 imap = mb->aux;
766 free(imap->freep);
767 free(imap->base);
768 free(imap->uid);
769 if(imap->fd >= 0)
770 close(imap->fd);
771 free(imap);
774 /* */
775 /* open mailboxes of the form /imap/host/user */
776 /* */
777 char*
778 imap4mbox(Mailbox *mb, char *path)
780 char *f[10];
781 int mustssl, nf;
782 Imap *imap;
784 quotefmtinstall();
785 fmtinstall('Z', doublequote);
786 if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0)
787 return Enotme;
788 mustssl = (strncmp(path, "/imaps/", 7) == 0);
790 path = strdup(path);
791 if(path == nil)
792 return "out of memory";
794 nf = getfields(path, f, 5, 0, "/");
795 if(nf < 3){
796 free(path);
797 return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
800 imap = emalloc(sizeof(*imap));
801 imap->fd = -1;
802 imap->debug = debug;
803 imap->freep = path;
804 imap->mustssl = mustssl;
805 imap->host = f[2];
806 if(nf < 4)
807 imap->user = nil;
808 else
809 imap->user = f[3];
810 if(nf < 5)
811 imap->mbox = "Inbox";
812 else
813 imap->mbox = f[4];
814 imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
816 mb->aux = imap;
817 mb->sync = imap4sync;
818 mb->close = imap4close;
819 mb->ctl = imap4ctl;
820 mb->d = emalloc(sizeof(*mb->d));
821 /*mb->fetch = imap4fetch; */
823 return nil;
826 /* */
827 /* Formatter for %" */
828 /* Use double quotes to protect white space, frogs, \ and " */
829 /* */
830 enum
832 Qok = 0,
833 Qquote,
834 Qbackslash
835 };
837 static int
838 needtoquote(Rune r)
840 if(r >= Runeself)
841 return Qquote;
842 if(r <= ' ')
843 return Qquote;
844 if(r=='\\' || r=='"')
845 return Qbackslash;
846 return Qok;
849 int
850 doublequote(Fmt *f)
852 char *s, *t;
853 int w, quotes;
854 Rune r;
856 s = va_arg(f->args, char*);
857 if(s == nil || *s == '\0')
858 return fmtstrcpy(f, "\"\"");
860 quotes = 0;
861 for(t=s; *t; t+=w){
862 w = chartorune(&r, t);
863 quotes |= needtoquote(r);
865 if(quotes == 0)
866 return fmtstrcpy(f, s);
868 fmtrune(f, '"');
869 for(t=s; *t; t+=w){
870 w = chartorune(&r, t);
871 if(needtoquote(r) == Qbackslash)
872 fmtrune(f, '\\');
873 fmtrune(f, r);
875 return fmtrune(f, '"');