summaryrefslogtreecommitdiff
path: root/sys/src/cmd/upas/imap4d/imap4d.c
diff options
context:
space:
mode:
authorcinap_lenrek <cinap_lenrek@felloff.net>2017-03-12 17:15:03 +0100
committercinap_lenrek <cinap_lenrek@felloff.net>2017-03-12 17:15:03 +0100
commit963cfc9a6f6e721f52aa949e6d1af0c3e8dc2ecc (patch)
tree749b74875dbc49bcf6ed0776648b8f0ef9417407 /sys/src/cmd/upas/imap4d/imap4d.c
parent8177d20fb2709ba9290dfd41308b8e5bee4e00f8 (diff)
merging erik quanstros nupas
Diffstat (limited to 'sys/src/cmd/upas/imap4d/imap4d.c')
-rw-r--r--sys/src/cmd/upas/imap4d/imap4d.c2292
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