diff options
author | cinap_lenrek <cinap_lenrek@felloff.net> | 2017-03-12 17:15:03 +0100 |
---|---|---|
committer | cinap_lenrek <cinap_lenrek@felloff.net> | 2017-03-12 17:15:03 +0100 |
commit | 963cfc9a6f6e721f52aa949e6d1af0c3e8dc2ecc (patch) | |
tree | 749b74875dbc49bcf6ed0776648b8f0ef9417407 /sys/src/cmd/upas/imap4d/imap4d.c | |
parent | 8177d20fb2709ba9290dfd41308b8e5bee4e00f8 (diff) |
merging erik quanstros nupas
Diffstat (limited to 'sys/src/cmd/upas/imap4d/imap4d.c')
-rw-r--r-- | sys/src/cmd/upas/imap4d/imap4d.c | 2292 |
1 files changed, 2292 insertions, 0 deletions
diff --git a/sys/src/cmd/upas/imap4d/imap4d.c b/sys/src/cmd/upas/imap4d/imap4d.c new file mode 100644 index 000000000..94fa0275e --- /dev/null +++ b/sys/src/cmd/upas/imap4d/imap4d.c @@ -0,0 +1,2292 @@ +#include "imap4d.h" + +/* + * these should be in libraries + */ +char *csquery(char *attr, char *val, char *rattr); + +/* + * implemented: + * /lib/rfc/rfc3501 imap4rev1 + * /lib/rfc/rfc2683 implementation advice + * /lib/rfc/rfc2342 namespace capability + * /lib/rfc/rfc2222 security protocols + * /lib/rfc/rfc1731 security protocols + * /lib/rfc/rfc2177 idle capability + * /lib/rfc/rfc2195 cram-md5 authentication + * /lib/rfc/rfc4315 uidplus capability + * + * not implemented, priority: + * /lib/rfc/rfc5256 sort and thread + * requires missing support from upas/fs. + * + * not implemented, low priority: + * /lib/rfc/rfc2088 literal+ capability + * /lib/rfc/rfc2221 login-referrals + * /lib/rfc/rfc2193 mailbox-referrals + * /lib/rfc/rfc1760 s/key authentication + * + */ + +typedef struct Parsecmd Parsecmd; +struct Parsecmd +{ + char *name; + void (*f)(char*, char*); +}; + +static void appendcmd(char*, char*); +static void authenticatecmd(char*, char*); +static void capabilitycmd(char*, char*); +static void closecmd(char*, char*); +static void copycmd(char*, char*); +static void createcmd(char*, char*); +static void deletecmd(char*, char*); +static void expungecmd(char*, char*); +static void fetchcmd(char*, char*); +static void getquotacmd(char*, char*); +static void getquotarootcmd(char*, char*); +static void idlecmd(char*, char*); +static void listcmd(char*, char*); +static void logincmd(char*, char*); +static void logoutcmd(char*, char*); +static void namespacecmd(char*, char*); +static void noopcmd(char*, char*); +static void renamecmd(char*, char*); +static void searchcmd(char*, char*); +static void selectcmd(char*, char*); +static void setquotacmd(char*, char*); +static void statuscmd(char*, char*); +static void storecmd(char*, char*); +static void subscribecmd(char*, char*); +static void uidcmd(char*, char*); +static void unsubscribecmd(char*, char*); +static void xdebugcmd(char*, char*); +static void copyucmd(char*, char*, int); +static void fetchucmd(char*, char*, int); +static void searchucmd(char*, char*, int); +static void storeucmd(char*, char*, int); + +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 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 uint litlen(void); +static Msgset *msgset(int); +static void mustbe(int c); +static uint number(int nonzero); +static int peekc(void); +static char *quoted(void); +static void secttext(Fetch *, int); +static uint seqno(void); +static Store *storewhat(void); +static char *tag(void); +static uint uidno(void); +static void ungetc(void); + +static Parsecmd Snonauthed[] = +{ + {"capability", capabilitycmd}, + {"logout", logoutcmd}, + {"noop", noopcmd}, + {"x-exit", logoutcmd}, + + {"authenticate", authenticatecmd}, + {"login", logincmd}, + + nil +}; + +static Parsecmd Sauthed[] = +{ + {"capability", capabilitycmd}, + {"logout", logoutcmd}, + {"noop", noopcmd}, + {"x-exit", logoutcmd}, + {"xdebug", xdebugcmd}, + + {"append", appendcmd}, + {"create", createcmd}, + {"delete", deletecmd}, + {"examine", selectcmd}, + {"select", selectcmd}, + {"idle", idlecmd}, + {"list", listcmd}, + {"lsub", listcmd}, + {"namespace", namespacecmd}, + {"rename", renamecmd}, + {"setquota", setquotacmd}, + {"getquota", getquotacmd}, + {"getquotaroot", getquotarootcmd}, + {"status", statuscmd}, + {"subscribe", subscribecmd}, + {"unsubscribe", unsubscribecmd}, + + nil +}; + +static Parsecmd Sselected[] = +{ + {"capability", capabilitycmd}, + {"xdebug", xdebugcmd}, + {"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 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[Userlen]; +char mboxdir[Pathlen]; +char *servername; +char *site; +char *remote; +char *binupas; +Box *selected; +Bin *parsebin; +int debug; +Uidplus *uidlist; +Uidplus **uidtl; + +void +usage(void) +{ + fprint(2, "usage: upas/imap4d [-acpv] [-l logfile] [-b binupas] [-d site] [-r remotehost] [-s servername]\n"); + bye("usage"); +} + +void +main(int argc, char *argv[]) +{ + int preauth; + + Binit(&bin, dup(0, -1), OREAD); + close(0); + Binit(&bout, 1, OWRITE); + quotefmtinstall(); + fmtinstall('F', Ffmt); + fmtinstall('D', Dfmt); /* rfc822; # imap date %Z */ + fmtinstall(L'δ', Dfmt); /* rfc822; # imap date %s */ + fmtinstall('X', Xfmt); + fmtinstall('Y', Zfmt); + fmtinstall('Z', Zfmt); + + preauth = 0; + allowpass = 0; + allowcr = 0; + ARGBEGIN{ + case 'a': + preauth = 1; + break; + case 'b': + binupas = EARGF(usage()); + break; + case 'c': + allowcr = 1; + break; + case 'd': + site = EARGF(usage()); + break; + case 'l': + snprint(logfile, sizeof logfile, "%s", EARGF(usage())); + break; + case 'p': + allowpass = 1; + break; + case 'r': + remote = EARGF(usage()); + break; + case 's': + servername = EARGF(usage()); + break; + case 'v': + debug ^= 1; + break; + default: + usage(); + break; + }ARGEND + + if(allowpass && allowcr){ + fprint(2, "imap4d: -c and -p are mutually exclusive\n"); + 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) + site = getenv("site"); + if(site == nil){ + site = strchr(servername, '.'); + if(site) + site++; + else + site = servername; + } + + 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(); + + 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(0); +} + +void +parseerr(char *msg) +{ + debuglog("parse error: %s", 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 *, char *msg) +{ + 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; + + debuglog("cleaner"); + if(idlepid < 0) + return; + exiting = 1; + close(0); + close(1); + close(2); + close(bin.fid); + bin.fid = -1; + /* + * 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, "* %ud EXISTS\r\n", selected->max); + selected->toldmax = selected->max; + } + if(tell || selected->toldrecent != selected->recent){ + Bprint(&bout, "* %ud 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]; + uint t, n, now; + int flags, ok; + Uidplus u; + + 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){ + check(); + Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); + return; + } + /* bug. this is upas/fs's job */ + 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, &u); + crnl(); + check(); + if(ok) + Bprint(&bout, "%s OK [APPENDUID %ud %ud] %s completed\r\n", + tg, u.uidvalidity, u.uid, 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(); + if(cistrcmp(s, "cram-md5") == 0){ + crnl(); + 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 if(cistrcmp(s, "plain") == 0){ + s = nil; + if(peekc() == ' '){ + mustbe(' '); + s = astring(); + } + crnl(); + if(!allowpass) + Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd); + else if(t = plainauth(s)) + Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t); + else{ + Bprint(&bout, "%s OK %s\r\n", tg, cmd); + imapstate = Sauthed; + } + }else + Bprint(&bout, "%s NO %s unsupported authentication protocol\r\n", tg, cmd); +} + +static void +capabilitycmd(char *tg, char *cmd) +{ + crnl(); + check(); + Bprint(&bout, "* CAPABILITY IMAP4REV1 IDLE NAMESPACE QUOTA XDEBUG"); + Bprint(&bout, " UIDPLUS"); + if(allowpass || allowcr) + Bprint(&bout, " AUTH=CRAM-MD5 AUTH=PLAIN"); + else + Bprint(&bout, " LOGINDISABLED AUTH=CRAM-MD5"); + Bprint(&bout, "\r\n%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 char *uidpsep; +static int +printuid(Box*, Msg *m, int, void*) +{ + Bprint(&bout, "%s%ud", uidpsep, m->uid); + uidpsep = ","; + return 1; +} + +static void +copyucmd(char *tg, char *cmd, int uids) +{ + char *uid, *mbox; + int ok; + uint max; + Msgset *ms; + Uidplus *u; + + mustbe(' '); + ms = msgset(uids); + mustbe(' '); + mbox = astring(); + crnl(); + + uid = ""; + if(uids) + uid = "UID "; + + mbox = mboxname(mbox); + if(mbox == nil){ + 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; + } + + uidlist = 0; + uidtl = &uidlist; + + max = selected->max; + checkbox(selected, 0); + ok = formsgs(selected, ms, max, uids, copycheck, nil); + if(ok) + ok = formsgs(selected, ms, max, uids, copysaveu, mbox); + status(1, uids); + if(ok && uidlist){ + u = uidlist; + Bprint(&bout, "%s OK [COPYUID %ud", tg, u->uidvalidity); + uidpsep = " "; + formsgs(selected, ms, max, uids, printuid, mbox); + Bprint(&bout, " %ud", u->uid); + for(u = u->next; u; u = u->next) + Bprint(&bout, ",%ud", u->uid); + Bprint(&bout, "] %s%s completed\r\n", uid, cmd); + }else 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; + + mustbe(' '); + mbox = astring(); + crnl(); + check(); + + mbox = mboxname(mbox); + if(mbox == nil){ + Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); + return; + } + if(cistrcmp(mbox, "mbox") == 0){ + Bprint(&bout, "%s NO %s cannot remotely create INBOX\r\n", tg, cmd); + return; + } + if(creatembox(mbox) == -1) + Bprint(&bout, "%s NO %s cannot create mailbox %#Y\r\n", tg, cmd, mbox); + else + Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd); +} + +static void +xdebugcmd(char *tg, char *) +{ + char *s, *t; + + mustbe(' '); + s = astring(); + t = 0; + if(!cistrcmp(s, "file")){ + mustbe(' '); + t = astring(); + } + crnl(); + check(); + if(!cistrcmp(s, "on") || !cistrcmp(s, "1")){ + Bprint(&bout, "%s OK debug on\r\n", tg); + debug = 1; + }else if(!cistrcmp(s, "file")){ + if(!strstr(t, "..")) + snprint(logfile, sizeof logfile, "%s", t); + Bprint(&bout, "%s OK debug file %#Z\r\n", tg, logfile); + }else{ + Bprint(&bout, "%s OK debug off\r\n", tg); + debug = 0; + } +} + +static void +deletecmd(char *tg, char *cmd) +{ + char *mbox; + + mustbe(' '); + mbox = astring(); + crnl(); + check(); + + mbox = mboxname(mbox); + if(mbox == nil){ + Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); + return; + } + + /* + * i don't know if this is a hack or not. a delete of the + * currently-selected box seems fishy. the standard doesn't + * specify any behavior. + */ + if(selected && strcmp(selected->name, mbox) == 0){ + ilog("delete: client bug? close of selected mbox %s", selected->fs); + imapstate = Sauthed; + closebox(selected, 1); + selected = nil; + setname("[none]"); + } + + if(!cistrcmp(mbox, "mbox") || !removembox(mbox) == -1) + Bprint(&bout, "%s NO %s cannot delete mailbox %#Y\r\n", tg, cmd, mbox); + else + Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd); +} + +static void +expungeucmd(char *tg, char *cmd, int uids) +{ + int ok; + Msgset *ms; + + ms = 0; + if(uids){ + mustbe(' '); + ms = msgset(uids); + } + crnl(); + ok = deletemsg(selected, ms); + check(); + if(ok) + Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); + else + Bprint(&bout, "%s NO %s some messages not expunged\r\n", tg, cmd); +} + +static void +expungecmd(char *tg, char *cmd) +{ + expungeucmd(tg, cmd, 0); +} + +static void +fetchcmd(char *tg, char *cmd) +{ + fetchucmd(tg, cmd, 0); +} + +static void +fetchucmd(char *tg, char *cmd, int uids) +{ + char *uid; + int ok; + uint max; + Fetch *f; + Msgset *ms; + Mblock *ml; + + 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{ + if(ml == nil) + ilog("nil maillock\n"); + 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){ + setname("imap idle"); + 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(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(0); + } + if(c == '\n') + break; + } + + qlock(&imaplock); + if(exiting) + _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, *ref, *mbox; + + mustbe(' '); + s = astring(); + mustbe(' '); + t = listmbox(); + crnl(); + check(); + ref = mutf7str(s); + mbox = mutf7str(t); + if(ref == nil || mbox == nil){ + Bprint(&bout, "%s BAD %s 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; + } + + /* + * hairy exception: these take non-fsencoded strings. BUG? + */ + if(cistrcmp(cmd, "lsub") == 0) + lsubboxes(cmd, ref, mbox); + else + listboxes(cmd, ref, mbox); + Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); +} + +static void +logincmd(char *tg, char *cmd) +{ + char *r, *s, *t; + + mustbe(' '); + s = astring(); /* uid */ + mustbe(' '); + t = astring(); /* password */ + crnl(); + if(allowcr){ + if(r = crauth(s, t)){ + Bprint(&bout, "* NO [ALERT] %s\r\n", r); + Bprint(&bout, "%s NO %s succeeded\r\n", tg, cmd); + }else{ + Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd); + imapstate = Sauthed; + } + return; + }else if(allowpass){ + if(r = passauth(s, t)) + Bprint(&bout, "%s NO %s failed check [%s]\r\n", tg, cmd, r); + else{ + Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd); + imapstate = Sauthed; + } + 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(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(); +} + +static void +getquota0(char *tg, char *cmd, char *r) +{ +extern vlong getquota(void); + vlong v; + + if(r[0]){ + Bprint(&bout, "%s NO %s no such quota root\r\n", tg, cmd); + return; + } + v = getquota(); + if(v == -1){ + Bprint(&bout, "%s NO %s bad [%r]\r\n", tg, cmd); + return; + } + Bprint(&bout, "* %s "" (storage %llud %d)\r\n", cmd, v/1024, 256*1024); + Bprint(&bout, "%s OK %s completed\r\n", tg, cmd); +} + +static void +getquotacmd(char *tg, char *cmd) +{ + char *r; + + mustbe(' '); + r = astring(); + crnl(); + check(); + getquota0(tg, cmd, r); +} + +static void +getquotarootcmd(char *tg, char *cmd) +{ + char *r; + + mustbe(' '); + r = astring(); + crnl(); + check(); + + Bprint(&bout, "* %s %s \"\"\r\n", cmd, r); + getquota0(tg, cmd, ""); +} + +static void +setquotacmd(char *tg, char *cmd) +{ + mustbe(' '); + astring(); + mustbe(' '); + mustbe('('); + for(;;){ + astring(); + mustbe(' '); + number(0); + if(peekc() == ')') + break; + } + getc(); + crnl(); + check(); + Bprint(&bout, "%s NO %s error: can't set that data\r\n", tg, cmd); +} + +/* + * 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; + + mustbe(' '); + from = astring(); + mustbe(' '); + to = astring(); + crnl(); + check(); + + to = mboxname(to); + if(to == nil || cistrcmp(to, "mbox") == 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){ + Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd); + return; + } + if(renamebox(from, to, strcmp(from, "mbox"))) + 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); +} + +/* + * mail.app has a vicious habit of appending a message to + * a folder and then immediately searching for it by message-id. + * for a 10,000 message sent folder, this can be quite painful. + * + * evil strategy. for message-id searches, check the last + * message in the mailbox! if that fails, use the normal algorithm. + */ +static Msg* +mailappsucks(Search *s) +{ + Msg *m; + + if(s->key == SKuid) + s = s->next; + if(s && s->next == nil) + if(s->key == SKheader && cistrcmp(s->hdr, "message-id") == 0){ + for(m = selected->msgs; m && m->next; m = m->next) + ; + if(m != nil) + if(m->matched = searchmsg(m, s, 0)) + return m; + } + return 0; +} + +static void +searchucmd(char *tg, char *cmd, int uids) +{ + char *uid; + uint id, ld; + Msg *m; + Search rock; + + mustbe(' '); + rock.next = nil; + searchkeys(1, &rock); + crnl(); + uid = ""; + if(uids) + uid = "UID "; /* android needs caps */ + 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"); + if(m = mailappsucks(rock.next)) + goto cheat; + ld = searchld(rock.next); + for(m = selected->msgs; m != nil; m = m->next) + m->matched = searchmsg(m, rock.next, ld); + for(m = selected->msgs; m != nil; m = m->next){ +cheat: + if(m->matched){ + if(uids) + id = m->uid; + else + id = m->seq; + Bprint(&bout, " %ud", 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) +{ + char *s, *m0, *mbox, buf[Pathlen]; + Msg *m; + + mustbe(' '); + m0 = astring(); + crnl(); + + if(selected){ + imapstate = Sauthed; + closebox(selected, 1); + selected = nil; + setname("[none]"); + } + debuglog("select %s", m0); + + mbox = mboxname(m0); + if(mbox == nil){ + debuglog("select %s [%s] -> no bad", mbox, m0); + 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 %#Y: %r\r\n", tg, cmd, mbox); + return; + } + + setname("%s", decfs(buf, sizeof buf, selected->name)); + imapstate = Sselected; + + Bprint(&bout, "* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n"); + Bprint(&bout, "* %ud EXISTS\r\n", selected->max); + selected->toldmax = selected->max; + Bprint(&bout, "* %ud RECENT\r\n", selected->recent); + selected->toldrecent = selected->recent; + for(m = selected->msgs; m != nil; m = m->next){ + if(!m->expunged && (m->flags & Fseen) != Fseen){ + Bprint(&bout, "* OK [UNSEEN %ud]\r\n", m->seq); + break; + } + } + Bprint(&bout, "* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Draft \\Deleted)]\r\n"); + Bprint(&bout, "* OK [UIDNEXT %ud]\r\n", selected->uidnext); + Bprint(&bout, "* OK [UIDVALIDITY %ud]\r\n", selected->uidvalidity); + s = "READ-ONLY"; + if(selected->writable) + s = "READ-WRITE"; + Bprint(&bout, "%s OK [%s] %s %#Y 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) +{ + char *s, *mbox; + int si, i, opened; + uint v; + Box *box; + Msg *m; + + 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){ + check(); + Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd); + return; + } + + opened = 0; + if(selected && !strcmp(mbox, selected->name)) + box = selected; + else{ + box = openbox(mbox, "status", 1); + if(box == nil){ + check(); + Bprint(&bout, "%s NO [TRYCREATE] %s can't open mailbox %#Y: %r\r\n", tg, cmd, mbox); + return; + } + opened = 1; + } + + Bprint(&bout, "* STATUS %#Y (", 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 & Fseen) != Fseen) + v++; + break; + default: + Bprint(&bout, ")"); + bye("internal error: status item not implemented"); + break; + } + Bprint(&bout, "%s%s %ud", s, statusitems[i].name, v); + s = " "; + } + Bprint(&bout, ")\r\n"); + if(opened) + 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) +{ + char *uid; + int ok; + uint max; + Mblock *ml; + Msgset *ms; + Store *st; + + 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) +{ + char *mbox; + int ok; + Box *box; + + mustbe(' '); + mbox = astring(); + crnl(); + check(); + mbox = mboxname(mbox); + ok = 0; + if(mbox != nil && (box = openbox(mbox, "subscribe", 0))){ + 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 if(cistrcmp(sub, "expunge") == 0) + expungeucmd(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 || !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 char *gbuf; +static void +badsyn(void) +{ + debuglog("syntax error [%s]", gbuf); + 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) +{ + int x; + + if((x = getc()) != c){ + ungetc(); + ilog("must be '%c' got %c", c, x); + 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) +{ + char *s; + int ff, flags, 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) +{ + char *s; + int c, f, 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) +{ + char *s; + Fetch *f; + + 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 + */ +Nlist* +mknlist(void) +{ + Nlist *nl; + + nl = binalloc(&parsebin, sizeof *nl, 1); + if(nl == nil) + parseerr("out of memory"); + nl->n = number(1); + return nl; +} + +static Fetch* +fetchatt(char *s, Fetch *f) +{ + int c; + Nlist *n; + + 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'){ + n = f->sect = mknlist(); + while(peekc() == '.'){ + getc(); + c = peekc(); + if(c < '1' || c > '9') + break; + n->next = mknlist(); + n = n->next; + } + } + 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) +{ + char *s; + Slist *h; + + 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) +{ + char *a; + int i, c; + Search *sr, rock; + Tm tm; + + sr = binalloc(&parsebin, sizeof *sr, 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(a == nil || !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) +{ + uint from, to; + Msgset head, *last, *ms; + + last = &head; + head.next = nil; + for(;;){ + from = uids ? uidno() : seqno(); + to = from; + if(peekc() == ':'){ + getc(); + to = uids? uidno(): seqno(); + } + ms = binalloc(&parsebin, sizeof *ms, 0); + if(ms == nil) + parseerr("out of memory"); + if(to < from){ + ms->from = to; + ms->to = from; + }else{ + ms->from = from; + ms->to = to; + } + ms->next = nil; + last->next = ms; + last = ms; + if(peekc() != ',') + break; + getc(); + } + return head.next; +} + +static uint +seqno(void) +{ + if(peekc() == '*'){ + getc(); + return ~0UL; + } + return number(1); +} + +static uint +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 uint +litlen(void) +{ + uint v; + + mustbe('{'); + v = number(0); + mustbe('}'); + crnl(); + return v; +} + +/* + * literal: litlen data<0:litlen> + */ +static char* +literal(void) +{ + char *s; + uint 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 + */ +enum{ + Max = 0xffffffff/10, +}; + +static uint +number(int nonzero) +{ + uint n, nn; + int c, first, ovfl; + + n = 0; + first = 1; + ovfl = 0; + for(;;){ + c = getc(); + if(c < '0' || c > '9'){ + ungetc(); + if(first) + badsyn(); + break; + } + c -= '0'; + first = 0; + if(n > Max) + ovfl = 1; + nn = n*10 + c; + if(nn < n) + ovfl = 1; + n = nn; + } + if(nonzero && n == 0) + badsyn(); + if(ovfl) + parseerr("number out of range\r\n"); + return n; +} + +static void +logit(char *o) +{ + char *s, *p, *q; + + if(!debug) + return; + s = strdup(o); + p = strchr(s, ' '); + if(!p) + goto emit; + q = strchr(++p, ' '); + if(!q) + goto emit; + if(!cistrncmp(p, "login", 5)){ + q = strchr(++q, ' '); + if(!q) + goto emit; + for(q = q + 1; *q != ' ' && *q; q++) + *q = '*'; + } +emit: + for(p = s + strlen(s) - 1; p >= s && (/**p == '\r' ||*/ *p == '\n'); ) + *p-- = 0; + ilog("%s", s); + free(s); +} + +static char *gbuf; +static char *gbufp = ""; + +static int +getc(void) +{ + if(*gbufp == 0){ + free(gbuf); + werrstr(""); + gbufp = gbuf = Brdstr(&bin, '\n', 0); + if(gbuf == 0){ + ilog("empty line [%d]: %r", bin.fid); + gbufp = ""; + return -1; + } + logit(gbuf); + } + return *gbufp++; +} + +static void +ungetc(void) +{ + if(gbufp > gbuf) + gbufp--; +} + +static int +peekc(void) +{ + return *gbufp; +} + +#ifdef normal + +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; +} +#endif |