Plan 9 from Bell Labs’s /usr/web/sources/contrib/quanstro/root/sys/src/cmd/upas/fs/pop3.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#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;
}


Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to [email protected].