ref: 39f18c9d88f52a22373790dec5721fa3521d3f00
dir: /sys/src/cmd/ip/imap4d/imap4d.c/
#include <u.h> #include <libc.h> #include <auth.h> #include <bio.h> #include "imap4d.h" /* * these should be in libraries */ char *csquery(char *attr, char *val, char *rattr); /* * /lib/rfc/rfc2060 imap4rev1 * /lib/rfc/rfc2683 is implementation advice * /lib/rfc/rfc2342 is namespace capability * /lib/rfc/rfc2222 is security protocols * /lib/rfc/rfc1731 is security protocols * /lib/rfc/rfc2221 is LOGIN-REFERRALS * /lib/rfc/rfc2193 is MAILBOX-REFERRALS * /lib/rfc/rfc2177 is IDLE capability * /lib/rfc/rfc2195 is CRAM-MD5 authentication * /lib/rfc/rfc2088 is LITERAL+ capability * /lib/rfc/rfc1760 is S/Key authentication * * outlook uses "Secure Password Authentication" aka ntlm authentication * * capabilities from nslocum * CAPABILITY IMAP4 IMAP4REV1 NAMESPACE IDLE SCAN SORT MAILBOX-REFERRALS LOGIN-REFERRALS AUTH=LOGIN THREAD=ORDEREDSUBJECT */ typedef struct ParseCmd ParseCmd; enum { UlongMax = 4294967295, }; struct ParseCmd { char *name; void (*f)(char *tg, char *cmd); }; static void appendCmd(char *tg, char *cmd); static void authenticateCmd(char *tg, char *cmd); static void capabilityCmd(char *tg, char *cmd); static void closeCmd(char *tg, char *cmd); static void copyCmd(char *tg, char *cmd); static void createCmd(char *tg, char *cmd); static void deleteCmd(char *tg, char *cmd); static void expungeCmd(char *tg, char *cmd); static void fetchCmd(char *tg, char *cmd); static void idleCmd(char *tg, char *cmd); static void listCmd(char *tg, char *cmd); static void loginCmd(char *tg, char *cmd); static void logoutCmd(char *tg, char *cmd); static void namespaceCmd(char *tg, char *cmd); static void noopCmd(char *tg, char *cmd); static void renameCmd(char *tg, char *cmd); static void searchCmd(char *tg, char *cmd); static void selectCmd(char *tg, char *cmd); static void statusCmd(char *tg, char *cmd); static void storeCmd(char *tg, char *cmd); static void subscribeCmd(char *tg, char *cmd); static void uidCmd(char *tg, char *cmd); static void unsubscribeCmd(char *tg, char *cmd); static void copyUCmd(char *tg, char *cmd, int uids); static void fetchUCmd(char *tg, char *cmd, int uids); static void searchUCmd(char *tg, char *cmd, int uids); static void storeUCmd(char *tg, char *cmd, int uids); static void imap4(int); static void status(int expungeable, int uids); static void cleaner(void); static void check(void); static int catcher(void*, char*); static Search *searchKey(int first); static Search *searchKeys(int first, Search *tail); static char *astring(void); static char *atomString(char *disallowed, char *initial); static char *atom(void); static void badsyn(void); static void clearcmd(void); static char *command(void); static void crnl(void); static Fetch *fetchAtt(char *s, Fetch *f); static Fetch *fetchWhat(void); static int flagList(void); static int flags(void); static int getc(void); static char *listmbox(void); static char *literal(void); static ulong litlen(void); static MsgSet *msgSet(int); static void mustBe(int c); static ulong number(int nonzero); static int peekc(void); static char *quoted(void); static void sectText(Fetch *f, int mimeOk); static ulong seqNo(void); static Store *storeWhat(void); static char *tag(void); static ulong uidNo(void); static void ungetc(void); static ParseCmd SNonAuthed[] = { {"capability", capabilityCmd}, {"logout", logoutCmd}, {"x-exit", logoutCmd}, {"noop", noopCmd}, {"login", loginCmd}, {"authenticate", authenticateCmd}, nil }; static ParseCmd SAuthed[] = { {"capability", capabilityCmd}, {"logout", logoutCmd}, {"x-exit", logoutCmd}, {"noop", noopCmd}, {"append", appendCmd}, {"create", createCmd}, {"delete", deleteCmd}, {"examine", selectCmd}, {"select", selectCmd}, {"idle", idleCmd}, {"list", listCmd}, {"lsub", listCmd}, {"namespace", namespaceCmd}, {"rename", renameCmd}, {"status", statusCmd}, {"subscribe", subscribeCmd}, {"unsubscribe", unsubscribeCmd}, nil }; static ParseCmd SSelected[] = { {"capability", capabilityCmd}, {"logout", logoutCmd}, {"x-exit", logoutCmd}, {"noop", noopCmd}, {"append", appendCmd}, {"create", createCmd}, {"delete", deleteCmd}, {"examine", selectCmd}, {"select", selectCmd}, {"idle", idleCmd}, {"list", listCmd}, {"lsub", listCmd}, {"namespace", namespaceCmd}, {"rename", renameCmd}, {"status", statusCmd}, {"subscribe", subscribeCmd}, {"unsubscribe", unsubscribeCmd}, {"check", noopCmd}, {"close", closeCmd}, {"copy", copyCmd}, {"expunge", expungeCmd}, {"fetch", fetchCmd}, {"search", searchCmd}, {"store", storeCmd}, {"uid", uidCmd}, nil }; static char *atomStop = "(){%*\"\\"; static Chalstate *chal; static int chaled; static ParseCmd *imapState; static jmp_buf parseJmp; static char *parseMsg; static int allowPass; static int allowCR; static int exiting; static QLock imaplock; static int idlepid = -1; Biobuf bout; Biobuf bin; char username[UserNameLen]; char mboxDir[MboxNameLen]; char *servername; char *site; char *remote; Box *selected; Bin *parseBin; int debug; void main(int argc, char *argv[]) { char *s, *t; int preauth, n; Binit(&bin, 0, OREAD); Binit(&bout, 1, OWRITE); /* for auth */ fmtinstall('H', encodefmt); fmtinstall('[', encodefmt); preauth = 0; allowPass = 0; allowCR = 0; ARGBEGIN{ case 'a': preauth = 1; break; case 'd': site = ARGF(); break; case 'c': allowCR = 1; break; case 'p': allowPass = 1; break; case 'r': remote = ARGF(); break; case 's': servername = ARGF(); break; case 'v': debug = 1; debuglog("imap4d debugging enabled\n"); break; default: fprint(2, "usage: ip/imap4d [-acpv] [-d site] [-r remotehost] [-s servername]\n"); bye("usage"); break; }ARGEND if(allowPass && allowCR){ fprint(2, "%s: -c and -p are mutually exclusive\n", argv0); bye("usage"); } if(preauth) setupuser(nil); if(servername == nil){ servername = csquery("sys", sysname(), "dom"); if(servername == nil) servername = sysname(); if(servername == nil){ fprint(2, "ip/imap4d can't find server name: %r\n"); bye("can't find system name"); } } if(site == nil){ t = getenv("site"); if(t == nil) site = servername; else{ n = strlen(t); s = strchr(servername, '.'); if(s == nil) s = servername; else s++; n += strlen(s) + 2; site = emalloc(n); snprint(site, n, "%s.%s", t, s); } } rfork(RFNOTEG|RFREND); atnotify(catcher, 1); qlock(&imaplock); atexit(cleaner); imap4(preauth); } static void imap4(int preauth) { char *volatile tg; char *volatile cmd; ParseCmd *st; if(preauth){ Bprint(&bout, "* preauth %s IMAP4rev1 server ready user %s authenticated\r\n", servername, username); imapState = SAuthed; }else{ Bprint(&bout, "* OK %s IMAP4rev1 server ready\r\n", servername); imapState = SNonAuthed; } if(Bflush(&bout) < 0) writeErr(); chaled = 0; tg = nil; cmd = nil; if(setjmp(parseJmp)){ if(tg == nil) Bprint(&bout, "* bad empty command line: %s\r\n", parseMsg); else if(cmd == nil) Bprint(&bout, "%s BAD no command: %s\r\n", tg, parseMsg); else Bprint(&bout, "%s BAD %s %s\r\n", tg, cmd, parseMsg); clearcmd(); if(Bflush(&bout) < 0) writeErr(); binfree(&parseBin); } for(;;){ if(mbLocked()) bye("internal error: mailbox lock held"); tg = nil; cmd = nil; tg = tag(); mustBe(' '); cmd = atom(); /* * note: outlook express is broken: it requires echoing the * command as part of matching response */ for(st = imapState; st->name != nil; st++){ if(cistrcmp(cmd, st->name) == 0){ (*st->f)(tg, cmd); break; } } if(st->name == nil){ clearcmd(); Bprint(&bout, "%s BAD %s illegal command\r\n", tg, cmd); } if(Bflush(&bout) < 0) writeErr(); binfree(&parseBin); } } void bye(char *fmt, ...) { va_list arg; va_start(arg, fmt); Bprint(&bout, "* bye "); Bvprint(&bout, fmt, arg); Bprint(&bout, "\r\n"); Bflush(&bout); exits("rob2"); exits(0); } void parseErr(char *msg) { parseMsg = msg; longjmp(parseJmp, 1); } /* * an error occured while writing to the client */ void writeErr(void) { cleaner(); _exits("connection closed"); } static int catcher(void *v, char *msg) { USED(v); if(strstr(msg, "closed pipe") != nil) return 1; return 0; } /* * wipes out the idleCmd backgroung process if it is around. * this can only be called if the current proc has qlocked imaplock. * it must be the last piece of imap4d code executed. */ static void cleaner(void) { int i; if(idlepid < 0) return; exiting = 1; close(0); close(1); close(2); /* * the other proc is either stuck in a read, a sleep, * or is trying to lock imap4lock. * get him out of it so he can exit cleanly */ qunlock(&imaplock); for(i = 0; i < 4; i++) postnote(PNGROUP, getpid(), "die"); } /* * send any pending status updates to the client * careful: shouldn't exit, because called by idle polling proc * * can't always send pending info * in particular, can't send expunge info * in response to a fetch, store, or search command. * * rfc2060 5.2: server must send mailbox size updates * rfc2060 5.2: server may send flag updates * rfc2060 5.5: servers prohibited from sending expunge while fetch, store, search in progress * rfc2060 7: in selected state, server checks mailbox for new messages as part of every command * sends untagged EXISTS and RECENT respsonses reflecting new size of the mailbox * should also send appropriate untagged FETCH and EXPUNGE messages if another agent * changes the state of any message flags or expunges any messages * rfc2060 7.4.1 expunge server response must not be sent when no command is in progress, * nor while responding to a fetch, stort, or search command (uid versions are ok) * command only "in progress" after entirely parsed. * * strategy for third party deletion of messages or of a mailbox * * deletion of a selected mailbox => act like all message are expunged * not strictly allowed by rfc2180, but close to method 3.2. * * renaming same as deletion * * copy * reject iff a deleted message is in the request * * search, store, fetch operations on expunged messages * ignore the expunged messages * return tagged no if referenced */ static void status(int expungeable, int uids) { int tell; if(!selected) return; tell = 0; if(expungeable) tell = expungeMsgs(selected, 1); if(selected->sendFlags) sendFlags(selected, uids); if(tell || selected->toldMax != selected->max){ Bprint(&bout, "* %lud EXISTS\r\n", selected->max); selected->toldMax = selected->max; } if(tell || selected->toldRecent != selected->recent){ Bprint(&bout, "* %lud RECENT\r\n", selected->recent); selected->toldRecent = selected->recent; } if(tell) closeImp(selected, checkBox(selected, 1)); } /* * careful: can't exit, because called by idle polling proc */ static void check(void) { if(!selected) return; checkBox(selected, 0); status(1, 0); } static void appendCmd(char *tg, char *cmd) { char *mbox, head[128]; ulong t, n, now; int flags, ok; mustBe(' '); mbox = astring(); mustBe(' '); flags = 0; if(peekc() == '('){ flags = flagList(); mustBe(' '); } now = time(nil); if(peekc() == '"'){ t = imap4DateTime(quoted()); if(t == ~0) parseErr("illegal date format"); mustBe(' '); if(t > now) t = now; }else t = now; n = litlen(); mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ check(); Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); return; } if(!cdExists(mboxDir, mbox)){ check(); Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd); return; } snprint(head, sizeof(head), "From %s %s", username, ctime(t)); ok = appendSave(mbox, flags, head, &bin, n); crnl(); check(); if(ok) Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); else Bprint(&bout, "%s NO %s message save failed\r\n", tg, cmd); } static void authenticateCmd(char *tg, char *cmd) { char *s, *t; mustBe(' '); s = atom(); crnl(); auth_freechal(chal); chal = nil; if(cistrcmp(s, "cram-md5") == 0){ t = cramauth(); if(t == nil){ Bprint(&bout, "%s OK %s\r\n", tg, cmd); imapState = SAuthed; }else Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t); }else Bprint(&bout, "%s NO %s unsupported authentication protocol\r\n", tg, cmd); } static void capabilityCmd(char *tg, char *cmd) { crnl(); check(); // nslocum's capabilities // Bprint(&bout, "* CAPABILITY IMAP4 IMAP4REV1 NAMESPACE IDLE SCAN SORT MAILBOX-REFERRALS LOGIN-REFERRALS AUTH=LOGIN THREAD=ORDEREDSUBJECT\r\n"); Bprint(&bout, "* CAPABILITY IMAP4REV1 IDLE NAMESPACE AUTH=CRAM-MD5\r\n"); Bprint(&bout, "%s OK %s\r\n", tg, cmd); } static void closeCmd(char *tg, char *cmd) { crnl(); imapState = SAuthed; closeBox(selected, 1); selected = nil; Bprint(&bout, "%s OK %s mailbox closed, now in authenticated state\r\n", tg, cmd); } /* * note: message id's are before any pending expunges */ static void copyCmd(char *tg, char *cmd) { copyUCmd(tg, cmd, 0); } static void copyUCmd(char *tg, char *cmd, int uids) { MsgSet *ms; char *uid, *mbox; ulong max; int ok; mustBe(' '); ms = msgSet(uids); mustBe(' '); mbox = astring(); crnl(); uid = ""; if(uids) uid = "uid "; mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ status(1, uids); Bprint(&bout, "%s NO %s%s bad mailbox\r\n", tg, uid, cmd); return; } if(!cdExists(mboxDir, mbox)){ check(); Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd); return; } max = selected->max; checkBox(selected, 0); ok = forMsgs(selected, ms, max, uids, copyCheck, nil); if(ok) ok = forMsgs(selected, ms, max, uids, copySave, mbox); status(1, uids); if(ok) Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd); else Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd); } static void createCmd(char *tg, char *cmd) { char *mbox, *m; int fd, slash; mustBe(' '); mbox = astring(); crnl(); check(); m = strchr(mbox, '\0'); slash = m != mbox && m[-1] == '/'; mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); return; } if(cistrcmp(mbox, "inbox") == 0){ Bprint(&bout, "%s NO %s cannot remotely create INBOX\r\n", tg, cmd); return; } if(access(mbox, AEXIST) >= 0){ Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd); return; } fd = createBox(mbox, slash); close(fd); if(fd < 0) Bprint(&bout, "%s NO %s cannot create mailbox %s\r\n", tg, cmd, mbox); else Bprint(&bout, "%s OK %s %s completed\r\n", tg, mbox, cmd); } static void deleteCmd(char *tg, char *cmd) { char *mbox, *imp; mustBe(' '); mbox = astring(); crnl(); check(); mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); return; } imp = impName(mbox); if(cistrcmp(mbox, "inbox") == 0 || imp != nil && cdRemove(mboxDir, imp) < 0 && cdExists(mboxDir, imp) || cdRemove(mboxDir, mbox) < 0) Bprint(&bout, "%s NO %s cannot delete mailbox %s\r\n", tg, cmd, mbox); else Bprint(&bout, "%s OK %s %s completed\r\n", tg, mbox, cmd); } static void expungeCmd(char *tg, char *cmd) { int ok; crnl(); ok = deleteMsgs(selected); check(); if(ok) Bprint(&bout, "%s OK %s messages erased\r\n", tg, cmd); else Bprint(&bout, "%s NO %s some messages not expunged\r\n", tg, cmd); } static void fetchCmd(char *tg, char *cmd) { fetchUCmd(tg, cmd, 0); } static void fetchUCmd(char *tg, char *cmd, int uids) { Fetch *f; MsgSet *ms; MbLock *ml; char *uid; ulong max; int ok; mustBe(' '); ms = msgSet(uids); mustBe(' '); f = fetchWhat(); crnl(); uid = ""; if(uids) uid = "uid "; max = selected->max; ml = checkBox(selected, 1); if(ml != nil) forMsgs(selected, ms, max, uids, fetchSeen, f); closeImp(selected, ml); ok = ml != nil && forMsgs(selected, ms, max, uids, fetchMsg, f); status(uids, uids); if(ok) Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd); else Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd); } static void idleCmd(char *tg, char *cmd) { int c, pid; crnl(); Bprint(&bout, "+ idling, waiting for done\r\n"); if(Bflush(&bout) < 0) writeErr(); if(idlepid < 0){ pid = rfork(RFPROC|RFMEM|RFNOWAIT); if(pid == 0){ for(;;){ qlock(&imaplock); if(exiting) break; /* * parent may have changed curDir, but it doesn't change our . */ resetCurDir(); check(); if(Bflush(&bout) < 0) writeErr(); qunlock(&imaplock); sleep(15*1000); enableForwarding(); } _exits("rob3"); _exits(0); } idlepid = pid; } qunlock(&imaplock); /* * clear out the next line, which is supposed to contain (case-insensitive) * done\n * this is special code since it has to dance with the idle polling proc * and handle exiting correctly. */ for(;;){ c = getc(); if(c < 0){ qlock(&imaplock); if(!exiting) cleaner(); _exits("rob4"); _exits(0); } if(c == '\n') break; } qlock(&imaplock); if(exiting) {_exits("rob5"); _exits(0); } /* * child may have changed curDir, but it doesn't change our . */ resetCurDir(); check(); Bprint(&bout, "%s OK %s terminated\r\n", tg, cmd); } static void listCmd(char *tg, char *cmd) { char *s, *t, *ss, *ref, *mbox; int n; mustBe(' '); s = astring(); mustBe(' '); t = listmbox(); crnl(); check(); ref = mutf7str(s); mbox = mutf7str(t); if(ref == nil || mbox == nil){ Bprint(&bout, "%s BAD %s mailbox name not in modified utf-7\r\n", tg, cmd); return; } /* * special request for hierarchy delimiter and root name * root name appears to be name up to and including any delimiter, * or the empty string, if there is no delimiter. * * this must change if the # namespace convention is supported. */ if(*mbox == '\0'){ s = strchr(ref, '/'); if(s == nil) ref = ""; else s[1] = '\0'; Bprint(&bout, "* %s (\\Noselect) \"/\" \"%s\"\r\n", cmd, ref); Bprint(&bout, "%s OK %s\r\n", tg, cmd); return; } /* * massage the listing name: * clean up the components individually, * then rip off componenets from the ref to * take care of leading ..'s in the mbox. * * the cleanup can wipe out * followed by a .. * tough luck if such a stupid pattern is given. */ cleanname(mbox); if(strcmp(mbox, ".") == 0) *mbox = '\0'; if(mbox[0] == '/') *ref = '\0'; else if(*ref != '\0'){ cleanname(ref); if(strcmp(ref, ".") == 0) *ref = '\0'; }else *ref = '\0'; while(*ref && isdotdot(mbox)){ s = strrchr(ref, '/'); if(s == nil) s = ref; if(isdotdot(s)) break; *s = '\0'; mbox += 2; if(*mbox == '/') mbox++; } if(*ref == '\0'){ s = mbox; ss = s; }else{ n = strlen(ref) + strlen(mbox) + 2; t = binalloc(&parseBin, n, 0); if(t == nil) parseErr("out of memory"); snprint(t, n, "%s/%s", ref, mbox); s = t; ss = s + strlen(ref); } /* * only allow activity in /mail/box */ if(s[0] == '/' || isdotdot(s)){ Bprint(&bout, "%s NO illegal mailbox pattern\r\n", tg); return; } if(cistrcmp(cmd, "lsub") == 0) lsubBoxes(cmd, s, ss); else listBoxes(cmd, s, ss); Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); } static char* passCR(char*u, char*p) { static char Ebadch[] = "can't get challenge"; static char nchall[64]; static char response[64]; static Chalstate *ch = nil; AuthInfo *ai; again: if (ch == nil){ if(!(ch = auth_challenge("proto=p9cr role=server user=%q", u))) return Ebadch; snprint(nchall, 64, " encrypt challenge: %s", ch->chal); return nchall; } else { strncpy(response, p, 64); ch->resp = response; ch->nresp = strlen(response); ai = auth_response(ch); auth_freechal(ch); ch = nil; if (ai == nil) goto again; setupuser(ai); return nil; } } static void loginCmd(char *tg, char *cmd) { char *s, *t; AuthInfo *ai; char*r; mustBe(' '); s = astring(); /* uid */ mustBe(' '); t = astring(); /* password */ crnl(); if(allowCR){ if ((r = passCR(s, t)) == nil){ Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd); imapState = SAuthed; } else { Bprint(&bout, "* NO [ALERT] %s\r\n", r); Bprint(&bout, "%s NO %s succeeded\r\n", tg, cmd); } return; } else if(allowPass){ if(ai = passLogin(s, t)){ setupuser(ai); Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd); imapState = SAuthed; }else Bprint(&bout, "%s NO %s failed check\r\n", tg, cmd); return; } Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd); } /* * logout or x-exit, which doesn't expunge the mailbox */ static void logoutCmd(char *tg, char *cmd) { crnl(); if(cmd[0] != 'x' && selected){ closeBox(selected, 1); selected = nil; } Bprint(&bout, "* bye\r\n"); Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); exits("rob6"); exits(0); } static void namespaceCmd(char *tg, char *cmd) { crnl(); check(); /* * personal, other users, shared namespaces * send back nil or descriptions of (prefix heirarchy-delim) for each case */ Bprint(&bout, "* NAMESPACE ((\"\" \"/\")) nil nil\r\n"); Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); } static void noopCmd(char *tg, char *cmd) { crnl(); check(); Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); enableForwarding(); } /* * this is only a partial implementation * should copy files to other directories, * and copy & truncate inbox */ static void renameCmd(char *tg, char *cmd) { char *from, *to; int ok; mustBe(' '); from = astring(); mustBe(' '); to = astring(); crnl(); check(); to = mboxName(to); if(to == nil || !okMbox(to) || cistrcmp(to, "inbox") == 0){ Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd); return; } if(access(to, AEXIST) >= 0){ Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd); return; } from = mboxName(from); if(from == nil || !okMbox(from)){ Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd); return; } if(cistrcmp(from, "inbox") == 0) ok = copyBox(from, to, 0); else ok = moveBox(from, to); if(ok) Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); else Bprint(&bout, "%s NO %s failed\r\n", tg, cmd); } static void searchCmd(char *tg, char *cmd) { searchUCmd(tg, cmd, 0); } static void searchUCmd(char *tg, char *cmd, int uids) { Search rock; Msg *m; char *uid; ulong id; mustBe(' '); rock.next = nil; searchKeys(1, &rock); crnl(); uid = ""; if(uids) uid = "uid "; if(rock.next != nil && rock.next->key == SKCharset){ if(cistrcmp(rock.next->s, "utf-8") != 0 && cistrcmp(rock.next->s, "us-ascii") != 0){ Bprint(&bout, "%s NO [BADCHARSET] (\"US-ASCII\" \"UTF-8\") %s%s failed\r\n", tg, uid, cmd); checkBox(selected, 0); status(uids, uids); return; } rock.next = rock.next->next; } Bprint(&bout, "* search"); for(m = selected->msgs; m != nil; m = m->next) m->matched = searchMsg(m, rock.next); for(m = selected->msgs; m != nil; m = m->next){ if(m->matched){ if(uids) id = m->uid; else id = m->seq; Bprint(&bout, " %lud", id); } } Bprint(&bout, "\r\n"); checkBox(selected, 0); status(uids, uids); Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd); } static void selectCmd(char *tg, char *cmd) { Msg *m; char *s, *mbox; mustBe(' '); mbox = astring(); crnl(); if(selected){ imapState = SAuthed; closeBox(selected, 1); selected = nil; } mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); return; } selected = openBox(mbox, "imap", cistrcmp(cmd, "select") == 0); if(selected == nil){ Bprint(&bout, "%s NO %s can't open mailbox %s: %r\r\n", tg, cmd, mbox); return; } imapState = SSelected; Bprint(&bout, "* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n"); Bprint(&bout, "* %lud EXISTS\r\n", selected->max); selected->toldMax = selected->max; Bprint(&bout, "* %lud RECENT\r\n", selected->recent); selected->toldRecent = selected->recent; for(m = selected->msgs; m != nil; m = m->next){ if(!m->expunged && (m->flags & MSeen) != MSeen){ Bprint(&bout, "* OK [UNSEEN %ld]\r\n", m->seq); break; } } Bprint(&bout, "* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Draft \\Deleted)]\r\n"); Bprint(&bout, "* OK [UIDNEXT %ld]\r\n", selected->uidnext); Bprint(&bout, "* OK [UIDVALIDITY %ld]\r\n", selected->uidvalidity); s = "READ-ONLY"; if(selected->writable) s = "READ-WRITE"; Bprint(&bout, "%s OK [%s] %s %s completed\r\n", tg, s, cmd, mbox); } static NamedInt statusItems[] = { {"MESSAGES", SMessages}, {"RECENT", SRecent}, {"UIDNEXT", SUidNext}, {"UIDVALIDITY", SUidValidity}, {"UNSEEN", SUnseen}, {nil, 0} }; static void statusCmd(char *tg, char *cmd) { Box *box; Msg *m; char *s, *mbox; ulong v; int si, i; mustBe(' '); mbox = astring(); mustBe(' '); mustBe('('); si = 0; for(;;){ s = atom(); i = mapInt(statusItems, s); if(i == 0) parseErr("illegal status item"); si |= i; if(peekc() == ')') break; mustBe(' '); } mustBe(')'); crnl(); mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox)){ check(); Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); return; } box = openBox(mbox, "status", 1); if(box == nil){ check(); Bprint(&bout, "%s NO [TRYCREATE] %s can't open mailbox %s: %r\r\n", tg, cmd, mbox); return; } Bprint(&bout, "* STATUS %s (", mbox); s = ""; for(i = 0; statusItems[i].name != nil; i++){ if(si & statusItems[i].v){ v = 0; switch(statusItems[i].v){ case SMessages: v = box->max; break; case SRecent: v = box->recent; break; case SUidNext: v = box->uidnext; break; case SUidValidity: v = box->uidvalidity; break; case SUnseen: v = 0; for(m = box->msgs; m != nil; m = m->next) if((m->flags & MSeen) != MSeen) v++; break; default: Bprint(&bout, ")"); bye("internal error: status item not implemented"); break; } Bprint(&bout, "%s%s %lud", s, statusItems[i].name, v); s = " "; } } Bprint(&bout, ")\r\n"); closeBox(box, 1); check(); Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); } static void storeCmd(char *tg, char *cmd) { storeUCmd(tg, cmd, 0); } static void storeUCmd(char *tg, char *cmd, int uids) { Store *st; MsgSet *ms; MbLock *ml; char *uid; ulong max; int ok; mustBe(' '); ms = msgSet(uids); mustBe(' '); st = storeWhat(); crnl(); uid = ""; if(uids) uid = "uid "; max = selected->max; ml = checkBox(selected, 1); ok = ml != nil && forMsgs(selected, ms, max, uids, storeMsg, st); closeImp(selected, ml); status(uids, uids); if(ok) Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd); else Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd); } /* * minimal implementation of subscribe * all folders are automatically subscribed, * and can't be unsubscribed */ static void subscribeCmd(char *tg, char *cmd) { Box *box; char *mbox; int ok; mustBe(' '); mbox = astring(); crnl(); check(); mbox = mboxName(mbox); ok = 0; if(mbox != nil && okMbox(mbox)){ box = openBox(mbox, "subscribe", 0); if(box != nil){ ok = subscribe(mbox, 's'); closeBox(box, 1); } } if(!ok) Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); else Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); } static void uidCmd(char *tg, char *cmd) { char *sub; mustBe(' '); sub = atom(); if(cistrcmp(sub, "copy") == 0) copyUCmd(tg, sub, 1); else if(cistrcmp(sub, "fetch") == 0) fetchUCmd(tg, sub, 1); else if(cistrcmp(sub, "search") == 0) searchUCmd(tg, sub, 1); else if(cistrcmp(sub, "store") == 0) storeUCmd(tg, sub, 1); else{ clearcmd(); Bprint(&bout, "%s BAD %s illegal uid command %s\r\n", tg, cmd, sub); } } static void unsubscribeCmd(char *tg, char *cmd) { char *mbox; mustBe(' '); mbox = astring(); crnl(); check(); mbox = mboxName(mbox); if(mbox == nil || !okMbox(mbox) || !subscribe(mbox, 'u')) Bprint(&bout, "%s NO %s can't unsubscribe\r\n", tg, cmd); else Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); } static void badsyn(void) { parseErr("bad syntax"); } static void clearcmd(void) { int c; for(;;){ c = getc(); if(c < 0) bye("end of input"); if(c == '\n') return; } } static void crnl(void) { int c; c = getc(); if(c == '\n') return; if(c != '\r' || getc() != '\n') badsyn(); } static void mustBe(int c) { if(getc() != c){ ungetc(); badsyn(); } } /* * flaglist : '(' ')' | '(' flags ')' */ static int flagList(void) { int f; mustBe('('); f = 0; if(peekc() != ')') f = flags(); mustBe(')'); return f; } /* * flags : flag | flags ' ' flag * flag : '\' atom | atom */ static int flags(void) { int ff, flags; char *s; int c; flags = 0; for(;;){ c = peekc(); if(c == '\\'){ mustBe('\\'); s = atomString(atomStop, "\\"); }else if(strchr(atomStop, c) != nil) s = atom(); else break; ff = mapFlag(s); if(ff == 0) parseErr("flag not supported"); flags |= ff; if(peekc() != ' ') break; mustBe(' '); } if(flags == 0) parseErr("no flags given"); return flags; } /* * storeWhat : osign 'FLAGS' ' ' storeflags * | osign 'FLAGS.SILENT' ' ' storeflags * osign : * | '+' | '-' * storeflags : flagList | flags */ static Store* storeWhat(void) { int f; char *s; int c, w; c = peekc(); if(c == '+' || c == '-') mustBe(c); else c = 0; s = atom(); w = 0; if(cistrcmp(s, "flags") == 0) w = STFlags; else if(cistrcmp(s, "flags.silent") == 0) w = STFlagsSilent; else parseErr("illegal store attribute"); mustBe(' '); if(peekc() == '(') f = flagList(); else f = flags(); return mkStore(c, w, f); } /* * fetchWhat : "ALL" | "FULL" | "FAST" | fetchAtt | '(' fetchAtts ')' * fetchAtts : fetchAtt | fetchAtts ' ' fetchAtt */ static char *fetchAtom = "(){}%*\"\\[]"; static Fetch* fetchWhat(void) { Fetch *f; char *s; if(peekc() == '('){ getc(); f = nil; for(;;){ s = atomString(fetchAtom, ""); f = fetchAtt(s, f); if(peekc() == ')') break; mustBe(' '); } getc(); return revFetch(f); } s = atomString(fetchAtom, ""); if(cistrcmp(s, "all") == 0) f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, mkFetch(FEnvelope, nil)))); else if(cistrcmp(s, "fast") == 0) f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, nil))); else if(cistrcmp(s, "full") == 0) f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, mkFetch(FEnvelope, mkFetch(FBody, nil))))); else f = fetchAtt(s, nil); return f; } /* * fetchAtt : "ENVELOPE" | "FLAGS" | "INTERNALDATE" * | "RFC822" | "RFC822.HEADER" | "RFC822.SIZE" | "RFC822.TEXT" * | "BODYSTRUCTURE" * | "UID" * | "BODY" * | "BODY" bodysubs * | "BODY.PEEK" bodysubs * bodysubs : sect * | sect '<' number '.' nz-number '>' * sect : '[' sectSpec ']' * sectSpec : sectMsgText * | sectPart * | sectPart '.' sectText * sectPart : nz-number * | sectPart '.' nz-number */ static Fetch* fetchAtt(char *s, Fetch *f) { NList *sect; int c; if(cistrcmp(s, "envelope") == 0) return mkFetch(FEnvelope, f); if(cistrcmp(s, "flags") == 0) return mkFetch(FFlags, f); if(cistrcmp(s, "internaldate") == 0) return mkFetch(FInternalDate, f); if(cistrcmp(s, "RFC822") == 0) return mkFetch(FRfc822, f); if(cistrcmp(s, "RFC822.header") == 0) return mkFetch(FRfc822Head, f); if(cistrcmp(s, "RFC822.size") == 0) return mkFetch(FRfc822Size, f); if(cistrcmp(s, "RFC822.text") == 0) return mkFetch(FRfc822Text, f); if(cistrcmp(s, "bodystructure") == 0) return mkFetch(FBodyStruct, f); if(cistrcmp(s, "uid") == 0) return mkFetch(FUid, f); if(cistrcmp(s, "body") == 0){ if(peekc() != '[') return mkFetch(FBody, f); f = mkFetch(FBodySect, f); }else if(cistrcmp(s, "body.peek") == 0) f = mkFetch(FBodyPeek, f); else parseErr("illegal fetch attribute"); mustBe('['); c = peekc(); if(c >= '1' && c <= '9'){ sect = mkNList(number(1), nil); while(peekc() == '.'){ getc(); c = peekc(); if(c >= '1' && c <= '9'){ sect = mkNList(number(1), sect); }else{ break; } } f->sect = revNList(sect); } if(peekc() != ']') sectText(f, f->sect != nil); mustBe(']'); if(peekc() != '<') return f; f->partial = 1; mustBe('<'); f->start = number(0); mustBe('.'); f->size = number(1); mustBe('>'); return f; } /* * sectText : sectMsgText | "MIME" * sectMsgText : "HEADER" * | "TEXT" * | "HEADER.FIELDS" ' ' hdrList * | "HEADER.FIELDS.NOT" ' ' hdrList * hdrList : '(' hdrs ')' * hdrs: : astring * | hdrs ' ' astring */ static void sectText(Fetch *f, int mimeOk) { SList *h; char *s; s = atomString(fetchAtom, ""); if(cistrcmp(s, "header") == 0){ f->part = FPHead; return; } if(cistrcmp(s, "text") == 0){ f->part = FPText; return; } if(mimeOk && cistrcmp(s, "mime") == 0){ f->part = FPMime; return; } if(cistrcmp(s, "header.fields") == 0) f->part = FPHeadFields; else if(cistrcmp(s, "header.fields.not") == 0) f->part = FPHeadFieldsNot; else parseErr("illegal fetch section text"); mustBe(' '); mustBe('('); h = nil; for(;;){ h = mkSList(astring(), h); if(peekc() == ')') break; mustBe(' '); } mustBe(')'); f->hdrs = revSList(h); } /* * searchWhat : "CHARSET" ' ' astring searchkeys | searchkeys * searchkeys : searchkey | searchkeys ' ' searchkey * searchkey : "ALL" | "ANSWERED" | "DELETED" | "FLAGGED" | "NEW" | "OLD" | "RECENT" * | "SEEN" | "UNANSWERED" | "UNDELETED" | "UNFLAGGED" | "DRAFT" | "UNDRAFT" * | astrkey ' ' astring * | datekey ' ' date * | "KEYWORD" ' ' flag | "UNKEYWORD" flag * | "LARGER" ' ' number | "SMALLER" ' ' number * | "HEADER" astring ' ' astring * | set | "UID" ' ' set * | "NOT" ' ' searchkey * | "OR" ' ' searchkey ' ' searchkey * | '(' searchkeys ')' * astrkey : "BCC" | "BODY" | "CC" | "FROM" | "SUBJECT" | "TEXT" | "TO" * datekey : "BEFORE" | "ON" | "SINCE" | "SENTBEFORE" | "SENTON" | "SENTSINCE" */ static NamedInt searchMap[] = { {"ALL", SKAll}, {"ANSWERED", SKAnswered}, {"DELETED", SKDeleted}, {"FLAGGED", SKFlagged}, {"NEW", SKNew}, {"OLD", SKOld}, {"RECENT", SKRecent}, {"SEEN", SKSeen}, {"UNANSWERED", SKUnanswered}, {"UNDELETED", SKUndeleted}, {"UNFLAGGED", SKUnflagged}, {"DRAFT", SKDraft}, {"UNDRAFT", SKUndraft}, {"UNSEEN", SKUnseen}, {nil, 0} }; static NamedInt searchMapStr[] = { {"CHARSET", SKCharset}, {"BCC", SKBcc}, {"BODY", SKBody}, {"CC", SKCc}, {"FROM", SKFrom}, {"SUBJECT", SKSubject}, {"TEXT", SKText}, {"TO", SKTo}, {nil, 0} }; static NamedInt searchMapDate[] = { {"BEFORE", SKBefore}, {"ON", SKOn}, {"SINCE", SKSince}, {"SENTBEFORE", SKSentBefore}, {"SENTON", SKSentOn}, {"SENTSINCE", SKSentSince}, {nil, 0} }; static NamedInt searchMapFlag[] = { {"KEYWORD", SKKeyword}, {"UNKEYWORD", SKUnkeyword}, {nil, 0} }; static NamedInt searchMapNum[] = { {"SMALLER", SKSmaller}, {"LARGER", SKLarger}, {nil, 0} }; static Search* searchKeys(int first, Search *tail) { Search *s; for(;;){ if(peekc() == '('){ getc(); tail = searchKeys(0, tail); mustBe(')'); }else{ s = searchKey(first); tail->next = s; tail = s; } first = 0; if(peekc() != ' ') break; getc(); } return tail; } static Search* searchKey(int first) { Search *sr, rock; Tm tm; char *a; int i, c; sr = binalloc(&parseBin, sizeof(Search), 1); if(sr == nil) parseErr("out of memory"); c = peekc(); if(c >= '0' && c <= '9'){ sr->key = SKSet; sr->set = msgSet(0); return sr; } a = atom(); if(i = mapInt(searchMap, a)) sr->key = i; else if(i = mapInt(searchMapStr, a)){ if(!first && i == SKCharset) parseErr("illegal search key"); sr->key = i; mustBe(' '); sr->s = astring(); }else if(i = mapInt(searchMapDate, a)){ sr->key = i; mustBe(' '); c = peekc(); if(c == '"') getc(); a = atom(); if(!imap4Date(&tm, a)) parseErr("bad date format"); sr->year = tm.year; sr->mon = tm.mon; sr->mday = tm.mday; if(c == '"') mustBe('"'); }else if(i = mapInt(searchMapFlag, a)){ sr->key = i; mustBe(' '); c = peekc(); if(c == '\\'){ mustBe('\\'); a = atomString(atomStop, "\\"); }else a = atom(); i = mapFlag(a); if(i == 0) parseErr("flag not supported"); sr->num = i; }else if(i = mapInt(searchMapNum, a)){ sr->key = i; mustBe(' '); sr->num = number(0); }else if(cistrcmp(a, "HEADER") == 0){ sr->key = SKHeader; mustBe(' '); sr->hdr = astring(); mustBe(' '); sr->s = astring(); }else if(cistrcmp(a, "UID") == 0){ sr->key = SKUid; mustBe(' '); sr->set = msgSet(0); }else if(cistrcmp(a, "NOT") == 0){ sr->key = SKNot; mustBe(' '); rock.next = nil; searchKeys(0, &rock); sr->left = rock.next; }else if(cistrcmp(a, "OR") == 0){ sr->key = SKOr; mustBe(' '); rock.next = nil; searchKeys(0, &rock); sr->left = rock.next; mustBe(' '); rock.next = nil; searchKeys(0, &rock); sr->right = rock.next; }else parseErr("illegal search key"); return sr; } /* * set : seqno * | seqno ':' seqno * | set ',' set * seqno: nz-number * | '*' * */ static MsgSet* msgSet(int uids) { MsgSet head, *last, *ms; ulong from, to; last = &head; head.next = nil; for(;;){ from = uids ? uidNo() : seqNo(); to = from; if(peekc() == ':'){ getc(); to = uids ? uidNo() : seqNo(); } ms = binalloc(&parseBin, sizeof(MsgSet), 0); if(ms == nil) parseErr("out of memory"); ms->from = from; ms->to = to; ms->next = nil; last->next = ms; last = ms; if(peekc() != ',') break; getc(); } return head.next; } static ulong seqNo(void) { if(peekc() == '*'){ getc(); return ~0UL; } return number(1); } static ulong uidNo(void) { if(peekc() == '*'){ getc(); return ~0UL; } return number(0); } /* * 7 bit, non-ctl chars, no (){%*"\ * NIL is special case for nstring or parenlist */ static char * atom(void) { return atomString(atomStop, ""); } /* * like an atom, but no + */ static char * tag(void) { return atomString("+(){%*\"\\", ""); } /* * string or atom allowing %* */ static char * listmbox(void) { int c; c = peekc(); if(c == '{') return literal(); if(c == '"') return quoted(); return atomString("(){\"\\", ""); } /* * string or atom */ static char * astring(void) { int c; c = peekc(); if(c == '{') return literal(); if(c == '"') return quoted(); return atom(); } /* * 7 bit, non-ctl chars, none from exception list */ static char * atomString(char *disallowed, char *initial) { char *s; int c, ns, as; ns = strlen(initial); s = binalloc(&parseBin, ns + StrAlloc, 0); if(s == nil) parseErr("out of memory"); strcpy(s, initial); as = ns + StrAlloc; for(;;){ c = getc(); if(c <= ' ' || c >= 0x7f || strchr(disallowed, c) != nil){ ungetc(); break; } s[ns++] = c; if(ns >= as){ s = bingrow(&parseBin, s, as, as + StrAlloc, 0); if(s == nil) parseErr("out of memory"); as += StrAlloc; } } if(ns == 0) badsyn(); s[ns] = '\0'; return s; } /* * quoted: '"' chars* '"' * chars: 1-128 except \r and \n */ static char * quoted(void) { char *s; int c, ns, as; mustBe('"'); s = binalloc(&parseBin, StrAlloc, 0); if(s == nil) parseErr("out of memory"); as = StrAlloc; ns = 0; for(;;){ c = getc(); if(c == '"') break; if(c < 1 || c > 0x7f || c == '\r' || c == '\n') badsyn(); if(c == '\\'){ c = getc(); if(c != '\\' && c != '"') badsyn(); } s[ns++] = c; if(ns >= as){ s = bingrow(&parseBin, s, as, as + StrAlloc, 0); if(s == nil) parseErr("out of memory"); as += StrAlloc; } } s[ns] = '\0'; return s; } /* * litlen: {number}\r\n */ static ulong litlen(void) { ulong v; mustBe('{'); v = number(0); mustBe('}'); crnl(); return v; } /* * literal: litlen data<0:litlen> */ static char * literal(void) { char *s; ulong v; v = litlen(); s = binalloc(&parseBin, v+1, 0); if(s == nil) parseErr("out of memory"); Bprint(&bout, "+ Ready for literal data\r\n"); if(Bflush(&bout) < 0) writeErr(); if(v != 0 && Bread(&bin, s, v) != v) badsyn(); s[v] = '\0'; return s; } /* * digits; number is 32 bits */ static ulong number(int nonzero) { ulong v; int c, first; v = 0; first = 1; for(;;){ c = getc(); if(c < '0' || c > '9'){ ungetc(); if(first) badsyn(); break; } if(nonzero && first && c == '0') badsyn(); c -= '0'; first = 0; if(v > UlongMax/10 || v == UlongMax/10 && c > UlongMax%10) parseErr("number out of range\r\n"); v = v * 10 + c; } return v; } static int getc(void) { return Bgetc(&bin); } static void ungetc(void) { Bungetc(&bin); } static int peekc(void) { int c; c = Bgetc(&bin); Bungetc(&bin); return c; }