/* 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);
}
}