shithub: riscv

ref: 985b2457cda207c2c65edeb647aedbb6e92dac46
dir: /sys/src/cmd/upas/fs/imap4.c/

View raw version
#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <libsec.h>
#include <auth.h>
#include "dat.h"

#pragma varargck argpos imap4cmd 2
#pragma varargck	type	"Z"	char*

int	doublequote(Fmt*);

// if pipeline == 1 and upas/fs is used with dovecot,
// 9Xn OK responses sometimes come much later after FETCH responses, i.e.
// <- * 1 FETCH ...
// <- * 2 FETCH ...
// <- * 3 FETCH ...
// <- 9X5 OK Fetch completed.
// <- 9X6 OK Fetch completed.
// download 40: did not get message body
// <- 9X7 OK Fetch completed.
// causing multiple messages to turn into one in imap4.c:/^imap4resp.
int	pipeline = 0;

static char Eio[] = "i/o error";

typedef struct Imap Imap;
struct Imap {
	char *freep;	// free this to free the strings below

	char *host;
	char *user;
	char *mbox;

	int mustssl;
	int refreshtime;
	int debug;

	ulong tag;
	ulong validity;
	int nmsg;
	int size;
	char *base;
	char *data;

	vlong *uid;
	int nuid;
	int muid;

	Thumbprint *thumb;

	// open network connection
	Biobuf bin;
	Biobuf bout;
	int fd;
};

static char*
removecr(char *s)
{
	char *r, *w;

	for(r=w=s; *r; r++)
		if(*r != '\r')
			*w++ = *r;
	*w = '\0';
	return s;
}

//
// send imap4 command
//
static void
imap4cmd(Imap *imap, char *fmt, ...)
{
	char buf[128], *p;
	va_list va;

	va_start(va, fmt);
	p = buf+sprint(buf, "9X%lud ", imap->tag);
	vseprint(p, buf+sizeof(buf), fmt, va);
	va_end(va);

	p = buf+strlen(buf);
	if(p > (buf+sizeof(buf)-3))
		sysfatal("imap4 command too long");

	if(imap->debug)
		fprint(2, "-> %s\n", buf);
	strcpy(p, "\r\n");
	Bwrite(&imap->bout, buf, strlen(buf));
	Bflush(&imap->bout);
}

enum {
	OK,
	NO,
	BAD,
	BYE,
	EXISTS,
	STATUS,
	FETCH,
	UNKNOWN,
};

static char *verblist[] = {
[OK]		"OK",
[NO]		"NO",
[BAD]	"BAD",
[BYE]	"BYE",
[EXISTS]	"EXISTS",
[STATUS]	"STATUS",
[FETCH]	"FETCH",
};

static int
verbcode(char *verb)
{
	int i;
	char *q;

	if(q = strchr(verb, ' '))
		*q = '\0';

	for(i=0; i<nelem(verblist); i++)
		if(verblist[i] && strcmp(verblist[i], verb)==0){
			if(q)
				*q = ' ';
			return i;
		}
	if(q)
		*q = ' ';
	return UNKNOWN;
}

static void
strupr(char *s)
{
	for(; *s; s++)
		if('a' <= *s && *s <= 'z')
			*s += 'A'-'a';
}

static void
imapgrow(Imap *imap, int n)
{
	int i;

	if(imap->data == nil){
		imap->base = emalloc(n+1);	
		imap->data = imap->base;
		imap->size = n+1;
	}
	if(n >= imap->size){
		// friggin microsoft - reallocate
		i = imap->data - imap->base;
		imap->base = erealloc(imap->base, i+n+1);
		imap->data = imap->base + i;
		imap->size = n+1;
	}
}


