summary refs log blame commit diff
path: root/imap.c
blob: e875595da4227d326a0495fe1ee90a11cf216a4d (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14












                                                                         


                                                                 
                                                                     


                                                                       
                                                                   
                

                
                       
                 


                    
                       
                     
                   
 






                                                  
                 



                                                           
































                                                                     
                      

                     
                                                          








                                                                             

























                                                                                  
 
                            
                             


                                                                       
 
                           
 
                                                               
                                                         


                                                                                   
 
                                                                               
                             
                                              
          
                         

                    
                                                   
                               
                                                              

          




                                                                     
                               
                                                  
                                                   
                             

                    




                                                                      



                                                  
                                                       




                                                       
                                                
 
                                                             
                                            
                                     
                                                      
         
                                    

                    






                                                                             

                                            
                                         





                                                                      
                                       
                                                   
                            














                                                                           
                                                   

                                                    
                                                   





                                             



















                                                                           
/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Additional permission under GNU GPL version 3 section 7:
 *
 * If you modify this Program, or any covered work, by linking or
 * combining it with OpenSSL (or a modified version of that library),
 * containing parts covered by the terms of the OpenSSL License and the
 * original SSLeay license, the licensors of this Program grant you
 * additional permission to convey the resulting work. Corresponding
 * Source for a non-source form of such a combination shall include the
 * source code for the parts of OpenSSL used as well as that of the
 * covered work.
 */

#include <err.h>
#include <netdb.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sysexits.h>
#include <tls.h>
#include <unistd.h>

FILE *funopen(
	const void *cookie,
	int (*readfn)(void *, char *, int),
	int (*writefn)(void *, const char *, int),
	fpos_t (*seekfn)(void *, fpos_t, int),
	int (*closefn)(void *)
);

#include "imap.h"

int getservinfo(
	const char *hostname, const char *servname,
	const struct addrinfo *hints, struct addrinfo **res
);

const char *Atoms[AtomCap] = {
#define X(id, str) [id] = str,
	ENUM_ATOM
#undef X
};

bool imapVerbose;

static int imapRead(void *_tls, char *ptr, int len) {
	struct tls *tls = _tls;
	ssize_t ret;
	do {
		ret = tls_read(tls, ptr, len);
	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
	if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(tls));
	if (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr);
	return ret;
}

static int imapWrite(void *_tls, const char *ptr, int len) {
	struct tls *tls = _tls;
	ssize_t ret;
	do {
		ret = tls_write(tls, ptr, len);
	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
	if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(tls));
	if (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr);
	return ret;
}

static int imapClose(void *_tls) {
	struct tls *tls = _tls;
	int error = tls_close(tls);
	if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls));
	tls_free(tls);
	return error;
}

struct IMAP imapOpen(const char *host, const char *port) {
	struct tls *client = tls_client();
	if (!client) errx(EX_SOFTWARE, "tls_client");

	struct tls_config *config = tls_config_new();
	if (!config) errx(EX_SOFTWARE, "tls_config_new");

	int error = tls_configure(client, config);
	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
	tls_config_free(config);

	struct addrinfo *head;
	struct addrinfo hints = {
		.ai_family = PF_UNSPEC,
		.ai_socktype = SOCK_STREAM,
		.ai_protocol = IPPROTO_TCP,
	};
	error = getservinfo(host, port, &hints, &head);
	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error));

	int sock = -1;
	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
		sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
		if (sock < 0) err(EX_OSERR, "socket");

		error = connect(sock, ai->ai_addr, ai->ai_addrlen);
		if (!error) break;

		close(sock);
		sock = -1;
	}
	if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port);

	error = tls_connect_socket(
		client, sock, (head->ai_canonname ? head->ai_canonname : host)
	);
	if (error) errx(EX_SOFTWARE, "tls_connect_socket: %s", tls_error(client));
	freeaddrinfo(head);

	struct IMAP imap = {
		.sock = sock,
		.r = funopen(client, imapRead, NULL, NULL, NULL),
		.w = funopen(client, NULL, imapWrite, NULL, imapClose),
	};
	if (!imap.r || !imap.w) err(EX_OSERR, "funopen");

	setlinebuf(imap.w);
	return imap;
}

