summaryrefslogtreecommitdiffstats
path: root/src/cmd/upas/fs/pop3.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/upas/fs/pop3.c')
-rw-r--r--src/cmd/upas/fs/pop3.c700
1 files changed, 700 insertions, 0 deletions
diff --git a/src/cmd/upas/fs/pop3.c b/src/cmd/upas/fs/pop3.c
new file mode 100644
index 00000000..4ea9adb3
--- /dev/null
+++ b/src/cmd/upas/fs/pop3.c
@@ -0,0 +1,700 @@
+#include "common.h"
+#include <ctype.h>
+#include <plumb.h>
+#include <libsec.h>
+#include <auth.h>
+#include <thread.h>
+#include "dat.h"
+
+#pragma varargck type "M" uchar*
+#pragma varargck argpos pop3cmd 2
+
+typedef struct Pop Pop;
+struct Pop {
+ char *freep; // free this to free the strings below
+
+ char *host;
+ char *user;
+ char *port;
+
+ int ppop;
+ int refreshtime;
+ int debug;
+ int pipeline;
+ int encrypted;
+ int needtls;
+ int notls;
+ int needssl;
+
+ // open network connection
+ Biobuf bin;
+ Biobuf bout;
+ int fd;
+ char *lastline; // from Brdstr
+
+ Thumbprint *thumb;
+};
+
+char*
+geterrstr(void)
+{
+ static char err[64];
+
+ err[0] = '\0';
+ errstr(err, sizeof(err));
+ return err;
+}
+
+//
+// get pop3 response line , without worrying
+// about multiline responses; the clients
+// will deal with that.
+//
+static int
+isokay(char *s)
+{
+ return s!=nil && strncmp(s, "+OK", 3)==0;
+}
+
+static void
+pop3cmd(Pop *pop, char *fmt, ...)
+{
+ char buf[128], *p;
+ va_list va;
+
+ va_start(va, fmt);
+ vseprint(buf, buf+sizeof(buf), fmt, va);
+ va_end(va);
+
+ p = buf+strlen(buf);
+ if(p > (buf+sizeof(buf)-3))
+ sysfatal("pop3 command too long");
+
+ if(pop->debug)
+ fprint(2, "<- %s\n", buf);
+ strcpy(p, "\r\n");
+ Bwrite(&pop->bout, buf, strlen(buf));
+ Bflush(&pop->bout);
+}
+
+static char*
+pop3resp(Pop *pop)
+{
+ char *s;
+ char *p;
+
+ alarm(60*1000);
+ if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
+ close(pop->fd);
+ pop->fd = -1;
+ alarm(0);
+ return "unexpected eof";
+ }
+ alarm(0);
+
+ p = s+strlen(s)-1;
+ while(p >= s && (*p == '\r' || *p == '\n'))
+ *p-- = '\0';
+
+ if(pop->debug)
+ fprint(2, "-> %s\n", s);
+ free(pop->lastline);
+ pop->lastline = s;
+ return s;
+}
+
+#if 0 /* jpc */
+static int
+pop3log(char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap,fmt);
+ syslog(0, "/sys/log/pop3", fmt, ap);
+ va_end(ap);
+ return 0;
+}
+#endif
+
+static char*
+pop3pushtls(Pop *pop)
+{
+ int fd;
+ uchar digest[SHA1dlen];
+ TLSconn conn;
+
+ memset(&conn, 0, sizeof conn);
+ // conn.trace = pop3log;
+ fd = tlsClient(pop->fd, &conn);
+ if(fd < 0)
+ return "tls error";
+ if(conn.cert==nil || conn.certlen <= 0){
+ close(fd);
+ return "server did not provide TLS certificate";
+ }
+ sha1(conn.cert, conn.certlen, digest, nil);
+ if(!pop->thumb || !okThumbprint(digest, pop->thumb)){
+ fmtinstall('H', encodefmt);
+ close(fd);
+ free(conn.cert);
+ fprint(2, "upas/fs pop3: server certificate %.*H not recognized\n", SHA1dlen, digest);
+ return "bad server certificate";
+ }
+ free(conn.cert);
+ close(pop->fd);
+ pop->fd = fd;
+ pop->encrypted = 1;
+ Binit(&pop->bin, pop->fd, OREAD);
+ Binit(&pop->bout, pop->fd, OWRITE);
+ return nil;
+}
+
+//
+// get capability list, possibly start tls
+//
+static char*
+pop3capa(Pop *pop)
+{
+ char *s;
+ int hastls;
+
+ pop3cmd(pop, "CAPA");
+ if(!isokay(pop3resp(pop)))
+ return nil;
+
+ hastls = 0;
+ for(;;){
+ s = pop3resp(pop);
+ if(strcmp(s, ".") == 0 || strcmp(s, "unexpected eof") == 0)
+ break;
+ if(strcmp(s, "STLS") == 0)
+ hastls = 1;
+ if(strcmp(s, "PIPELINING") == 0)
+ pop->pipeline = 1;
+ }
+
+ if(hastls && !pop->notls){
+ pop3cmd(pop, "STLS");
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+ if((s = pop3pushtls(pop)) != nil)
+ return s;
+ }
+ return nil;
+}
+
+//
+// log in using APOP if possible, password if allowed by user
+//
+static char*
+pop3login(Pop *pop)
+{
+ int n;
+ char *s, *p, *q;
+ char ubuf[128], user[128];
+ char buf[500];
+ UserPasswd *up;
+
+ s = pop3resp(pop);
+ if(!isokay(s))
+ return "error in initial handshake";
+
+ if(pop->user)
+ snprint(ubuf, sizeof ubuf, " user=%q", pop->user);
+ else
+ ubuf[0] = '\0';
+
+ // look for apop banner
+ if(pop->ppop==0 && (p = strchr(s, '<')) && (q = strchr(p+1, '>'))) {
+ *++q = '\0';
+ if((n=auth_respond(p, q-p, user, sizeof user, buf, sizeof buf, auth_getkey, "proto=apop role=client server=%q%s",
+ pop->host, ubuf)) < 0)
+ return "factotum failed";
+ if(user[0]=='\0')
+ return "factotum did not return a user name";
+
+ if(s = pop3capa(pop))
+ return s;
+
+ pop3cmd(pop, "APOP %s %.*s", user, n, buf);
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+
+ return nil;
+ } else {
+ if(pop->ppop == 0)
+ return "no APOP hdr from server";
+
+ if(s = pop3capa(pop))
+ return s;
+
+ if(pop->needtls && !pop->encrypted)
+ return "could not negotiate TLS";
+
+ up = auth_getuserpasswd(auth_getkey, "role=client proto=pass service=pop dom=%q%s",
+ pop->host, ubuf);
+ /* up = auth_getuserpasswd(auth_getkey, "proto=pass service=pop dom=%q%s",
+ pop->host, ubuf); jpc */
+ if(up == nil)
+ return "no usable keys found";
+
+ pop3cmd(pop, "USER %s", up->user);
+ if(!isokay(s = pop3resp(pop))){
+ free(up);
+ return s;
+ }
+ pop3cmd(pop, "PASS %s", up->passwd);
+ free(up);
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+
+ return nil;
+ }
+}
+
+//
+// dial and handshake with pop server
+//
+static char*
+pop3dial(Pop *pop)
+{
+ char *err;
+
+ if((pop->fd = dial(netmkaddr(pop->host, "net", pop->needssl ? "pop3s" : "pop3"), 0, 0, 0)) < 0)
+ return geterrstr();
+
+ if(pop->needssl){
+ if((err = pop3pushtls(pop)) != nil)
+ return err;
+ }else{
+ Binit(&pop->bin, pop->fd, OREAD);
+ Binit(&pop->bout, pop->fd, OWRITE);
+ }
+
+ if(err = pop3login(pop)) {
+ close(pop->fd);
+ return err;
+ }
+
+ return nil;
+}
+
+//
+// close connection
+//
+static void
+pop3hangup(Pop *pop)
+{
+ pop3cmd(pop, "QUIT");
+ pop3resp(pop);
+ close(pop->fd);
+}
+
+//
+// download a single message
+//
+static char*
+pop3download(Pop *pop, Message *m)
+{
+ char *s, *f[3], *wp, *ep;
+ char sdigest[SHA1dlen*2+1];
+ int i, l, sz;
+
+ if(!pop->pipeline)
+ pop3cmd(pop, "LIST %d", m->mesgno);
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+
+ if(tokenize(s, f, 3) != 3)
+ return "syntax error in LIST response";
+
+ if(atoi(f[1]) != m->mesgno)
+ return "out of sync with pop3 server";
+
+ sz = atoi(f[2])+200; /* 200 because the plan9 pop3 server lies */
+ if(sz == 0)
+ return "invalid size in LIST response";
+
+ m->start = wp = emalloc(sz+1);
+ ep = wp+sz;
+
+ if(!pop->pipeline)
+ pop3cmd(pop, "RETR %d", m->mesgno);
+ if(!isokay(s = pop3resp(pop))) {
+ m->start = nil;
+ free(wp);
+ return s;
+ }
+
+ s = nil;
+ while(wp <= ep) {
+ s = pop3resp(pop);
+ if(strcmp(s, "unexpected eof") == 0) {
+ free(m->start);
+ m->start = nil;
+ return "unexpected end of conversation";
+ }
+ if(strcmp(s, ".") == 0)
+ break;
+
+ l = strlen(s)+1;
+ if(s[0] == '.') {
+ s++;
+ l--;
+ }
+ /*
+ * grow by 10%/200bytes - some servers
+ * lie about message sizes
+ */
+ if(wp+l > ep) {
+ int pos = wp - m->start;
+ sz += ((sz / 10) < 200)? 200: sz/10;
+ m->start = erealloc(m->start, sz+1);
+ wp = m->start+pos;
+ ep = m->start+sz;
+ }
+ memmove(wp, s, l-1);
+ wp[l-1] = '\n';
+ wp += l;
+ }
+
+ if(s == nil || strcmp(s, ".") != 0)
+ return "out of sync with pop3 server";
+
+ m->end = wp;
+
+ // make sure there's a trailing null
+ // (helps in body searches)
+ *m->end = 0;
+ m->bend = m->rbend = m->end;
+ m->header = m->start;
+
+ // digest message
+ sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+ for(i = 0; i < SHA1dlen; i++)
+ sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
+ m->sdigest = s_copy(sdigest);
+
+ return nil;
+}
+
+//
+// check for new messages on pop server
+// UIDL is not required by RFC 1939, but
+// netscape requires it, so almost every server supports it.
+// we'll use it to make our lives easier.
+//
+static char*
+pop3read(Pop *pop, Mailbox *mb, int doplumb)
+{
+ char *s, *p, *uidl, *f[2];
+ int mesgno, ignore, nnew;
+ Message *m, *next, **l;
+
+ // Some POP servers disallow UIDL if the maildrop is empty.
+ pop3cmd(pop, "STAT");
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+
+ // fetch message listing; note messages to grab
+ l = &mb->root->part;
+ if(strncmp(s, "+OK 0 ", 6) != 0) {
+ pop3cmd(pop, "UIDL");
+ if(!isokay(s = pop3resp(pop)))
+ return s;
+
+ for(;;){
+ p = pop3resp(pop);
+ if(strcmp(p, ".") == 0 || strcmp(p, "unexpected eof") == 0)
+ break;
+
+ if(tokenize(p, f, 2) != 2)
+ continue;
+
+ mesgno = atoi(f[0]);
+ uidl = f[1];
+ if(strlen(uidl) > 75) // RFC 1939 says 70 characters max
+ continue;
+
+ ignore = 0;
+ while(*l != nil) {
+ if(strcmp((*l)->uidl, uidl) == 0) {
+ // matches mail we already have, note mesgno for deletion
+ (*l)->mesgno = mesgno;
+ ignore = 1;
+ l = &(*l)->next;
+ break;
+ } else {
+ // old mail no longer in box mark deleted
+ if(doplumb)
+ mailplumb(mb, *l, 1);
+ (*l)->inmbox = 0;
+ (*l)->deleted = 1;
+ l = &(*l)->next;
+ }
+ }
+ if(ignore)
+ continue;
+
+ m = newmessage(mb->root);
+ m->mallocd = 1;
+ m->inmbox = 1;
+ m->mesgno = mesgno;
+ strcpy(m->uidl, uidl);
+
+ // chain in; will fill in message later
+ *l = m;
+ l = &m->next;
+ }
+ }
+
+ // whatever is left has been removed from the mbox, mark as deleted
+ while(*l != nil) {
+ if(doplumb)
+ mailplumb(mb, *l, 1);
+ (*l)->inmbox = 0;
+ (*l)->deleted = 1;
+ l = &(*l)->next;
+ }
+
+ // download new messages
+ nnew = 0;
+ if(pop->pipeline){
+ switch(rfork(RFPROC|RFMEM)){
+ case -1:
+ fprint(2, "rfork: %r\n");
+ pop->pipeline = 0;
+
+ default:
+ break;
+
+ case 0:
+ for(m = mb->root->part; m != nil; m = m->next){
+ if(m->start != nil)
+ continue;
+ Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", m->mesgno, m->mesgno);
+ }
+ Bflush(&pop->bout);
+ threadexits(nil);
+ /* _exits(nil); jpc */
+ }
+ }
+
+ for(m = mb->root->part; m != nil; m = next) {
+ next = m->next;
+
+ if(m->start != nil)
+ continue;
+
+ if(s = pop3download(pop, m)) {
+ // message disappeared? unchain
+ fprint(2, "download %d: %s\n", m->mesgno, s);
+ delmessage(mb, m);
+ mb->root->subname--;
+ continue;
+ }
+ nnew++;
+ parse(m, 0, mb, 1);
+
+ if(doplumb)
+ mailplumb(mb, m, 0);
+ }
+ if(pop->pipeline)
+ waitpid();
+
+ if(nnew || mb->vers == 0) {
+ mb->vers++;
+ henter(PATH(0, Qtop), mb->name,
+ (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+ }
+
+ return nil;
+}
+
+//
+// delete marked messages
+//
+static void
+pop3purge(Pop *pop, Mailbox *mb)
+{
+ Message *m, *next;
+
+ if(pop->pipeline){
+ switch(rfork(RFPROC|RFMEM)){
+ case -1:
+ fprint(2, "rfork: %r\n");
+ pop->pipeline = 0;
+
+ default:
+ break;
+
+ case 0:
+ for(m = mb->root->part; m != nil; m = next){
+ next = m->next;
+ if(m->deleted && m->refs == 0){
+ if(m->inmbox)
+ Bprint(&pop->bout, "DELE %d\r\n", m->mesgno);
+ }
+ }
+ Bflush(&pop->bout);
+ /* _exits(nil); jpc */
+ threadexits(nil);
+ }
+ }
+ for(m = mb->root->part; m != nil; m = next) {
+ next = m->next;
+ if(m->deleted && m->refs == 0) {
+ if(m->inmbox) {
+ if(!pop->pipeline)
+ pop3cmd(pop, "DELE %d", m->mesgno);
+ if(isokay(pop3resp(pop)))
+ delmessage(mb, m);
+ } else
+ delmessage(mb, m);
+ }
+ }
+}
+
+
+// connect to pop3 server, sync mailbox
+static char*
+pop3sync(Mailbox *mb, int doplumb)
+{
+ char *err;
+ Pop *pop;
+
+ pop = mb->aux;
+
+ if(err = pop3dial(pop)) {
+ mb->waketime = time(0) + pop->refreshtime;
+ return err;
+ }
+
+ if((err = pop3read(pop, mb, doplumb)) == nil){
+ pop3purge(pop, mb);
+ mb->d->atime = mb->d->mtime = time(0);
+ }
+ pop3hangup(pop);
+ mb->waketime = time(0) + pop->refreshtime;
+ return err;
+}
+
+static char Epop3ctl[] = "bad pop3 control message";
+
+static char*
+pop3ctl(Mailbox *mb, int argc, char **argv)
+{
+ int n;
+ Pop *pop;
+ char *m, *me;
+
+ pop = mb->aux;
+ if(argc < 1)
+ return Epop3ctl;
+
+ if(argc==1 && strcmp(argv[0], "debug")==0){
+ pop->debug = 1;
+ return nil;
+ }
+
+ if(argc==1 && strcmp(argv[0], "nodebug")==0){
+ pop->debug = 0;
+ return nil;
+ }
+
+ if(argc==1 && strcmp(argv[0], "thumbprint")==0){
+ if(pop->thumb)
+ freeThumbprints(pop->thumb);
+ /* pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); jpc */
+ m = unsharp("#9/sys/lib/tls/mail");
+ me = unsharp("#9/sys/lib/tls/mail.exclude");
+ pop->thumb = initThumbprints(m, me);
+ }
+ if(strcmp(argv[0], "refresh")==0){
+ if(argc==1){
+ pop->refreshtime = 60;
+ return nil;
+ }
+ if(argc==2){
+ n = atoi(argv[1]);
+ if(n < 15)
+ return Epop3ctl;
+ pop->refreshtime = n;
+ return nil;
+ }
+ }
+
+ return Epop3ctl;
+}
+
+// free extra memory associated with mb
+static void
+pop3close(Mailbox *mb)
+{
+ Pop *pop;
+
+ pop = mb->aux;
+ free(pop->freep);
+ free(pop);
+}
+
+//
+// open mailboxes of the form /pop/host/user or /apop/host/user
+//
+char*
+pop3mbox(Mailbox *mb, char *path)
+{
+ char *f[10];
+ int nf, apop, ppop, popssl, apopssl, apoptls, popnotls, apopnotls, poptls;
+ Pop *pop;
+ char *m, *me;
+
+ quotefmtinstall();
+ popssl = strncmp(path, "/pops/", 6) == 0;
+ apopssl = strncmp(path, "/apops/", 7) == 0;
+ poptls = strncmp(path, "/poptls/", 8) == 0;
+ popnotls = strncmp(path, "/popnotls/", 10) == 0;
+ ppop = popssl || poptls || popnotls || strncmp(path, "/pop/", 5) == 0;
+ apoptls = strncmp(path, "/apoptls/", 9) == 0;
+ apopnotls = strncmp(path, "/apopnotls/", 11) == 0;
+ apop = apopssl || apoptls || apopnotls || strncmp(path, "/apop/", 6) == 0;
+
+ if(!ppop && !apop)
+ return Enotme;
+
+ path = strdup(path);
+ if(path == nil)
+ return "out of memory";
+
+ nf = getfields(path, f, nelem(f), 0, "/");
+ if(nf != 3 && nf != 4) {
+ free(path);
+ return "bad pop3 path syntax /[a]pop[tls|ssl]/system[/user]";
+ }
+
+ pop = emalloc(sizeof(*pop));
+ pop->freep = path;
+ pop->host = f[2];
+ if(nf < 4)
+ pop->user = nil;
+ else
+ pop->user = f[3];
+ pop->ppop = ppop;
+ pop->needssl = popssl || apopssl;
+ pop->needtls = poptls || apoptls;
+ pop->refreshtime = 60;
+ pop->notls = popnotls || apopnotls;
+ /* pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); jpc */
+ m = unsharp("#9/sys/lib/tls/mail");
+ me = unsharp("#9/sys/lib/tls/mail.exclude");
+ pop->thumb = initThumbprints(m, me);
+
+ mb->aux = pop;
+ mb->sync = pop3sync;
+ mb->close = pop3close;
+ mb->ctl = pop3ctl;
+ mb->d = emalloc(sizeof(*mb->d));
+
+ return nil;
+}
+