//
// get imap4 response line.  there might be various 
// data or other informational lines mixed in.
//
static char*
imap4resp(Imap *imap)
{
	char *line, *p, *ep, *op, *q, *r, *en, *verb;
	int i, n;
	static char error[256];

	while(p = Brdline(&imap->bin, '\n')){
		ep = p+Blinelen(&imap->bin);
		while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r'))
			*--ep = '\0';
		
		if(imap->debug)
			fprint(2, "<- %s\n", p);
		strupr(p);

		switch(p[0]){
		case '+':
			if(imap->tag == 0)
				fprint(2, "unexpected: %s\n", p);
			break;

		// ``unsolicited'' information; everything happens here.
		case '*':
			if(p[1]!=' ')
				continue;
			p += 2;
			line = p;
			n = strtol(p, &p, 10);
			if(*p==' ')
				p++;
			verb = p;
			
			if(p = strchr(verb, ' '))
				p++;
			else
				p = verb+strlen(verb);

			switch(verbcode(verb)){
			case OK:
			case NO:
			case BAD:
				// human readable text at p;
				break;
			case BYE:
				// early disconnect
				// human readable text at p;
				break;

			// * 32 EXISTS
			case EXISTS:
				imap->nmsg = n;
				break;

			// * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964)
			case STATUS:
				if(q = strstr(p, "MESSAGES"))
					imap->nmsg = atoi(q+8);
				if(q = strstr(p, "UIDVALIDITY"))
					imap->validity = strtoul(q+11, 0, 10);
				break;

			case FETCH:
				// * 1 FETCH (uid 8889 RFC822.SIZE 3031 body[] {3031}
				// <3031 bytes of data>
 				// )
				if(strstr(p, "RFC822.SIZE") && strstr(p, "BODY[]")){
					if((q = strchr(p, '{')) 
					&& (n=strtol(q+1, &en, 0), *en=='}')){
						if(imap->data == nil || n >= imap->size)
							imapgrow(imap, n);
						if((i = Bread(&imap->bin, imap->data, n)) != n){
							snprint(error, sizeof error,
								"short read %d != %d: %r\n",
								i, n);
							return error;
						}
						if(imap->debug)
							fprint(2, "<- read %d bytes\n", n);
						imap->data[n] = '\0';
						if(imap->debug)
							fprint(2, "<- %s\n", imap->data);
						imap->data += n;
						imap->size -= n;
						p = Brdline(&imap->bin, '\n');
						if(imap->debug)
							fprint(2, "<- ignoring %.*s\n",
								Blinelen(&imap->bin), p);
					}else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
						*r = '\0';
						q++;
						n = r-q;
						if(imap->data == nil || n >= imap->size)
							imapgrow(imap, n);
						memmove(imap->data, q, n);
						imap->data[n] = '\0';
						imap->data += n;
						imap->size -= n;
					}else
						return "confused about FETCH response";
					break;
				}

				// * 1 FETCH (UID 1 RFC822.SIZE 511)
				if(q=strstr(p, "RFC822.SIZE")){
					imap->size = atoi(q+11);
					break;
				}

				// * 1 FETCH (UID 1 RFC822.HEADER {496}
				// <496 bytes of data>
 				// )
				// * 1 FETCH (UID 1 RFC822.HEADER "data")
				if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){
					if((q = strchr(p, '{')) 
					&& (n=strtol(q+1, &en, 0), *en=='}')){
						if(imap->data == nil || n >= imap->size)
							imapgrow(imap, n);
						if((i = Bread(&imap->bin, imap->data, n)) != n){
							snprint(error, sizeof error,
								"short read %d != %d: %r\n",
								i, n);
							return error;
						}
						if(imap->debug)
							fprint(2, "<- read %d bytes\n", n);
						imap->data[n] = '\0';
						if(imap->debug)
							fprint(2, "<- %s\n", imap->data);
						imap->data += n;
						imap->size -= n;
						p = Brdline(&imap->bin, '\n');
						if(imap->debug)
							fprint(2, "<- ignoring %.*s\n",
								Blinelen(&imap->bin), p);
					}else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
						*r = '\0';
						q++;
						n = r-q;
						if(imap->data == nil || n >= imap->size)
							imapgrow(imap, n);
						memmove(imap->data, q, n);
						imap->data[n] = '\0';
						imap->data += n;
						imap->size -= n;
					}else
						return "confused about FETCH response";
					break;
				}

				// * 1 FETCH (UID 1)
				// * 2 FETCH (UID 6)
				if(q = strstr(p, "UID")){
					if(imap->nuid < imap->muid)
						imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10);
					break;
				}
			}

			if(imap->tag == 0)
				return line;
			break;

		case '9':		// response to our message
			op = p;
			if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){
				while(*p==' ')
					p++;
				imap->tag++;
				return p;
			}
			fprint(2, "expected %lud; got %s\n", imap->tag, op);
			break;

		default:
			if(imap->debug || *p)
				fprint(2, "unexpected line: %s\n", p);
		}
	}
	snprint(error, sizeof error, "i/o error: %r\n");
	return error;
}

