#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(cistrstr(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