708 lines
13 KiB
C
708 lines
13 KiB
C
#include "common.h"
|
|
#include <libsec.h>
|
|
#include <auth.h>
|
|
#include "dat.h"
|
|
|
|
#pragma varargck type "M" uchar*
|
|
#pragma varargck argpos pop3cmd 2
|
|
#define pdprint(p, ...) if((p)->debug) fprint(2, __VA_ARGS__); else{}
|
|
|
|
typedef struct Popm Popm;
|
|
struct Popm{
|
|
int mesgno;
|
|
};
|
|
|
|
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;
|
|
|
|
Biobuf bin; /* open network connection */
|
|
Biobuf bout;
|
|
int fd;
|
|
char *lastline; /* from Brdstr */
|
|
Thumbprint *thumb;
|
|
};
|
|
|
|
static int
|
|
mesgno(Message *m)
|
|
{
|
|
Popm *a;
|
|
|
|
a = m->aux;
|
|
return a->mesgno;
|
|
}
|
|
|
|
static 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");
|
|
pdprint(pop, "<- %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';
|
|
|
|
pdprint(pop, "-> %s\n", s);
|
|
free(pop->lastline);
|
|
pop->lastline = 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)){
|
|
close(fd);
|
|
free(conn.cert);
|
|
eprint("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(strcmp(s, "EXPIRE 0") == 0)
|
|
return "server does not allow mail to be left on server";
|
|
}
|
|
|
|
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(Mailbox *mb, Pop *pop, Message *m)
|
|
{
|
|
char *s, *f[3], *wp, *ep;
|
|
int l, sz, pos, n;
|
|
Popm *a;
|
|
|
|
a = m->aux;
|
|
if(!pop->pipeline)
|
|
pop3cmd(pop, "LIST %d", a->mesgno);
|
|
if(!isokay(s = pop3resp(pop)))
|
|
return s;
|
|
|
|
if(tokenize(s, f, 3) != 3)
|
|
return "syntax error in LIST response";
|
|
|
|
if(atoi(f[1]) != a->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", a->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) {
|
|
pos = wp - m->start;
|
|
n = sz/10;
|
|
if(n < 200)
|
|
n = 200;
|
|
sz += n;
|
|
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;
|
|
m->size = m->end - m->start;
|
|
if(m->digest == nil)
|
|
digestmessage(mb, m);
|
|
|
|
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, int *new)
|
|
{
|
|
char *s, *p, *uidl, *f[2];
|
|
int mno, ignore, nnew;
|
|
Message *m, *next, **l;
|
|
Popm *a;
|
|
|
|
*new = 0;
|
|
/* 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;
|
|
|
|
mno = atoi(f[0]);
|
|
uidl = f[1];
|
|
if(strlen(uidl) > 75) /* RFC 1939 says 70 characters max */
|
|
continue;
|
|
|
|
ignore = 0;
|
|
while(*l != nil) {
|
|
a = (*l)->aux;
|
|
if(strcmp((*l)->idxaux, uidl) == 0){
|
|
if(a == 0){
|
|
m = *l;
|
|
m->mallocd = 1;
|
|
m->inmbox = 1;
|
|
m->aux = a = emalloc(sizeof *a);
|
|
}
|
|
/* matches mail we already have, note mesgno for deletion */
|
|
a->mesgno = mno;
|
|
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 = Deleted;
|
|
l = &(*l)->next;
|
|
}
|
|
}
|
|
if(ignore)
|
|
continue;
|
|
|
|
m = newmessage(mb->root);
|
|
m->mallocd = 1;
|
|
m->inmbox = 1;
|
|
m->idxaux = strdup(uidl);
|
|
m->aux = a = emalloc(sizeof *a);
|
|
a->mesgno = mno;
|
|
|
|
/* 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 = Disappear;
|
|
l = &(*l)->next;
|
|
}
|
|
|
|
/* download new messages */
|
|
nnew = 0;
|
|
if(pop->pipeline){
|
|
switch(rfork(RFPROC|RFMEM)){
|
|
case -1:
|
|
eprint("pop3: rfork: %r\n");
|
|
pop->pipeline = 0;
|
|
|
|
default:
|
|
break;
|
|
|
|
case 0:
|
|
for(m = mb->root->part; m != nil; m = m->next){
|
|
if(m->start != nil || m->deleted)
|
|
continue;
|
|
Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", mesgno(m), mesgno(m));
|
|
}
|
|
Bflush(&pop->bout);
|
|
_exits("");
|
|
}
|
|
}
|
|
|
|
for(m = mb->root->part; m != nil; m = next) {
|
|
next = m->next;
|
|
|
|
if(m->start != nil || m->deleted)
|
|
continue;
|
|
if(s = pop3download(mb, pop, m)) {
|
|
/* message disappeared? unchain */
|
|
eprint("pop3: download %d: %s\n", mesgno(m), s);
|
|
delmessage(mb, m);
|
|
mb->root->subname--;
|
|
continue;
|
|
}
|
|
nnew++;
|
|
parse(mb, m, 1, 0);
|
|
newcachehash(mb, m, doplumb);
|
|
putcache(mb, m);
|
|
}
|
|
if(pop->pipeline)
|
|
waitpid();
|
|
*new = nnew;
|
|
return nil;
|
|
}
|
|
|
|
/*
|
|
* delete marked messages
|
|
*/
|
|
static void
|
|
pop3purge(Pop *pop, Mailbox *mb)
|
|
{
|
|
Message *m, *next;
|
|
|
|
if(pop->pipeline){
|
|
switch(rfork(RFPROC|RFMEM)){
|
|
case -1:
|
|
eprint("pop3: 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", mesgno(m));
|
|
}
|
|
}
|
|
Bflush(&pop->bout);
|
|
_exits("");
|
|
}
|
|
}
|
|
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", mesgno(m));
|
|
if(isokay(pop3resp(pop)))
|
|
delmessage(mb, m);
|
|
} else
|
|
delmessage(mb, m);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* connect to pop3 server, sync mailbox */
|
|
static char*
|
|
pop3sync(Mailbox *mb, int doplumb, int *new)
|
|
{
|
|
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, new)) == 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);
|
|
}
|
|
|
|
static char*
|
|
mkmbox(Pop *pop, char *p, char *e)
|
|
{
|
|
p = seprint(p, e, "%s/box/%s/pop.%s", MAILROOT, getlog(), pop->host);
|
|
if(pop->user && strcmp(pop->user, getlog()))
|
|
p = seprint(p, e, ".%s", pop->user);
|
|
return p;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
|
|
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");
|
|
|
|
mkmbox(pop, mb->path, mb->path + sizeof mb->path);
|
|
mb->aux = pop;
|
|
mb->sync = pop3sync;
|
|
mb->close = pop3close;
|
|
mb->ctl = pop3ctl;
|
|
mb->d = emalloc(sizeof *mb->d);
|
|
mb->addfrom = 1;
|
|
return nil;
|
|
}
|
|
|