static int
isokay(char *resp)
{
	return strncmp(resp, "OK", 2)==0;
}

//
// log in to IMAP4 server, select mailbox, no SSL at the moment
//
static char*
imap4login(Imap *imap)
{
	char *s;
	UserPasswd *up;

	imap->tag = 0;
	s = imap4resp(imap);
	if(!isokay(s))
		return "error in initial IMAP handshake";

	if(imap->user != nil)
		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
	else
		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
	if(up == nil)
		return "cannot find IMAP password";

	imap->tag = 1;
	imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd);
	free(up);
	if(!isokay(s = imap4resp(imap)))
		return s;

	imap4cmd(imap, "SELECT %Z", imap->mbox);
	if(!isokay(s = imap4resp(imap)))
		return s;

	return nil;
}

static char*
imaperrstr(char *host, char *port)
{
	/*
	 * make mess big enough to hold a TLS certificate fingerprint
	 * plus quite a bit of slop.
	 */
	static char mess[3 * Errlen];
	char err[Errlen];

	err[0] = '\0';
	errstr(err, sizeof(err));
	snprint(mess, sizeof(mess), "%s/%s:%s", host, port, err);
	return mess;
}

static int
starttls(Imap *imap)
{
	int sfd;
	uchar digest[SHA1dlen];
	TLSconn conn;

	memset(&conn, 0, sizeof(conn));
	sfd = tlsClient(imap->fd, &conn);
	if(sfd < 0) {
		werrstr("tlsClient: %r");
		return -1;
	}
	imap->fd = sfd;
	free(conn.sessionID);
	if(conn.cert==nil || conn.certlen <= 0) {
		werrstr("server did not provide TLS certificate");
		return -1;
	}
	sha1(conn.cert, conn.certlen, digest, nil);
	free(conn.cert);
	if(!imap->thumb || !okThumbprint(digest, imap->thumb)){
		fmtinstall('H', encodefmt);
		werrstr("server certificate %.*H not recognized",
			SHA1dlen, digest);
		return -1;
	}
	return sfd;
}

//
// dial and handshake with the imap server
//
static char*
imap4dial(Imap *imap)
{
	char *err, *port;

	if(imap->fd >= 0){
		imap4cmd(imap, "noop");
		if(isokay(imap4resp(imap)))
			return nil;
		close(imap->fd);
		imap->fd = -1;
	}

	if(imap->mustssl)
		port = "imaps";
	else
		port = "imap";

	if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
		return imaperrstr(imap->host, port);

	if(imap->mustssl){
		if(starttls(imap) < 0){
			err = imaperrstr(imap->host, port);
			goto Out;
		}
	}
	Binit(&imap->bin, imap->fd, OREAD);
	Binit(&imap->bout, imap->fd, OWRITE);
	err = imap4login(imap);
Out:
	if(err != nil){
		if(imap->fd >= 0){
			close(imap->fd);
			imap->fd = -1;
		}
	}
	return err;
}

//
// close connection
//
static void
imap4hangup(Imap *imap)
{
	if(imap->fd < 0)
		return;
	imap4cmd(imap, "LOGOUT");
	imap4resp(imap);
	close(imap->fd);
	imap->fd = -1;
}

