#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <libsec.h>
#include <auth.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;
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;
static char *l = nil;
alarm(60*1000);
if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
close(pop->fd);
pop->fd = -1;
alarm(0);
return "unexpected eof";
}
alarm(0);
if(l){
free(l);
l = s;
}
p = s+Blinelen(&pop->bin)-1;
while(p >= s && (*p == '\r' || *p == '\n'))
*p-- = '\0';
if(pop->debug)
fprint(2, "-> %s\n", s);
return s;
}
static int
pop3log(char *fmt, ...)
{
va_list ap;
va_start(ap,fmt);
syslog(0, "/sys/log/pop3", fmt, ap);
va_end(ap);
return 0;
}
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, "proto=pass service=pop dom=%q%s",
pop->host, ubuf);
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);
_exits(nil);
}
}
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);
}
}
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;
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");
}
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;
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");
mb->aux = pop;
mb->sync = pop3sync;
mb->close = pop3close;
mb->ctl = pop3ctl;
mb->d = emalloc(sizeof(*mb->d));
return nil;
}
|