static void imapLine(struct IMAP *imap) {
	ssize_t len = getline(&imap->buf, &imap->cap, imap->r);
	if (len < 0) errx(EX_PROTOCOL, "unexpected eof");
	if (len < 1 || imap->buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF");
	if (len < 2 || imap->buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR");
	imap->buf[len - 2] = '\0';
	imap->ptr = imap->buf;
}

static struct Data parseAtom(struct IMAP *imap) {
	size_t len = (*imap->ptr == '.' ? 1 : strcspn(imap->ptr, " .()[]{\""));
	struct Data data = {
		.type = Atom,
		.atom = atomn(imap->ptr, len),
	};
	imap->ptr += len;
	return data;
}

static struct Data parseNumber(struct IMAP *imap) {
	return (struct Data) {
		.type = Number,
		.number = strtoull(imap->ptr, &imap->ptr, 10),
	};
}

static struct Data parseQuoted(struct IMAP *imap) {
	imap->ptr++;
	size_t len = strcspn(imap->ptr, "\"");
	if (imap->ptr[len] != '"') {
		errx(EX_PROTOCOL, "missing quoted string delimiter");
	}
	struct Data data = {
		.type = String,
		.string = strndup(imap->ptr, len),
	};
	if (!data.string) err(EX_OSERR, "strndup");
	imap->ptr += len + 1;
	return data;
}

static struct Data parseLiteral(struct IMAP *imap) {
	imap->ptr++;
	size_t len = strtoull(imap->ptr, &imap->ptr, 10);
	if (*imap->ptr != '}') {
		errx(EX_PROTOCOL, "missing literal prefix delimiter");
	}
	struct Data data = {
		.type = String,
		.string = malloc(len + 1),
	};
	if (!data.string) err(EX_OSERR, "malloc");
	size_t n = fread(data.string, len, 1, imap->r);
	if (!n) errx(EX_PROTOCOL, "truncated literal");
	imapLine(imap);
	data.string[len] = '\0';
	return data;
}

static struct Data parseData(struct IMAP *imap);

static struct Data parseList(struct IMAP *imap, char close) {
	if (*imap->ptr) imap->ptr++;
	struct Data data = { .type = List };
	while (*imap->ptr != close) {
		listPush(&data.list, parseData(imap));
	}
	if (*imap->ptr) imap->ptr++;
	return data;
}

static struct Data parseData(struct IMAP *imap) {
	if (*imap->ptr == ' ') imap->ptr++;
	if (*imap->ptr == '"') return parseQuoted(imap);
	if (*imap->ptr == '{') return parseLiteral(imap);
	if (*imap->ptr == '(') return parseList(imap, ')');
	if (*imap->ptr == '[') return parseList(imap, ']');
	if (*imap->ptr >= '0' && *imap->ptr <= '9') return parseNumber(imap);
	if (*imap->ptr) return parseAtom(imap);
	errx(EX_PROTOCOL, "unexpected eof");
}

struct Resp imapResp(struct IMAP *imap) {
	struct Data data;
	struct Resp resp = {0};
	imapLine(imap);

	data = parseData(imap);
	if (data.type != Atom) errx(EX_PROTOCOL, "expected tag atom");
	resp.tag = data.atom;
	if (resp.tag == AtomContinue) {
		if (*imap->ptr == ' ') imap->ptr++;
		resp.text = imap->ptr;
		return resp;
	}

	data = parseData(imap);
	if (data.type == Number) {
		resp.number = data.number;
		data = parseData(imap);
	}
	if (data.type != Atom) errx(EX_PROTOCOL, "expected response atom");
	resp.resp = data.atom;

	if (
		resp.resp == AtomOk ||
		resp.resp == AtomNo ||
		resp.resp == AtomBad ||
		resp.resp == AtomPreauth ||
		resp.resp == AtomBye
	) {
		if (*imap->ptr == ' ') imap->ptr++;
		if (*imap->ptr == '[') {
			data = parseList(imap, ']');
			resp.code = data.list;
		}
		if (*imap->ptr == ' ') imap->ptr++;
		resp.text = imap->ptr;
	} else {
		data = parseList(imap, '\0');
		resp.data = data.list;
	}

	return resp;
}

struct Resp imapIdle(struct IMAP *imap, enum Atom tag) {
	for (;;) {
		fprintf(imap->w, "%s IDLE\r\n", Atoms[tag]);
		struct Resp resp = imapResp(imap);
		if (resp.tag != AtomContinue) {
			fprintf(imap->w, "DONE\r\n");
			return resp;
		}
		respFree(resp);

		struct pollfd pfd = { .fd = imap->sock, .events = POLLIN };
		int ready = poll(&pfd, 1, 29 * 60 * 1000);
		if (ready < 0) err(EX_IOERR, "poll");

		fprintf(imap->w, "DONE\r\n");
		resp = imapResp(imap);
		if (ready || resp.tag != tag) return resp;
		respFree(resp);
	}
}