//
// download a single message
//
static char*
imap4fetch(Mailbox *mb, Message *m)
{
	int i;
	char *p, *s, sdigest[2*SHA1dlen+1];
	Imap *imap;

	imap = mb->aux;

	imap->size = 0;

	if(!isokay(s = imap4resp(imap)))
		return s;

	p = imap->base;
	if(p == nil)
		return "did not get message body";

	removecr(p);
	free(m->start);
	m->start = p;
	m->end = p+strlen(p);
	m->bend = m->rbend = m->end;
	m->header = m->start;

	imap->base = nil;
	imap->data = nil;

	parse(m, 0, mb, 1);

	// digest headers
	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 imap4 server
// download new messages, mark deleted messages
//
static char*
imap4read(Imap *imap, Mailbox *mb, int doplumb)
{
	char *s;
	int i, ignore, nnew, t;
	Message *m, *next, **l;

	imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox);
	if(!isokay(s = imap4resp(imap)))
		return s;

	imap->nuid = 0;
	imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0]));
	imap->muid = imap->nmsg;

	if(imap->nmsg > 0){
		imap4cmd(imap, "UID FETCH 1:* UID");
		if(!isokay(s = imap4resp(imap)))
			return s;
	}

	l = &mb->root->part;
	for(i=0; i<imap->nuid; i++){
		ignore = 0;
		while(*l != nil){
			if((*l)->imapuid == imap->uid[i]){
				ignore = 1;
				l = &(*l)->next;
				break;
			}else{
				// old mail, we don't have it anymore
				if(doplumb)
					mailplumb(mb, *l, 1);
				(*l)->inmbox = 0;
				(*l)->deleted = 1;
				l = &(*l)->next;
			}
		}
		if(ignore)
			continue;

		// new message
		m = newmessage(mb->root);
		m->mallocd = 1;
		m->inmbox = 1;
		m->imapuid = imap->uid[i];

		// add to chain, will download soon
		*l = m;
		l = &m->next;
	}

	// whatever is left at the end of the chain is gone
	while(*l != nil){
		if(doplumb)
			mailplumb(mb, *l, 1);
		(*l)->inmbox = 0;
		(*l)->deleted = 1;
		l = &(*l)->next;
	}

	// download new messages
	t = imap->tag;
	if(pipeline)
	switch(rfork(RFPROC|RFMEM)){
	case -1:
		sysfatal("rfork: %r");
	default:
		break;
	case 0:
		for(m = mb->root->part; m != nil; m = m->next){
			if(m->start != nil)
				continue;
			if(imap->debug)
				fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
					t, (ulong)m->imapuid);
			Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
				t++, (ulong)m->imapuid);
		}
		Bflush(&imap->bout);
		_exits(nil);
	}

	nnew = 0;
	for(m=mb->root->part; m!=nil; m=next){
		next = m->next;
		if(m->start != nil)
			continue;

		if(!pipeline){
			Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
				(ulong)imap->tag, (ulong)m->imapuid);
			Bflush(&imap->bout);
		}

		if(s = imap4fetch(mb, m)){
			// message disappeared?  unchain
			fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s);
			delmessage(mb, m);
			mb->root->subname--;
			continue;
		}
		nnew++;
		if(doplumb)
			mailplumb(mb, m, 0);
	}
	if(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;
}

//
// sync mailbox
//
static void
imap4purge(Imap *imap, Mailbox *mb)
{
	int ndel;
	Message *m, *next;

	ndel = 0;
	for(m=mb->root->part; m!=nil; m=next){
		next = m->next;
		if(m->deleted && m->refs==0){
			if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){
				imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid);
				if(isokay(imap4resp(imap))){
					ndel++;
					delmessage(mb, m);
				}
			}else
				delmessage(mb, m);
		}
	}

	if(ndel){
		imap4cmd(imap, "EXPUNGE");
		imap4resp(imap);
	}
}

