3 * 1. sync with imap server's flags
4 * 2. better algorithm for avoiding downloading message list.
5 * 3. get sender — eating envelope is lots of work!
12 #define idprint(i, ...) if(i->flags & Fdebug) fprint(2, __VA_ARGS__); else {}
13 #pragma varargck argpos imap4cmd 2
14 #pragma varargck type "Z" char*
15 #pragma varargck type "U" uvlong
16 #pragma varargck type "U" vlong
18 static char confused[] = "confused about fetch response";
19 static char qsep[] = " \t\r\n";
20 static char Eimap4ctl[] = "bad imap4 control message";
40 typedef struct Imap Imap;
43 /* free this to free the strings below */
61 /* open network connection */
82 if(r == '\\' || r == '"')
94 s = va_arg(f->args, char*);
96 return fmtstrcpy(f, "\"\"");
99 for(t = s; *t; t += w){
100 w = chartorune(&r, t);
101 quotes |= needtoquote(r);
104 return fmtstrcpy(f, s);
107 for(t = s; *t; t += w){
108 w = chartorune(&r, t);
109 if(needtoquote(r) == Qbackslash)
113 return fmtrune(f, '"');
123 u = va_arg(f->args, uvlong);
125 return fmtstrcpy(f, "nil");
127 return fmtstrcpy(f, "-");
130 snprint(buf, sizeof buf, "%lud:%lud", a, b);
131 return fmtstrcpy(f, buf);
135 imap4cmd(Imap *imap, char *fmt, ...)
141 p = buf + sprint(buf, "9x%lud ", imap->tag);
142 vseprint(p, buf + sizeof buf, fmt, va);
145 p = buf + strlen(buf);
146 if(p > buf + sizeof buf - 3)
147 sysfatal("imap4 command too long");
148 idprint(imap, "-> %s\n", buf);
150 Bwrite(&imap->bout, buf, strlen(buf));
168 static char *verblist[] = {
177 [Auth] "authenticate",
186 if(q = strchr(verb, ' '))
188 for(i = 0; i < nelem(verblist) - 1; i++)
189 if(strcmp(verblist[i], verb) == 0)
197 mkuid(Imap *i, char *id)
201 v = (vlong)i->validity<<32;
202 return v | strtoul(id, 0, 10);
206 xnum(char *s, int a, int b)
212 v = strtoull(s + 1, &s, 10);
222 "Answered", Fanswered,
223 "\\Deleted", Fdeleted,
225 "\\Flagged", Fflagged,
232 parseflags(Message *m, char *s)
237 n = tokenize(s, f, nelem(f));
238 qsort(f, n, sizeof *f, (int (*)(void*,void*))strcmp);
240 for(i = 0; i < n; i++)
242 if(j == nelem(ftab)){
243 j = j0; /* restart search */
246 if(strcmp(f[i], ftab[j].flag) == 0){
247 m->flags |= ftab[j].e;
253 /* "17-Jul-1996 02:44:25 -0700" */
255 internaltounix(char *s)
258 if(strlen(s) < 20 || s[2] != '-' || s[6] != '-')
262 if(strtotm(s, &tm) == -1)
268 qtoken(char *s, char *sep)
274 t = s; /* s is output string, t is input string */
275 while(*t!='\0' && (quoting || utfrune(sep, *t)==nil)){
276 if(*t != '"' && *t != '(' && *t != ')'){
281 if(!quoting || *t == '('){
286 /* quoting and we're on a quote */
288 /* end of quoted section; absorb closing quote */
294 /* doubled quote; fold one quote into two */
307 imaptokenize(char *s, char **args, int maxargs)
311 for(nargs=0; nargs < maxargs; nargs++){
312 while(*s!='\0' && utfrune(qsep, *s)!=nil)
324 fetchrsp(Imap *imap, char *p, Mailbox *, Message *m)
330 static char error[256];
331 extern void msgrealloc(Message*, ulong);
334 n = imaptokenize(p, f, nelem(f));
337 for(i = 0; i < n; i += 2){
338 if(strcmp(f[i], "internaldate") == 0){
339 l = internaltounix(f[i + 1]);
342 if(imap->nuid < imap->muid)
343 imap->f[imap->nuid].dates = l;
344 }else if(strcmp(f[i], "rfc822.size") == 0){
345 l = strtoul(f[i + 1], 0, 0);
348 else if(imap->nuid < imap->muid)
349 imap->f[imap->nuid].sizes = l;
350 }else if(strcmp(f[i], "uid") == 0){
351 v = mkuid(imap, f[1]);
354 if(imap->nuid < imap->muid)
355 imap->f[imap->nuid].uid = v;
356 }else if(strcmp(f[i], "flags") == 0)
357 parseflags(m, f[i + 1]);
358 else if(strncmp(f[i], "body[]", 6) == 0){
362 o = xnum(s, '<', '>');
365 l = xnum(f[i + 1], '{', '}');
366 a = o + l - m->ibadchars - m->size;
368 assert(imap->flags & Fgmail);
370 msgrealloc(m, m->size);
371 m->size -= m->ibadchars;
373 if(Bread(&imap->bin, m->start + o, l) != l){
374 snprint(error, sizeof error, "read: %r");
377 if(Bgetc(&imap->bin) == ')'){
378 while(Bgetc(&imap->bin) != '\n')
383 if(!(p = Brdline(&imap->bin, '\n')))
385 q = p + Blinelen(&imap->bin);
386 while(q > p && (q[-1] == '\n' || q[-1] == '\r'))
390 idprint(imap, "<- %s\n", p);
400 parsecap(Imap *imap, char *s)
406 n = getfields(s, t, nelem(t), 0, " ");
407 for(i = 0; i < n; i++){
408 if(strncmp(t[i], "auth=", 5) == 0){
410 if(strcmp(p, "cram-md5") == 0)
412 if(strcmp(p, "ntlm") == 0)
414 }else if(strcmp(t[i], "logindisabled") == 0)
421 * get imap4 response line. there might be various
422 * data or other informational lines mixed in.
425 imap4resp0(Imap *imap, Mailbox *mb, Message *m)
427 char *e, *line, *p, *ep, *op, *q, *verb;
429 static char error[256];
432 while(p = Brdline(&imap->bin, '\n')){
433 ep = p + Blinelen(&imap->bin);
434 while(ep > p && (ep[-1] == '\n' || ep[-1] == '\r'))
436 idprint(imap, "<- %s\n", p);
437 if(unexp && p[0] != '9' && p[1] != 'x')
438 if(strtoul(p + 2, &p, 10) != imap->tag)
441 lowercase(p); /* botch */
444 case '+': /* cram challenge */
453 n = strtol(p, &p, 10);
458 if(p = strchr(verb, ' '))
461 p = verb + strlen(verb);
463 switch(verbcode(verb)){
465 /* early disconnect */
466 snprint(error, sizeof error, "%s", p);
471 /* human readable text at p; */
480 /* * status inbox (messages 2 uidvalidity 960164964) */
481 if(q = strstr(p, "messages"))
482 imap->nmsg = strtoul(q + 8, 0, 10);
483 if(q = strstr(p, "uidvalidity"))
484 imap->validity = strtoul(q + 11, 0, 10);
492 if(e = fetchrsp(imap, p, mb, m))
493 eprint("imap: fetchrsp: %s\n", e);
502 case '9': /* response to our message */
504 if(p[1] == 'x' && strtoul(p + 2, &p, 10) == imap->tag){
510 eprint("imap: expected %lud; got %s\n", imap->tag, op);
513 if(imap->flags&Fdebug || *p){
514 eprint("imap: unexpected line: %s\n", p);
519 snprint(error, sizeof error, "i/o error: %r\n");
526 return imap4resp0(i, 0, 0);
532 return cistrncmp(resp, "OK", 2) == 0;
540 for(i = 0; i < nelem(ftab); i++)
541 if(ftab[i].e == 1<<idx)
547 imap4modflags(Mailbox *mb, Message *m, int flags)
549 char buf[128], *p, *e, *fs;
554 e = buf + sizeof buf;
556 f = flags & ~Frecent;
557 for(i = 0; i < Nflags; i++)
558 if(f & 1<<i && (fs = findflag(i)))
559 p = seprint(p, e, "%s ", fs);
562 imap4cmd(imap, "uid store %lud flags (%s)", (ulong)m->imapuid, buf);
568 imap4cram(Imap *imap)
570 char *s, *p, ch[128], usr[64], rbuf[128], ubuf[128], ebuf[192];
573 fmtinstall('[', encodefmt);
575 imap4cmd(imap, "authenticate cram-md5");
578 return "no challenge";
579 l = dec64((uchar*)ch, sizeof ch, p, strlen(p));
583 idprint(imap, "challenge [%s]\n", ch);
585 if(imap->user == nil)
586 imap->user = getlog();
587 n = auth_respond(ch, l, usr, sizeof usr, rbuf, sizeof rbuf, auth_getkey,
588 "proto=cram role=client server=%q user=%s", imap->host, imap->user);
590 return "cannot find IMAP password";
591 for(i = 0; i < n; i++)
592 if(rbuf[i] >= 'A' && rbuf[i] <= 'Z')
593 rbuf[i] += 'a' - 'A';
594 l = snprint(ubuf, sizeof ubuf, "%s %.*s", usr, n, rbuf);
595 idprint(imap, "raw cram [%s]\n", ubuf);
596 snprint(ebuf, sizeof ebuf, "%.*[", l, ubuf);
599 idprint(imap, "-> %s\n", ebuf);
600 Bprint(&imap->bout, "%s\r\n", ebuf);
603 if(!isokay(s = imap4resp(imap)))
609 * authenticate to IMAP4 server using NTLM (untested)
611 * http://davenport.sourceforge.net/ntlm.html#ntlmImapAuthentication
612 * http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx
615 psecb(uchar *p, uint o, int n)
629 psecq(uchar *q, char *s, int n)
636 imap4ntlm(Imap *imap)
638 char *s, ruser[64], enc[256];
639 uchar buf[128], *p, *ep, *q, *eq, *chal;
643 imap4cmd(imap, "authenticate ntlm");
646 /* simple NtLmNegotiate blob with NTLM+OEM flags */
647 imap4cmd(imap, "TlRMTVNTUAABAAAAAgIAAA==");
649 n = dec64(buf, sizeof buf, s, strlen(s));
650 if(n < 32 || memcmp(buf, "NTLMSSP", 8) != 0)
651 return "bad NtLmChallenge";
654 if(auth_respond(chal, 8, ruser, sizeof ruser,
655 &mcr, sizeof mcr, auth_getkey,
656 "proto=mschap role=client service=imap server=%q user?",
658 return "auth_respond failed";
660 /* prepare NtLmAuthenticate blob */
662 memset(buf, sizeof buf, 0);
664 ep = p + 8 + 6*8 + 2*4;
666 eq = buf + sizeof buf;
669 memcpy(p, "NTLMSSP", 8); /* magic */
677 p = psecb(p, q-buf, 24); /* LMresp */
678 q = psecq(q, mcr.LMresp, 24);
680 p = psecb(p, q-buf, 24); /* NTresp */
681 q = psecq(q, mcr.NTresp, 24);
683 p = psecb(p, q-buf, 0); /* realm */
686 p = psecb(p, q-buf, n); /* user name */
687 q = psecq(q, ruser, n);
689 p = psecb(p, q-buf, 0); /* workstation name */
690 p = psecb(p, q-buf, 0); /* session key */
692 *p++ = 0x02; /* flags: oem(2)|ntlm(0x200) */
698 return "error creating NtLmAuthenticate";
699 enc64(enc, sizeof enc, buf, q-buf);
702 if(!isokay(s = imap4resp(imap)))
708 imap4passwd(Imap *imap)
713 if(imap->user != nil)
714 up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
716 up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
718 return "cannot find IMAP password";
721 imap4cmd(imap, "login %Z %Z", up->user, up->passwd);
723 if(!isokay(s = imap4resp(imap)))
729 imap4login(Imap *imap)
733 if(imap->cap & Ccram)
735 else if(imap->cap & Cntlm)
738 e = imap4passwd(imap);
741 imap4cmd(imap, "select %Z", imap->mbox);
742 if(!isokay(e = imap4resp(imap)))
748 imaperrstr(char *host, char *port)
751 static char buf[256];
754 errstr(err, sizeof err);
755 snprint(buf, sizeof buf, "%s/%s:%s", host, port, err);
760 imap4disconnect(Imap *imap)
774 capabilties(Imap *imap)
778 imap4cmd(imap, "capability");
780 err = imap4resp(imap);
787 imap4dial(Imap *imap)
792 imap4cmd(imap, "noop");
793 if(isokay(imap4resp(imap)))
795 imap4disconnect(imap);
797 if(imap->flags & Fssl)
801 if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
802 return imaperrstr(imap->host, port);
803 if(imap->flags & Fssl && (imap->fd = wraptls(imap->fd, imap->host)) < 0){
804 err = imaperrstr(imap->host, port);
805 imap4disconnect(imap);
808 assert(imap->binit == 0);
809 Binit(&imap->bin, imap->fd, OREAD);
810 Binit(&imap->bout, imap->fd, OWRITE);
814 err = imap4resp(imap);
816 return "error in initial IMAP handshake";
818 if((err = capabilties(imap)) || (err = imap4login(imap))){
819 eprint("imap: err is %s\n", err);
820 imap4disconnect(imap);
827 imap4hangup(Imap *imap)
829 imap4cmd(imap, "logout");
831 imap4disconnect(imap);
834 /* gmail lies about message sizes */
836 gmaildiscount(Message *m, uvlong o, ulong l)
838 if((m->cstate&Cidx) == 0)
840 return l + 100 + (o + l)/5;
845 imap4fetch(Mailbox *mb, Message *m, uvlong o, ulong l)
850 if(imap->flags & Fgmail)
851 l = gmaildiscount(m, o, l);
852 idprint(imap, "uid fetch %lud (body.peek[]<%llud.%lud>)\n", (ulong)m->imapuid, o, l);
853 imap4cmd(imap, "uid fetch %lud (body.peek[]<%llud.%lud>)", (ulong)m->imapuid, o, l);
854 if(!isokay(imap4resp0(imap, mb, m))){
855 eprint("imap: imap fetch failed\n");
862 datesec(Imap *imap, int i)
869 v = (uvlong)f[i].dates << 8;
871 /* shifty; these sequences should be stable. */
872 for(j = i; j-- > 0; )
873 if(f[i].dates != f[j].dates)
880 markdel(Mailbox *mb, Message *m, int doplumb)
885 m->deleted = Disappear;
889 vcmp(vlong a, vlong b)
900 fetchicmp(Fetchi *f1, Fetchi *f2)
902 return vcmp(f1->uid, f2->uid);
906 imap4read(Imap *imap, Mailbox *mb, int doplumb, int *new)
909 int i, n, c, nnew, ndel;
914 imap4cmd(imap, "status %Z (messages uidvalidity)", imap->mbox);
915 if(!isokay(s = imap4resp(imap)))
919 imap->muid = imap->nmsg;
920 imap->f = erealloc(imap->f, imap->nmsg*sizeof imap->f[0]);
925 imap4cmd(imap, "uid fetch 1:* (uid rfc822.size internaldate)");
926 if(!isokay(s = imap4resp(imap)))
930 qsort(f, n, sizeof f[0], (int(*)(void*, void*))fetchicmp);
932 ll = &mb->root->part;
933 for(i = 0; (m = *ll) != nil || i < n; ){
939 m->imapuid = strtoull(m->idxaux, 0, 0);
940 c = vcmp(f[i].uid, m->imapuid);
942 idprint(imap, "consider %U and %U -> %d\n", i<n? f[i].uid: 0, m? m->imapuid: 1, c);
945 idprint(imap, "new: %U (%U)\n", f[i].uid, m? m->imapuid: 0);
946 if(f[i].sizes > Maxmsg){
947 idprint(imap, "skipping bad size: %lud\n", f[i].sizes);
952 m = newmessage(mb->root);
954 m->idxaux = smprint("%llud", f[i].uid);
955 m->imapuid = f[i].uid;
956 m->fileid = datesec(imap, i);
957 m->size = f[i].sizes;
961 newcachehash(mb, m, doplumb);
965 /* deleted message; */
966 idprint(imap, "deleted: %U (%U)\n", i<n? f[i].uid: 0, m? m->imapuid: 0);
968 markdel(mb, m, doplumb);
981 imap4delete(Mailbox *mb, Message *m)
986 if((ulong)(m->imapuid>>32) == imap->validity){
987 imap4cmd(imap, "uid store %lud +flags (\\Deleted)", (ulong)m->imapuid);
989 imap4cmd(imap, "expunge");
991 // if(!isokay(imap4resp(imap))
998 imap4sync(Mailbox *mb, int doplumb, int *new)
1004 if(err = imap4dial(imap))
1006 err = imap4read(imap, mb, doplumb, new);
1008 mb->waketime = (ulong)time(0) + imap->refreshtime;
1013 imap4ctl(Mailbox *mb, int argc, char **argv)
1021 if(argc == 1 && strcmp(argv[0], "debug") == 0){
1022 imap->flags ^= Fdebug;
1025 if(argc == 2 && strcmp(argv[0], "uid") == 0){
1029 for(m = mb->root->part; m; m = m->next)
1030 if(strcmp(argv[1], m->name) == 0){
1031 l = strtoull(m->idxaux, 0, 0);
1032 fprint(2, "uid %s %lud %lud %lud %lud\n", m->name, (ulong)(l>>32), (ulong)l,
1033 (ulong)(m->imapuid>>32), (ulong)m->imapuid);
1037 if(strcmp(argv[0], "refresh") == 0)
1040 imap->refreshtime = 60;
1043 imap->refreshtime = atoi(argv[1]);
1051 imap4close(Mailbox *mb)
1056 imap4disconnect(imap);
1062 mkmbox(Imap *imap, char *p, char *e)
1064 p = seprint(p, e, "%s/box/%s/imap.%s", MAILROOT, getlog(), imap->host);
1065 if(imap->user && strcmp(imap->user, getlog()))
1066 p = seprint(p, e, ".%s", imap->user);
1067 if(cistrcmp(imap->mbox, "inbox"))
1068 p = seprint(p, e, ".%s", imap->mbox);
1075 char *f[10], path[Pathlen];
1078 snprint(path, sizeof path, "%s", p);
1079 nf = getfields(path, f, 5, 0, "/");
1086 imap4rename(Mailbox *mb, char *p2, int)
1093 idprint(imap, "rename %s %s\n", imap->mbox, new);
1094 imap4cmd(imap, "rename %s %s", imap->mbox, new);
1095 r = imap4resp(imap);
1099 imap->mbox = smprint("%s", new);
1100 mkmbox(imap, mb->path, mb->path + sizeof mb->path);
1105 * incomplete; when we say remove we want to get subfolders, too.
1106 * so we need to to a list, and recursivly nuke folders.
1109 imap4remove(Mailbox *mb, int flags)
1115 idprint(imap, "remove %s\n", imap->mbox);
1116 imap4cmd(imap, "delete %s", imap->mbox);
1117 r = imap4resp(imap);
1121 imap4cmd(imap, "create %s", imap->mbox);
1122 r = imap4resp(imap);
1130 imap4mbox(Mailbox *mb, char *path)
1137 fmtinstall('Z', Zfmt);
1138 fmtinstall('U', Ufmt);
1139 if(strncmp(path, "/imap/", 6) == 0)
1141 else if(strncmp(path, "/imaps/", 7) == 0)
1146 path = strdup(path);
1148 return "out of memory";
1150 nf = getfields(path, f, 5, 0, "/");
1153 return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
1156 imap = emalloc(sizeof *imap);
1159 imap->flags = flags;
1161 if(strstr(imap->host, "gmail.com"))
1162 imap->flags |= Fgmail;
1163 imap->refreshtime = 60;
1169 imap->mbox = strdup("inbox");
1171 imap->mbox = strdup(f[4]);
1172 mkmbox(imap, mb->path, mb->path + sizeof mb->path);
1174 mb->sync = imap4sync;
1175 mb->close = imap4close;
1177 mb->fetch = imap4fetch;
1178 mb->delete = imap4delete;
1179 mb->rename = imap4rename;
1180 // mb->remove = imap4remove;
1181 mb->modflags = imap4modflags;