pop3

Tiny pop3 client designed to be tunneled through ssh
git clone git://jacobedwards.org/pop3
Log | Files | Refs | README

pop3.c (11038B)


      1 /*
      2  * Copyright (c) 2022, 2023 Jacob R. Edwards <jacob@jacobedwards.org>
      3  *
      4  * Permission to use, copy, modify, and distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 /* ignore const */
     18 #define const
     19 
     20 #include <sys/socket.h>
     21 #include <sys/types.h>
     22 #include <sys/wait.h>
     23 
     24 #include <netdb.h>
     25 
     26 #include <ctype.h>
     27 #include <errno.h>
     28 #include <libgen.h>
     29 #include <limits.h>
     30 #include <readpassphrase.h>
     31 #include <signal.h>
     32 #include <stdarg.h>
     33 #include <stdio.h>
     34 #include <stdlib.h>
     35 #include <string.h>
     36 #include <time.h>
     37 #include <unistd.h>
     38 
     39 #ifndef __OpenBSD__
     40 #define pledge(A,B) (0)
     41 #include "openbsd-compat/explicit_bzero.h"
     42 #endif /* __OpenBSD__ */
     43 
     44 /* Limits */
     45 #define LISTMAX	9999	/* Maximum number of messages (see scanlisting struct) */
     46 #define LINEMAX	1002	/* Includes CRLF and nul (see RFC 5321 5.4.3.1.6.)  */
     47 #define POPMAX	513	/* POP3 status size including CRLF and nul */
     48 #define ARGMAX	40	/* POP3 argument size excluding nul */
     49 #define KEYMAX	4	/* POP3 keyword size excluding nul */
     50 
     51 /* Messy global error buffers */
     52 static char poperr[128];
     53 static int gaierr;
     54 
     55 /* Flag variables */
     56 static int delete;
     57 static int query;
     58 static int usestdin;
     59 static int trace;
     60 static int verbose;
     61 
     62 /* Option and argument variables */
     63 static char *port = "110";
     64 static char *user;
     65 static char *host;
     66 static char **mailer;
     67 
     68 static int bail;
     69 
     70 struct scanlisting {
     71 	char msg[4 + 1]; /* See LISTMAX */
     72 	int bytes;
     73 };
     74 
     75 void
     76 die(char *fmt, ...)
     77 {
     78 	va_list ap;
     79 	char *info;
     80 
     81 	if (gaierr && gaierr != EAI_SYSTEM)
     82 		info = gai_strerror(gaierr);
     83 	else if (*poperr)
     84 		info = poperr;
     85 	else
     86 		info = strerror(errno);
     87 
     88 	fprintf(stderr, "%s: ", getprogname());
     89 	va_start(ap, fmt);
     90 	vfprintf(stderr, fmt, ap);
     91 	va_end(ap);
     92 	fprintf(stderr, ": %s\n", info);
     93 	exit(1);
     94 }
     95 
     96 void
     97 usage(char *why)
     98 {
     99 	if (why)
    100 		fprintf(stderr, "%s\n", why);
    101 	fprintf(stderr, "usage: %s [-dqstv] [-p port] [-u user] host [mailer [arg ...]]\n",
    102 	    getprogname());
    103 	exit(1);
    104 }
    105 
    106 int
    107 resolve(char *host, char *serv)
    108 {
    109 	struct addrinfo hints;
    110 	struct addrinfo *res, *res0;
    111 	int fd;
    112 	int errno2;
    113 
    114 	memset(&hints, 0, sizeof(hints));
    115 	hints.ai_family = AF_UNSPEC;
    116 	hints.ai_socktype = SOCK_STREAM;
    117 	hints.ai_protocol = IPPROTO_TCP;
    118 	hints.ai_flags = AI_PASSIVE;
    119  
    120 	if ((gaierr = getaddrinfo(host, serv, &hints, &res0)) != 0)
    121 		return -1;
    122 
    123 	fd = -1;
    124 	errno2 = errno;
    125 	for (res = res0; fd < 0 && res; res = res->ai_next) {
    126 		fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    127 		if (fd >= 0) {
    128 			if (connect(fd, res->ai_addr, res->ai_addrlen)) {
    129 				close(fd);
    130 				fd = -1;
    131 			}
    132 		}
    133 	}
    134 
    135 	if (fd >= 0)
    136 		errno = errno2;
    137 	freeaddrinfo(res0);
    138 
    139 	return fd;
    140 }
    141 
    142 int
    143 pop_split(char **ap, int len, char *buf)
    144 {
    145 	int i;
    146 	char *p;
    147 
    148 	for (i = 0; (p = strsep(&buf, " ")); ++i)
    149 		if (i < len)
    150 			ap[i] = p;
    151 	return i;
    152 }
    153 
    154 char *
    155 pop_okay(char *buf)
    156 {
    157 	if (!buf)
    158 		return NULL;
    159 
    160 	if (strncmp(buf, "+OK", 3) == 0)
    161 		return buf + ((buf[3] == ' ') ? 4 : 3);
    162 	if (strncmp(buf, "-ERR ", 5) == 0)
    163 		strlcpy(poperr, buf + 5, sizeof(poperr));
    164 	return NULL;
    165 }
    166 
    167 char *
    168 pop_charcheck(char *buf)
    169 {
    170 	while (*buf && *buf != '\r' && *buf != '\n')
    171 		++buf;
    172 	return buf;
    173 }
    174 
    175 char *
    176 pop_read(FILE *fp, char *buf, int size)
    177 {
    178 	char *p;
    179 
    180 	if (!fgets(buf, size, fp)) {
    181 		if (!errno)
    182 			errno = ECONNRESET;
    183 		return NULL;
    184 	}
    185 
    186 	p = pop_charcheck(buf);
    187 	if (strcmp(p, "\r\n") != 0) {
    188 		errno = EFTYPE;
    189 		return NULL;
    190 	}
    191 
    192 	*p = '\0';
    193 	if (trace)
    194 		fprintf(stderr, "<S %s\n", buf);
    195 	return buf;
    196 }
    197 
    198 int
    199 pop_write(FILE *fp, char *keyword, char *arg) 
    200 {
    201 	char buf[KEYMAX + 1 + ARGMAX + 1];
    202 	int len;
    203 
    204 	if (arg)
    205 		len = snprintf(buf, sizeof(buf), "%s %s", keyword, arg);
    206 	else
    207 		len = snprintf(buf, sizeof(buf), "%s", keyword);
    208 
    209 	if (len < 0)
    210 		return -1;
    211 
    212 	if (len >= sizeof(buf)) {
    213 		errno = ENOBUFS;
    214 		return -1;
    215 	}
    216 
    217 	if (*pop_charcheck(buf)) {
    218 		errno = EFTYPE;
    219 		return -1;
    220 	}
    221 
    222 	if (trace)
    223 		fprintf(stderr, "C> %s\n", buf);
    224 	return fprintf(fp, "%s\r\n", buf) != len + 2;
    225 }
    226 
    227 FILE *
    228 pop_open(char *host, char *serv)
    229 {
    230 	FILE *fp;
    231 	char buf[POPMAX];
    232 	int fd;
    233 
    234 	fd = resolve(host, serv);
    235 	if (fd < 0)
    236 		return NULL;
    237 
    238 	if ((fp = fdopen(fd, "r+")) == NULL) {
    239 		close(fd);
    240 		return NULL;
    241 	}
    242 
    243 	if (!pop_okay(pop_read(fp, buf, sizeof(buf)))) {
    244 		fclose(fp);
    245 		return NULL;
    246 	}
    247 	return fp;
    248 }
    249 
    250 char *
    251 pop_comd(FILE *fp, char *buf, int size, char *keyword, char *arg)
    252 {
    253 	if (pop_write(fp, keyword, arg))
    254 		return NULL;
    255 	if (!pop_read(fp, buf, size))
    256 		return NULL;
    257 	return buf;
    258 }
    259 
    260 int
    261 pop_quit(FILE *fp)
    262 {
    263 	char buf[POPMAX];
    264 
    265 	if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "QUIT", NULL))) {
    266 		fclose(fp);
    267 		return 1;
    268 	}
    269 	return fclose(fp) < 0;
    270 }
    271 
    272 int
    273 pop_auth(FILE *fp, char *user, char *pass)
    274 {
    275 	char buf[POPMAX];
    276 
    277 	if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "USER", user)))
    278 		return 1;
    279 	if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "PASS", pass)))
    280 		return 1;
    281 	return 0;
    282 }
    283 
    284 int
    285 pop_stat(FILE *fp, int *msgs, int *bytes)
    286 {
    287 	char buf[POPMAX];
    288 	char *ap[2];
    289 	int nums[2];
    290 	char *p;
    291 	int i;
    292 
    293 	if (!pop_comd(fp, buf, sizeof(buf), "STAT", NULL))
    294 		return 1;
    295 
    296 	if (!(p = pop_okay(buf)) || pop_split(ap, 2, p) < 2) {
    297 		errno = EFTYPE;
    298 		return 1;
    299 	}
    300 
    301 	for (i = 0; i < 2; ++i) {
    302 		nums[i] = strtonum(ap[i], 0, INT_MAX, &p);
    303 		if (p)
    304 			return 1;
    305 	}
    306 
    307 	if (msgs)
    308 		*msgs = nums[0];
    309 	if (bytes)
    310 		*bytes = nums[1];
    311 	return 0;
    312 }
    313 
    314 struct scanlisting *
    315 pop_list(FILE *fp, int *_len)
    316 {
    317 	char *ap[2];
    318 	char buf[POPMAX];
    319 	struct scanlisting *listings;
    320 	int len, i;
    321 
    322 	if (pop_stat(fp, &len, NULL))
    323 		return NULL;
    324 
    325 	if (len > LISTMAX) {
    326 		/* More descriptive errors would be nice,
    327 		 * maybe just print them out.
    328 		 */
    329 		errno = EOVERFLOW;
    330 		return NULL;
    331 	}
    332 
    333 	listings = calloc(len, sizeof(*listings));
    334 	if (len == 0 || listings == NULL) {
    335 		if (_len)
    336 			*_len = 0;
    337 		return listings;
    338 	}
    339 
    340 	if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "LIST", NULL))) {
    341 		free(listings);
    342 		return NULL;
    343 	}
    344 
    345 	for (i = 0; i < len && pop_read(fp, buf, sizeof(buf)) &&
    346 	    strcmp(buf, ".") != 0; ++i) {
    347 		/* Beginning . would be invalid anyway,
    348 		 * so strtonum can check it.
    349 		 */
    350 		if (pop_split(ap, 2, buf) != 2) {
    351 			free(listings);
    352 			errno = EFTYPE;
    353 			return NULL;
    354 		}
    355 
    356 		if (strlcpy(listings[i].msg, ap[0], sizeof(listings[i].msg)) >=
    357 		    sizeof(listings[i].msg)) {
    358 			free(listings);
    359 			errno = ENOBUFS;
    360 			return NULL;
    361 		}
    362 
    363 		listings[i].bytes = strtonum(ap[1], 0, INT_MAX, &ap[0]);
    364 		if (ap[0]) {
    365 			free(listings);
    366 			return NULL;
    367 		}
    368 	}
    369 
    370 	if (_len)
    371 		*_len = len;
    372 	return listings;
    373 }
    374 
    375 int
    376 pop_readmsg(FILE *fp, char *buf)
    377 {
    378 	int len;
    379 
    380 	if (!pop_read(fp, buf, LINEMAX))
    381 		return -1;
    382 	if (strcmp(buf, ".") == 0)
    383 		return 0;
    384 
    385 	len = strlen(buf);
    386 	if (*buf == '.')
    387 		memmove(buf, buf + 1, --len);
    388 	return len + 1;
    389 }
    390 
    391 int
    392 pop_retr(FILE *fp, char *msg)
    393 {
    394 	char buf[POPMAX];
    395 
    396 	return pop_okay(pop_comd(fp, buf, sizeof(buf), "RETR", msg)) == NULL;
    397 }
    398 
    399 int
    400 pop_dele(FILE *fp, char *msg)
    401 {
    402 	char buf[POPMAX];
    403 
    404 	return pop_okay(pop_comd(fp, buf, sizeof(buf), "DELE", msg)) == NULL;
    405 }
    406 
    407 int
    408 writemsg(FILE *fp, char *msg, int out)
    409 {
    410 	char buf[LINEMAX];
    411 	int len;
    412 
    413 	if (pop_retr(fp, msg))
    414 		return 1;
    415 	while ((len = pop_readmsg(fp, buf)) > 0) {
    416 		if (dprintf(out, "%s\n", buf) < 0)
    417 			return 1;
    418 	}
    419 	return len != 0;
    420 }
    421 
    422 int
    423 mail(FILE *fp, char *msg, char **mailer)
    424 {
    425 	int fds[2];
    426 	int status;
    427 	pid_t pid;
    428 
    429 	if (pipe(fds))
    430 		return -1;
    431 
    432 	switch ((pid = fork())) {
    433 	case -1:
    434 		close(fds[0]);
    435 		close(fds[1]);
    436 		return -1;
    437 	case 0:
    438 		if (close(fds[1]) < 0 || dup2(fds[0], 0) < 0)
    439 			_exit(127);
    440 		execvp(*mailer, mailer);
    441 		_exit(127);
    442 	}
    443 
    444 	if (close(fds[0]) < 0 || writemsg(fp, msg, fds[1])) {
    445 		close(fds[1]);
    446 		kill(pid, SIGTERM);
    447 		return -1;
    448 	}
    449 
    450 	if (close(fds[1]) < 0) {
    451 		kill(pid, SIGTERM);
    452 		return -1;
    453 	}
    454 
    455 	if (waitpid(pid, &status, 0) < 0)
    456 		return -1;
    457 	return WEXITSTATUS(status);
    458 }
    459 
    460 int
    461 mbox(FILE *fp, char *msg, char **unused)
    462 {
    463 	char buf[LINEMAX];
    464 	int escape;
    465 	int len;
    466 	time_t tim;
    467 	char *stim;
    468 
    469 	if (pop_retr(fp, msg))
    470 		return -1;
    471 
    472 	tim = time(NULL);
    473 	stim = ctime(&tim);
    474 	if (!stim || printf("From <unknown> %s", stim) < 0)
    475 		return -1;
    476 	while ((len = pop_readmsg(fp, buf)) > 0) {
    477 		escape = strncmp(buf, "From ", 5) == 0;
    478 		if (printf(escape ? ">%s\n" : "%s\n", buf) < 0)
    479 			return -1;
    480 	}
    481 
    482 	if (len != 0) {
    483 		errno = ENOBUFS;
    484 		return -1;
    485 	}
    486 	if (printf("\n") < 0)
    487 		return -1;
    488 	return 0;
    489 }
    490 
    491 void
    492 sigbail(int unused)
    493 {
    494 	bail = 1;
    495 }
    496 
    497 int
    498 getmail(FILE *fp, int delete, char **mailer)
    499 {
    500 	int len, i;
    501 	int fails;
    502 	struct scanlisting *listings;
    503 	int (*sendmail)(FILE *, char *, char **);
    504 
    505 	listings = pop_list(fp, &len);
    506 	if (listings == NULL)
    507 		return -1;
    508 
    509 	fails = 0;
    510 	sendmail = mailer ? mail : mbox;
    511 	for (i = 0; i < len && !bail; ++i) {
    512 		if (sendmail(fp, listings[i].msg, mailer) ||
    513 		    (delete && pop_dele(fp, listings[i].msg)))
    514 			++fails;
    515 	}
    516 
    517 	free(listings);
    518 	return fails + (len - i);
    519 }
    520 
    521 int
    522 stat(FILE *fp, FILE *out)
    523 {
    524 	int msgs, bytes;
    525 
    526 	if (pop_stat(fp, &msgs, &bytes))
    527 		return 1;;
    528 	return fprintf(out, "%d %s %d %s\n",
    529 	    msgs, msgs == 1 ? "message" : "messages",
    530 	    bytes, bytes == 1 ? "byte" : "bytes") < 0;
    531 }
    532 
    533 int
    534 auth(FILE *fp, char *user, int usestdin)
    535 {
    536 	char pass[ARGMAX + 1];
    537 	int status;
    538 
    539 	if (!readpassphrase("Passphrase: ", pass, sizeof(pass),
    540 	    usestdin ? RPP_STDIN : RPP_REQUIRE_TTY))
    541 		die("unable to read passphrase");
    542 	status = pop_auth(fp, user, pass);
    543 	explicit_bzero(pass, sizeof(pass));
    544 
    545 	return status;
    546 }
    547 
    548 int
    549 main(int argc, char *argv[])
    550 {
    551 	FILE *fp;
    552 	int c;
    553 
    554 	if (pledge("stdio tty inet dns proc exec", NULL))
    555 		die("pledge");
    556 
    557 	user = getlogin();
    558 	while ((c = getopt(argc, argv, "dp:qstu:v")) >= 0) {
    559 		switch (c) {
    560 		case 'd':
    561 			delete = 1;
    562 			break;
    563 		case 'p':
    564 			port = optarg;
    565 			break;
    566 		case 'q':
    567 			query = 1;
    568 			break;
    569 		case 's':
    570 			usestdin = 1;
    571 			break;
    572 		case 't':
    573 			trace = 1;
    574 			break;
    575 		case 'u':
    576 			user = optarg;
    577 			break;
    578 		case 'v':
    579 			verbose = 1;
    580 			break;
    581 		default:
    582 			usage(NULL);
    583 		}
    584 	}
    585 	argc -= optind;
    586 	argv += optind;
    587 
    588 	if (argc < 1)
    589 		usage("No host");
    590 	host = argv[0];
    591 
    592 	if (argc > 1)
    593 		mailer = argv + 1;
    594 
    595 	fp = pop_open(host, port);
    596 	if (fp == NULL)
    597 		die("'%s'", host);
    598 
    599 	if (auth(fp, user, usestdin))
    600 		die("unable to authenticate for '%s'", user);
    601 
    602 	if (pledge(mailer ? "stdio proc exec" : "stdio", NULL))
    603 		die("pledge");
    604 
    605 	if (query) {
    606 		if (stat(fp, stdout))
    607 			die("unable to stat");
    608 	} else {
    609 		signal(SIGINT, sigbail);
    610 		signal(SIGTERM, sigbail);
    611 		if (getmail(fp, delete, mailer)) {
    612 			pop_quit(fp);
    613 			die("unable to get mail");
    614 		}
    615 		if (verbose && stat(fp, stderr))
    616 			die("unable to stat");
    617 	}
    618 
    619 	return !!pop_quit(fp);
    620 }