//
// connect to imap4 server, sync mailbox
//
static char*
imap4sync(Mailbox *mb, int doplumb)
{
	char *err;
	Imap *imap;

	imap = mb->aux;

	if(err = imap4dial(imap)){
		mb->waketime = time(0) + imap->refreshtime;
		return err;
	}

	if((err = imap4read(imap, mb, doplumb)) == nil){
		imap4purge(imap, mb);
		mb->d->atime = mb->d->mtime = time(0);
	}
	/*
	 * don't hang up; leave connection open for next time.
	 */
	// imap4hangup(imap);
	mb->waketime = time(0) + imap->refreshtime;
	return err;
}

static char Eimap4ctl[] = "bad imap4 control message";

static char*
imap4ctl(Mailbox *mb, int argc, char **argv)
{
	int n;
	Imap *imap;

	imap = mb->aux;
	if(argc < 1)
		return Eimap4ctl;

	if(argc==1 && strcmp(argv[0], "debug")==0){
		imap->debug = 1;
		return nil;
	}

	if(argc==1 && strcmp(argv[0], "nodebug")==0){
		imap->debug = 0;
		return nil;
	}

	if(argc==1 && strcmp(argv[0], "thumbprint")==0){
		if(imap->thumb)
			freeThumbprints(imap->thumb);
		imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
	}
	if(strcmp(argv[0], "refresh")==0){
		if(argc==1){
			imap->refreshtime = 60;
			return nil;
		}
		if(argc==2){
			n = atoi(argv[1]);
			if(n < 15)
				return Eimap4ctl;
			imap->refreshtime = n;
			return nil;
		}
	}

	return Eimap4ctl;
}

//
// free extra memory associated with mb
//
static void
imap4close(Mailbox *mb)
{
	Imap *imap;

	imap = mb->aux;
	free(imap->freep);
	free(imap->base);
	free(imap->uid);
	if(imap->fd >= 0)
		close(imap->fd);
	free(imap);
}

//
// open mailboxes of the form /imap/host/user
//
char*
imap4mbox(Mailbox *mb, char *path)
{
	char *f[10];
	int mustssl, nf;
	Imap *imap;

	quotefmtinstall();
	fmtinstall('Z', doublequote);
	if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0)
		return Enotme;
	mustssl = (strncmp(path, "/imaps/", 7) == 0);

	path = strdup(path);
	if(path == nil)
		return "out of memory";

	nf = getfields(path, f, 5, 0, "/");
	if(nf < 3){
		free(path);
		return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
	}

	imap = emalloc(sizeof(*imap));
	imap->fd = -1;
	imap->debug = debug;
	imap->freep = path;
	imap->mustssl = mustssl;
	imap->host = f[2];
	if(nf < 4)
		imap->user = nil;
	else
		imap->user = f[3];
	if(nf < 5)
		imap->mbox = "Inbox";
	else
		imap->mbox = f[4];
	imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");

	mb->aux = imap;
	mb->sync = imap4sync;
	mb->close = imap4close;
	mb->ctl = imap4ctl;
	mb->d = emalloc(sizeof(*mb->d));
	//mb->fetch = imap4fetch;

	return nil;
}

//
// Formatter for %"
// Use double quotes to protect white space, frogs, \ and "
//
enum
{
	Qok = 0,
	Qquote,
	Qbackslash,
};

static int
needtoquote(Rune r)
{
	if(r >= Runeself)
		return Qquote;
	if(r <= ' ')
		return Qquote;
	if(r=='\\' || r=='"')
		return Qbackslash;
	return Qok;
}

int
doublequote(Fmt *f)
{
	char *s, *t;
	int w, quotes;
	Rune r;

	s = va_arg(f->args, char*);
	if(s == nil || *s == '\0')
		return fmtstrcpy(f, "\"\"");

	quotes = 0;
	for(t=s; *t; t+=w){
		w = chartorune(&r, t);
		quotes |= needtoquote(r);
	}
	if(quotes == 0)
		return fmtstrcpy(f, s);

	fmtrune(f, '"');
	for(t=s; *t; t+=w){
		w = chartorune(&r, t);
		if(needtoquote(r) == Qbackslash)
			fmtrune(f, '\\');
		fmtrune(f, r);
	}
	return fmtrune(f, '"');
}