| #include "common.h" |
| #include <ctype.h> |
| #include <auth.h> |
| #include <libsec.h> |
| |
| typedef struct Cmd Cmd; |
| struct Cmd |
| { |
| char *name; |
| int needauth; |
| int (*f)(char*); |
| }; |
| |
| static void hello(void); |
| static int apopcmd(char*); |
| static int capacmd(char*); |
| static int delecmd(char*); |
| static int listcmd(char*); |
| static int noopcmd(char*); |
| static int passcmd(char*); |
| static int quitcmd(char*); |
| static int rsetcmd(char*); |
| static int retrcmd(char*); |
| static int statcmd(char*); |
| static int stlscmd(char*); |
| static int topcmd(char*); |
| static int synccmd(char*); |
| static int uidlcmd(char*); |
| static int usercmd(char*); |
| static char *nextarg(char*); |
| static int getcrnl(char*, int); |
| static int readmbox(char*); |
| static void sendcrnl(char*, ...); |
| static int senderr(char*, ...); |
| static int sendok(char*, ...); |
| #pragma varargck argpos sendcrnl 1 |
| #pragma varargck argpos senderr 1 |
| #pragma varargck argpos sendok 1 |
| |
| Cmd cmdtab[] = |
| { |
| "apop", 0, apopcmd, |
| "capa", 0, capacmd, |
| "dele", 1, delecmd, |
| "list", 1, listcmd, |
| "noop", 0, noopcmd, |
| "pass", 0, passcmd, |
| "quit", 0, quitcmd, |
| "rset", 0, rsetcmd, |
| "retr", 1, retrcmd, |
| "stat", 1, statcmd, |
| "stls", 0, stlscmd, |
| "sync", 1, synccmd, |
| "top", 1, topcmd, |
| "uidl", 1, uidlcmd, |
| "user", 0, usercmd, |
| 0, 0, 0 |
| }; |
| |
| static Biobuf in; |
| static Biobuf out; |
| static int passwordinclear; |
| static int didtls; |
| |
| typedef struct Msg Msg; |
| struct Msg |
| { |
| int upasnum; |
| char digest[64]; |
| int bytes; |
| int deleted; |
| }; |
| |
| static int totalbytes; |
| static int totalmsgs; |
| static Msg *msg; |
| static int nmsg; |
| static int loggedin; |
| static int debug; |
| static uchar *tlscert; |
| static int ntlscert; |
| static char *peeraddr; |
| static char tmpaddr[64]; |
| |
| void |
| usage(void) |
| { |
| fprint(2, "usage: upas/pop3 [-a authmboxfile] [-d debugfile] [-p]\n"); |
| exits("usage"); |
| } |
| |
| void |
| main(int argc, char **argv) |
| { |
| int fd; |
| char *arg, cmdbuf[1024]; |
| Cmd *c; |
| |
| rfork(RFNAMEG); |
| Binit(&in, 0, OREAD); |
| Binit(&out, 1, OWRITE); |
| |
| ARGBEGIN{ |
| case 'a': |
| loggedin = 1; |
| if(readmbox(EARGF(usage())) < 0) |
| exits(nil); |
| break; |
| case 'd': |
| debug++; |
| if((fd = create(EARGF(usage()), OWRITE, 0666)) >= 0 && fd != 2){ |
| dup(fd, 2); |
| close(fd); |
| } |
| break; |
| case 'r': |
| strecpy(tmpaddr, tmpaddr+sizeof tmpaddr, EARGF(usage())); |
| if(arg = strchr(tmpaddr, '!')) |
| *arg = '\0'; |
| peeraddr = tmpaddr; |
| break; |
| case 't': |
| tlscert = readcert(EARGF(usage()), &ntlscert); |
| if(tlscert == nil){ |
| senderr("cannot read TLS certificate: %r"); |
| exits(nil); |
| } |
| break; |
| case 'p': |
| passwordinclear = 1; |
| break; |
| }ARGEND |
| |
| /* do before TLS */ |
| if(peeraddr == nil) |
| peeraddr = remoteaddr(0,0); |
| |
| hello(); |
| |
| while(Bflush(&out), getcrnl(cmdbuf, sizeof cmdbuf) > 0){ |
| arg = nextarg(cmdbuf); |
| for(c=cmdtab; c->name; c++) |
| if(cistrcmp(c->name, cmdbuf) == 0) |
| break; |
| if(c->name == 0){ |
| senderr("unknown command %s", cmdbuf); |
| continue; |
| } |
| if(c->needauth && !loggedin){ |
| senderr("%s requires authentication", cmdbuf); |
| continue; |
| } |
| (*c->f)(arg); |
| } |
| exits(nil); |
| } |
| |
| /* sort directories in increasing message number order */ |
| static int |
| dircmp(void *a, void *b) |
| { |
| return atoi(((Dir*)a)->name) - atoi(((Dir*)b)->name); |
| } |
| |
| static int |
| readmbox(char *box) |
| { |
| int fd, i, n, nd, lines, pid; |
| char buf[100], err[ERRMAX]; |
| char *p; |
| Biobuf *b; |
| Dir *d, *draw; |
| Msg *m; |
| Waitmsg *w; |
| |
| unmount(nil, "/mail/fs"); |
| switch(pid = fork()){ |
| case -1: |
| return senderr("can't fork to start upas/fs"); |
| |
| case 0: |
| close(0); |
| close(1); |
| open("/dev/null", OREAD); |
| open("/dev/null", OWRITE); |
| execl("/bin/upas/fs", "upas/fs", "-np", "-f", box, nil); |
| snprint(err, sizeof err, "upas/fs: %r"); |
| _exits(err); |
| break; |
| |
| default: |
| break; |
| } |
| |
| if((w = wait()) == nil || w->pid != pid || w->msg[0] != '\0'){ |
| if(w && w->pid==pid) |
| return senderr("%s", w->msg); |
| else |
| return senderr("can't initialize upas/fs"); |
| } |
| free(w); |
| |
| if(chdir("/mail/fs/mbox") < 0) |
| return senderr("can't initialize upas/fs: %r"); |
| |
| if((fd = open(".", OREAD)) < 0) |
| return senderr("cannot open /mail/fs/mbox: %r"); |
| nd = dirreadall(fd, &d); |
| close(fd); |
| if(nd < 0) |
| return senderr("cannot read from /mail/fs/mbox: %r"); |
| |
| msg = mallocz(sizeof(Msg)*nd, 1); |
| if(msg == nil) |
| return senderr("out of memory"); |
| |
| if(nd == 0) |
| return 0; |
| qsort(d, nd, sizeof(d[0]), dircmp); |
| |
| for(i=0; i<nd; i++){ |
| m = &msg[nmsg]; |
| m->upasnum = atoi(d[i].name); |
| sprint(buf, "%d/digest", m->upasnum); |
| if((fd = open(buf, OREAD)) < 0) |
| continue; |
| n = readn(fd, m->digest, sizeof m->digest - 1); |
| close(fd); |
| if(n < 0) |
| continue; |
| m->digest[n] = '\0'; |
| |
| /* |
| * We need the number of message lines so that we |
| * can adjust the byte count to include \r's. |
| * Upas/fs gives us the number of lines in the raw body |
| * in the lines file, but we have to count rawheader ourselves. |
| * There is one blank line between raw header and raw body. |
| */ |
| sprint(buf, "%d/rawheader", m->upasnum); |
| if((b = Bopen(buf, OREAD)) == nil) |
| continue; |
| lines = 0; |
| for(;;){ |
| p = Brdline(b, '\n'); |
| if(p == nil){ |
| if((n = Blinelen(b)) == 0) |
| break; |
| Bseek(b, n, 1); |
| }else |
| lines++; |
| } |
| Bterm(b); |
| lines++; |
| sprint(buf, "%d/lines", m->upasnum); |
| if((fd = open(buf, OREAD)) < 0) |
| continue; |
| n = readn(fd, buf, sizeof buf - 1); |
| close(fd); |
| if(n < 0) |
| continue; |
| buf[n] = '\0'; |
| lines += atoi(buf); |
| |
| sprint(buf, "%d/raw", m->upasnum); |
| if((draw = dirstat(buf)) == nil) |
| continue; |
| m->bytes = lines+draw->length; |
| free(draw); |
| nmsg++; |
| totalmsgs++; |
| totalbytes += m->bytes; |
| } |
| return 0; |
| } |
| |
| /* |
| * get a line that ends in crnl or cr, turn terminating crnl into a nl |
| * |
| * return 0 on EOF |
| */ |
| static int |
| getcrnl(char *buf, int n) |
| { |
| int c; |
| char *ep; |
| char *bp; |
| Biobuf *fp = ∈ |
| |
| Bflush(&out); |
| |
| bp = buf; |
| ep = bp + n - 1; |
| while(bp != ep){ |
| c = Bgetc(fp); |
| if(debug) { |
| seek(2, 0, 2); |
| fprint(2, "%c", c); |
| } |
| switch(c){ |
| case -1: |
| *bp = 0; |
| if(bp==buf) |
| return 0; |
| else |
| return bp-buf; |
| case '\r': |
| c = Bgetc(fp); |
| if(c == '\n'){ |
| if(debug) { |
| seek(2, 0, 2); |
| fprint(2, "%c", c); |
| } |
| *bp = 0; |
| return bp-buf; |
| } |
| Bungetc(fp); |
| c = '\r'; |
| break; |
| case '\n': |
| *bp = 0; |
| return bp-buf; |
| } |
| *bp++ = c; |
| } |
| *bp = 0; |
| return bp-buf; |
| } |
| |
| static void |
| sendcrnl(char *fmt, ...) |
| { |
| char buf[1024]; |
| va_list arg; |
| |
| va_start(arg, fmt); |
| vseprint(buf, buf+sizeof(buf), fmt, arg); |
| va_end(arg); |
| if(debug) |
| fprint(2, "-> %s\n", buf); |
| Bprint(&out, "%s\r\n", buf); |
| } |
| |
| static int |
| senderr(char *fmt, ...) |
| { |
| char buf[1024]; |
| va_list arg; |
| |
| va_start(arg, fmt); |
| vseprint(buf, buf+sizeof(buf), fmt, arg); |
| va_end(arg); |
| if(debug) |
| fprint(2, "-> -ERR %s\n", buf); |
| Bprint(&out, "-ERR %s\r\n", buf); |
| return -1; |
| } |
| |
| static int |
| sendok(char *fmt, ...) |
| { |
| char buf[1024]; |
| va_list arg; |
| |
| va_start(arg, fmt); |
| vseprint(buf, buf+sizeof(buf), fmt, arg); |
| va_end(arg); |
| if(*buf){ |
| if(debug) |
| fprint(2, "-> +OK %s\n", buf); |
| Bprint(&out, "+OK %s\r\n", buf); |
| } else { |
| if(debug) |
| fprint(2, "-> +OK\n"); |
| Bprint(&out, "+OK\r\n"); |
| } |
| return 0; |
| } |
| |
| static int |
| capacmd(char*) |
| { |
| sendok(""); |
| sendcrnl("TOP"); |
| if(passwordinclear || didtls) |
| sendcrnl("USER"); |
| sendcrnl("PIPELINING"); |
| sendcrnl("UIDL"); |
| sendcrnl("STLS"); |
| sendcrnl("."); |
| return 0; |
| } |
| |
| static int |
| delecmd(char *arg) |
| { |
| int n; |
| |
| if(*arg==0) |
| return senderr("DELE requires a message number"); |
| |
| n = atoi(arg)-1; |
| if(n < 0 || n >= nmsg || msg[n].deleted) |
| return senderr("no such message"); |
| |
| msg[n].deleted = 1; |
| totalmsgs--; |
| totalbytes -= msg[n].bytes; |
| sendok("message %d deleted", n+1); |
| return 0; |
| } |
| |
| static int |
| listcmd(char *arg) |
| { |
| int i, n; |
| |
| if(*arg == 0){ |
| sendok("+%d message%s (%d octets)", totalmsgs, totalmsgs==1 ? "":"s", totalbytes); |
| for(i=0; i<nmsg; i++){ |
| if(msg[i].deleted) |
| continue; |
| sendcrnl("%d %d", i+1, msg[i].bytes); |
| } |
| sendcrnl("."); |
| }else{ |
| n = atoi(arg)-1; |
| if(n < 0 || n >= nmsg || msg[n].deleted) |
| return senderr("no such message"); |
| sendok("%d %d", n+1, msg[n].bytes); |
| } |
| return 0; |
| } |
| |
| static int |
| noopcmd(char *arg) |
| { |
| USED(arg); |
| sendok(""); |
| return 0; |
| } |
| |
| static void |
| _synccmd(char*) |
| { |
| int i, fd; |
| char *s; |
| Fmt f; |
| |
| if(!loggedin){ |
| sendok(""); |
| return; |
| } |
| |
| fmtstrinit(&f); |
| fmtprint(&f, "delete mbox"); |
| for(i=0; i<nmsg; i++) |
| if(msg[i].deleted) |
| fmtprint(&f, " %d", msg[i].upasnum); |
| s = fmtstrflush(&f); |
| if(strcmp(s, "delete mbox") != 0){ /* must have something to delete */ |
| if((fd = open("../ctl", OWRITE)) < 0){ |
| senderr("open ctl to delete messages: %r"); |
| return; |
| } |
| if(write(fd, s, strlen(s)) < 0){ |
| senderr("error deleting messages: %r"); |
| return; |
| } |
| } |
| sendok(""); |
| } |
| |
| static int |
| synccmd(char*) |
| { |
| _synccmd(nil); |
| return 0; |
| } |
| |
| static int |
| quitcmd(char*) |
| { |
| synccmd(nil); |
| exits(nil); |
| return 0; |
| } |
| |
| static int |
| retrcmd(char *arg) |
| { |
| int n; |
| Biobuf *b; |
| char buf[40], *p; |
| |
| if(*arg == 0) |
| return senderr("RETR requires a message number"); |
| n = atoi(arg)-1; |
| if(n < 0 || n >= nmsg || msg[n].deleted) |
| return senderr("no such message"); |
| snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum); |
| if((b = Bopen(buf, OREAD)) == nil) |
| return senderr("message disappeared"); |
| sendok(""); |
| while((p = Brdstr(b, '\n', 1)) != nil){ |
| if(p[0]=='.') |
| Bwrite(&out, ".", 1); |
| Bwrite(&out, p, strlen(p)); |
| Bwrite(&out, "\r\n", 2); |
| free(p); |
| } |
| Bterm(b); |
| sendcrnl("."); |
| return 0; |
| } |
| |
| static int |
| rsetcmd(char*) |
| { |
| int i; |
| |
| for(i=0; i<nmsg; i++){ |
| if(msg[i].deleted){ |
| msg[i].deleted = 0; |
| totalmsgs++; |
| totalbytes += msg[i].bytes; |
| } |
| } |
| return sendok(""); |
| } |
| |
| static int |
| statcmd(char*) |
| { |
| return sendok("%d %d", totalmsgs, totalbytes); |
| } |
| |
| static int |
| trace(char *fmt, ...) |
| { |
| va_list arg; |
| int n; |
| |
| va_start(arg, fmt); |
| n = vfprint(2, fmt, arg); |
| va_end(arg); |
| return n; |
| } |
| |
| static int |
| stlscmd(char*) |
| { |
| int fd; |
| TLSconn conn; |
| |
| if(didtls) |
| return senderr("tls already started"); |
| if(!tlscert) |
| return senderr("don't have any tls credentials"); |
| sendok(""); |
| Bflush(&out); |
| |
| memset(&conn, 0, sizeof conn); |
| conn.cert = tlscert; |
| conn.certlen = ntlscert; |
| if(debug) |
| conn.trace = trace; |
| fd = tlsServer(0, &conn); |
| if(fd < 0) |
| sysfatal("tlsServer: %r"); |
| dup(fd, 0); |
| dup(fd, 1); |
| close(fd); |
| Binit(&in, 0, OREAD); |
| Binit(&out, 1, OWRITE); |
| didtls = 1; |
| return 0; |
| } |
| |
| static int |
| topcmd(char *arg) |
| { |
| int done, i, lines, n; |
| char buf[40], *p; |
| Biobuf *b; |
| |
| if(*arg == 0) |
| return senderr("TOP requires a message number"); |
| n = atoi(arg)-1; |
| if(n < 0 || n >= nmsg || msg[n].deleted) |
| return senderr("no such message"); |
| arg = nextarg(arg); |
| if(*arg == 0) |
| return senderr("TOP requires a line count"); |
| lines = atoi(arg); |
| if(lines < 0) |
| return senderr("bad args to TOP"); |
| snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum); |
| if((b = Bopen(buf, OREAD)) == nil) |
| return senderr("message disappeared"); |
| sendok(""); |
| while(p = Brdstr(b, '\n', 1)){ |
| if(p[0]=='.') |
| Bputc(&out, '.'); |
| Bwrite(&out, p, strlen(p)); |
| Bwrite(&out, "\r\n", 2); |
| done = p[0]=='\0'; |
| free(p); |
| if(done) |
| break; |
| } |
| for(i=0; i<lines; i++){ |
| p = Brdstr(b, '\n', 1); |
| if(p == nil) |
| break; |
| if(p[0]=='.') |
| Bwrite(&out, ".", 1); |
| Bwrite(&out, p, strlen(p)); |
| Bwrite(&out, "\r\n", 2); |
| free(p); |
| } |
| sendcrnl("."); |
| Bterm(b); |
| return 0; |
| } |
| |
| static int |
| uidlcmd(char *arg) |
| { |
| int n; |
| |
| if(*arg==0){ |
| sendok(""); |
| for(n=0; n<nmsg; n++){ |
| if(msg[n].deleted) |
| continue; |
| sendcrnl("%d %s", n+1, msg[n].digest); |
| } |
| sendcrnl("."); |
| }else{ |
| n = atoi(arg)-1; |
| if(n < 0 || n >= nmsg || msg[n].deleted) |
| return senderr("no such message"); |
| sendok("%d %s", n+1, msg[n].digest); |
| } |
| return 0; |
| } |
| |
| static char* |
| nextarg(char *p) |
| { |
| while(*p && *p != ' ' && *p != '\t') |
| p++; |
| while(*p == ' ' || *p == '\t') |
| *p++ = 0; |
| return p; |
| } |
| |
| /* |
| * authentication |
| */ |
| Chalstate *chs; |
| char user[256]; |
| char box[256]; |
| char cbox[256]; |
| |
| static void |
| hello(void) |
| { |
| fmtinstall('H', encodefmt); |
| if((chs = auth_challenge("proto=apop role=server")) == nil){ |
| senderr("auth server not responding, try later"); |
| exits(nil); |
| } |
| |
| sendok("POP3 server ready %s", chs->chal); |
| } |
| |
| static int |
| setuser(char *arg) |
| { |
| char *p; |
| |
| strcpy(box, "/mail/box/"); |
| strecpy(box+strlen(box), box+sizeof box-7, arg); |
| strcpy(cbox, box); |
| cleanname(cbox); |
| if(strcmp(cbox, box) != 0) |
| return senderr("bad mailbox name"); |
| strcat(box, "/mbox"); |
| |
| strecpy(user, user+sizeof user, arg); |
| if(p = strchr(user, '/')) |
| *p = '\0'; |
| return 0; |
| } |
| |
| static int |
| usercmd(char *arg) |
| { |
| if(loggedin) |
| return senderr("already authenticated"); |
| if(*arg == 0) |
| return senderr("USER requires argument"); |
| if(setuser(arg) < 0) |
| return -1; |
| return sendok(""); |
| } |
| |
| static void |
| enableaddr(void) |
| { |
| int fd; |
| char buf[64]; |
| |
| /* hide the peer IP address under a rock in the ratifier FS */ |
| if(peeraddr == 0 || *peeraddr == 0) |
| return; |
| |
| sprint(buf, "/mail/ratify/trusted/%s#32", peeraddr); |
| |
| /* |
| * if the address is already there and the user owns it, |
| * remove it and recreate it to give him a new time quanta. |
| */ |
| if(access(buf, 0) >= 0 && remove(buf) < 0) |
| return; |
| |
| fd = create(buf, OREAD, 0666); |
| if(fd >= 0){ |
| close(fd); |
| /* syslog(0, "pop3", "ratified %s", peeraddr); */ |
| } |
| } |
| |
| static int |
| dologin(char *response) |
| { |
| AuthInfo *ai; |
| static int tries; |
| |
| chs->user = user; |
| chs->resp = response; |
| chs->nresp = strlen(response); |
| if((ai = auth_response(chs)) == nil){ |
| if(tries++ >= 5){ |
| senderr("authentication failed: %r; server exiting"); |
| exits(nil); |
| } |
| return senderr("authentication failed"); |
| } |
| |
| if(auth_chuid(ai, nil) < 0){ |
| senderr("chuid failed: %r; server exiting"); |
| exits(nil); |
| } |
| auth_freeAI(ai); |
| auth_freechal(chs); |
| chs = nil; |
| |
| loggedin = 1; |
| if(newns(user, 0) < 0){ |
| senderr("newns failed: %r; server exiting"); |
| exits(nil); |
| } |
| |
| enableaddr(); |
| if(readmbox(box) < 0) |
| exits(nil); |
| return sendok("mailbox is %s", box); |
| } |
| |
| static int |
| passcmd(char *arg) |
| { |
| DigestState *s; |
| uchar digest[MD5dlen]; |
| char response[2*MD5dlen+1]; |
| |
| if(passwordinclear==0 && didtls==0) |
| return senderr("password in the clear disallowed"); |
| |
| /* use password to encode challenge */ |
| if((chs = auth_challenge("proto=apop role=server")) == nil) |
| return senderr("couldn't get apop challenge"); |
| |
| /* hash challenge with secret and convert to ascii */ |
| s = md5((uchar*)chs->chal, chs->nchal, 0, 0); |
| md5((uchar*)arg, strlen(arg), digest, s); |
| snprint(response, sizeof response, "%.*H", MD5dlen, digest); |
| return dologin(response); |
| } |
| |
| static int |
| apopcmd(char *arg) |
| { |
| char *resp; |
| |
| resp = nextarg(arg); |
| if(setuser(arg) < 0) |
| return -1; |
| return dologin(resp); |
| } |
| |