pop3

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

commit b27996f61e27d1416dcd11ee3143146f4fdeb8b8
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon,  7 Mar 2022 07:42:39 +0000

Add pop3 client

Diffstat:
AMakefile | 36++++++++++++++++++++++++++++++++++++
Apop3.1 | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apop3.c | 493+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 656 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,36 @@ +# $Id: Makefile,v 1.1 2022/03/07 07:42:39 jacob Exp $ + +NAME = pop3 +SRC = pop3.c +OBJ = ${SRC:.c=.o} + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man + +CC = cc +CFLAGS = -Wall -Wno-write-strings +LDFLAGS = -static + +all: ${NAME} + +${NAME}: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +${OBJ}: Makefile + +.c.o: + ${CC} ${CFLAGS} -c -o $@ $< + +clean: + rm -f ${NAME} ${OBJ} + +install: ${NAME} + cp -f ${NAME} ${PREFIX}/bin + cp -f ${NAME}.1 ${MANPREFIX}/man1 + +uninstall: + rm -f ${PREFIX}/bin/${NAME} + rm -f ${MANPREFIX}/man1/${NAME}.1 + +.SUFFIXES: .c .o +.PHONY: clean install uninstall diff --git a/pop3.1 b/pop3.1 @@ -0,0 +1,127 @@ +.\" $Id: pop3.1,v 1.1 2022/03/07 07:42:39 jacob Exp $ +.\" +.\" Copyright (c) 2022 Jacob R. Edwards +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd March 6, 2022 +.Dt POP3 1 +.Os +.Sh NAME +.Nm pop3 +.Nd fetch mail from POP3 server +.Sh SYNOPSIS +.Nm +.Op Fl kqst +.Op Fl p Ar port +.Op Fl u Ar user +.Ar host +.Sh DESCRIPTION +The +.Nm +utility retrieves mail from a POP3 server on +.Ar host , +outputting all mail in the mbox format. See +.Sx SECURITY CONSIDERATIONS +before use. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl k +Don't delete mail after retreiving it. +.It Fl q +Query maildrop statistics. Output is of the form: +.Bd -literal -offset indent -compact +M message(s) B byte(s) +.Ed +where M is the number of messages and B is the number of bytes. +.It Fl s +Read passphrase from +.Pa /dev/stdin , +by default +.Pa /dev/tty +is used. +.It Fl t +Trace POP3 communication. +.It Fl p Ar port +Connect to +.Ar host +on port +.Ar port . +Defaults to 110 (pop3). +.It Fl u Ar user +Login to POP3 +.Ar user +uesr. By default your login name (as returned by +.Xr getlogin 2 ) +is used. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Retrieve mail from pop3.xyz, append it to +.Pa mbox , +and delete it from the server +.Bd -literal -offset indent +$ pop3 pop3.xyz >> mbox +.Ed +.Pp +The same, except don't delete the mail +.Bd -literal -offset indent +$ pop3 -k pop3.xyz >> mbox +.Ed +.Pp +Like the last example, except read the passphrase from the +.Pa pass +file +.Bd -literal -offset indent +$ pop3 -ks < pass >> mbox +.Ed +.Pp +Like the first example, except communication is tunneled +through +.Xr ssh 1 +.Bd -literal -offset indent +$ ssh -f -L 1110:localhost:110 pop3.xyz sleep 10 +$ pop3 -p 1110 localhost >> mbox +.Ed +.Pp +Emulate pop3s support using +.Xr stunnel 8 +.Bd -literal -offset indent +$ stunnel -fd 0 < stunnel.conf +$ pop3 -p 1110 localhost +.Ed +where +.Pa stunnel.conf +contains +.Bd -literal -offset indent +[pop3s] +client = yes +connect = pop3.xyz +accept = 1110 +.Ed +.Sh SEE ALSO +.Xr popa3d 8 , +.Xr ssh 1 , +.Xr stunnel 8 +.Sh STANDARDS +.Nm +tries to comply with Internet Standard STD 53 (RFC 1939). +.St "STD 53" +.Sh AUTHORS +.An Jacob R. Edwards +.Sh SECURITY CONSIDERATIONS +.Nm +communicates in cleartext; an encrypted tunnel should be used if +security is a concern. diff --git a/pop3.c b/pop3.c @@ -0,0 +1,493 @@ +/* $Id: pop3.c,v 1.1 2022/03/07 07:42:39 jacob Exp $ */ + +/* + * Copyright (c) 2022 Jacob R. Edwards + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* ignore const */ +#define const + +#include <sys/socket.h> +#include <sys/types.h> + +#include <netdb.h> + +#include <ctype.h> +#include <errno.h> +#include <libgen.h> +#include <limits.h> +#include <readpassphrase.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* Limits */ +#define LISTMAX 9999 /* Maximum number of messages (see scanlisting struct) */ +#define LINEMAX 1002 /* Includes CRLF and nul (see RFC 5321 5.4.3.1.6.) */ +#define POPMAX 513 /* POP3 status size including CRLF and nul */ +#define ARGMAX 40 /* POP3 argument size excluding nul */ +#define KEYMAX 4 /* POP3 keyword size excluding nul */ + +/* Messy global error buffers */ +static char poperr[128]; +static int gaierr; + +/* Flag variables */ +static int keep; +static int query; +static int usestdin; +static int trace; + +/* Option and argument variables */ +static char *port = "110"; +static char *user; +static char *host; + +struct scanlisting { + char msg[4 + 1]; /* See LISTMAX */ + int bytes; +}; + +void +die(char *fmt, ...) +{ + va_list ap; + char *info; + + if (gaierr && gaierr != EAI_SYSTEM) + info = gai_strerror(gaierr); + else if (*poperr) + info = poperr; + else + info = strerror(errno); + + fprintf(stderr, "%s: ", getprogname()); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", info); + exit(1); +} + +void +usage(char *why) +{ + if (why) + fprintf(stderr, "%s\n", why); + fprintf(stderr, "usage: %s [-kqst] [-u user] [-p port] host\n", + getprogname()); + exit(1); +} + +int +resolve(char *host, char *serv) +{ + struct addrinfo hints; + struct addrinfo *res, *res0; + int fd; + int errno2; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + if ((gaierr = getaddrinfo(host, serv, &hints, &res0)) != 0) + return -1; + + fd = -1; + errno2 = errno; + for (res = res0; fd < 0 && res; res = res->ai_next) { + fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (fd >= 0) { + if (connect(fd, res->ai_addr, res->ai_addrlen)) { + close(fd); + fd = -1; + } + } + } + + if (fd >= 0) + errno = errno2; + freeaddrinfo(res0); + + return fd; +} + +int +pop_split(char **ap, int len, char *buf) +{ + int i; + char *p; + + for (i = 0; (p = strsep(&buf, " ")); ++i) + if (i < len) + ap[i] = p; + return i; +} + +char * +pop_okay(char *buf) +{ + if (!buf) + return NULL; + + if (strncmp(buf, "+OK", 3) == 0) + return buf + ((buf[3] == ' ') ? 4 : 3); + if (strncmp(buf, "-ERR ", 5) == 0) + strlcpy(poperr, buf + 5, sizeof(poperr)); + return NULL; +} + +char * +pop_charcheck(char *buf) +{ + /* + * The RFC doesn't seem to specify what servers are allowed + * to send. It DOES specify that clients can only send + * "printable ASCII characters". I'll just allow printable + * characters and tab. + */ + while (isprint(*buf) || *buf == '\t') + ++buf; + return buf; +} + +char * +pop_read(FILE *fp, char *buf, int size) +{ + char *p; + + if (!fgets(buf, size, fp)) + return NULL; + + p = pop_charcheck(buf); + if (strcmp(p, "\r\n") != 0) { + errno = EFTYPE; + return NULL; + } + + *p = '\0'; + if (trace) + fprintf(stderr, "<S %s\n", buf); + return buf; +} + +int +pop_write(FILE *fp, char *keyword, char *arg) +{ + char buf[KEYMAX + 1 + ARGMAX + 1]; + int len; + + if (arg) + len = snprintf(buf, sizeof(buf), "%s %s", keyword, arg); + else + len = snprintf(buf, sizeof(buf), "%s", keyword); + + if (len < 0) + return -1; + + if (len >= sizeof(buf)) { + errno = ENOBUFS; + return -1; + } + + if (*pop_charcheck(buf)) { + errno = EFTYPE; + return -1; + } + + if (trace) + fprintf(stderr, "C> %s\n", buf); + return fprintf(fp, "%s\r\n", buf) != len + 2; +} + +FILE * +pop_open(char *host, char *serv) +{ + FILE *fp; + char buf[POPMAX]; + int fd; + + fd = resolve(host, serv); + if (fd < 0) + return NULL; + + if ((fp = fdopen(fd, "r+")) == NULL) { + close(fd); + return NULL; + } + + if (!pop_okay(pop_read(fp, buf, sizeof(buf)))) { + fclose(fp); + return NULL; + } + return fp; +} + +char * +pop_comd(FILE *fp, char *buf, int size, char *keyword, char *arg) +{ + if (pop_write(fp, keyword, arg)) + return NULL; + if (!pop_read(fp, buf, size)) + return NULL; + return buf; +} + +int +pop_quit(FILE *fp) +{ + char buf[POPMAX]; + + if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "QUIT", NULL))) { + fclose(fp); + return 1; + } + return fclose(fp) < 0; +} + +int +pop_auth(FILE *fp, char *user, char *pass) +{ + char buf[POPMAX]; + + if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "USER", user))) + return 1; + if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "PASS", pass))) + return 1; + return 0; +} + +int +pop_stat(FILE *fp, int *msgs, int *bytes) +{ + char buf[POPMAX]; + char *ap[2]; + int nums[2]; + char *p; + int i; + + if (!pop_comd(fp, buf, sizeof(buf), "STAT", NULL)) + return 1; + + if (!(p = pop_okay(buf)) || pop_split(ap, 2, p) < 2) { + errno = EFTYPE; + return 1; + } + + for (i = 0; i < 2; ++i) { + nums[i] = strtonum(ap[i], 0, INT_MAX, &p); + if (p) + return 1; + } + + if (msgs) + *msgs = nums[0]; + if (bytes) + *bytes = nums[1]; + return 0; +} + +struct scanlisting * +pop_list(FILE *fp, int *_len) +{ + char *ap[2]; + char buf[POPMAX]; + struct scanlisting *listings; + int len, i; + + if (pop_stat(fp, &len, NULL)) + return NULL; + + if (len > LISTMAX) { + /* More descriptive errors would be nice, + * maybe just print them out. + */ + errno = EOVERFLOW; + return NULL; + } + listings = calloc(len, sizeof(*listings)); + if (len == 0 || listings == NULL) + return listings; + + if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "LIST", NULL))) + return NULL; + + for (i = 0; i < len && pop_read(fp, buf, sizeof(buf)) && + strcmp(buf, ".") != 0; ++i) { + /* Beginning . would be invalid anyway, + * so strtonum can check it. + */ + if (pop_split(ap, 2, buf) != 2) { + free(listings); + errno = EFTYPE; + return NULL; + } + + if (strlcpy(listings[i].msg, ap[0], sizeof(listings[i].msg)) >= + sizeof(listings[i].msg)) { + free(listings); + errno = ENOBUFS; + return NULL; + } + + listings[i].bytes = strtonum(ap[1], 0, INT_MAX, &ap[0]); + if (ap[0]) { + free(listings); + return NULL; + } + } + + if (_len) + *_len = len; + return listings; +} + +int +pop_retr(FILE *fp, char *msg) +{ + char buf[LINEMAX], *p; + + if (!pop_okay(pop_comd(fp, buf, sizeof(buf), "RETR", msg))) + return 1; + + while ((p = pop_read(fp, buf, sizeof(buf))) && strcmp(p, ".") != 0) { + if (*p == '.') + ++p; + if (puts(p) < 0) + return 1; + } + if (p == NULL) + return 1; + return puts("") < 0; +} + +int +pop_dele(FILE *fp, char *msg) +{ + char buf[POPMAX]; + + return pop_okay(pop_comd(fp, buf, sizeof(buf), "DELE", msg)) == NULL; +} + +int +mbox(FILE *fp, int delete) +{ + int len, i; + struct scanlisting *listings; + + listings = pop_list(fp, &len); + if (listings == NULL) + return 1; + + for (i = 0; i < len; ++i) { + if (pop_retr(fp, listings[i].msg) || + (delete && pop_dele(fp, listings[i].msg))) { + free(listings); + return 1; + } + } + + free(listings); + return 0; +} + +int +stat(FILE *fp) +{ + int msgs, bytes; + if (pop_stat(fp, &msgs, &bytes)) + return 1;; + return printf("%d %s %d %s\n", + msgs, msgs == 1 ? "message" : "messages", + bytes, bytes == 1 ? "byte" : "bytes") < 0; +} + +int +auth(FILE *fp, char *user, int usestdin) +{ + char pass[ARGMAX + 1]; + int status; + + if (!readpassphrase("Passphrase: ", pass, sizeof(pass), + usestdin ? RPP_STDIN : RPP_REQUIRE_TTY)) + die("unable to read passphrase"); + status = pop_auth(fp, user, pass); + explicit_bzero(pass, sizeof(pass)); + + return status; +} + +int +main(int argc, char *argv[]) +{ + FILE *fp; + int c; + + if (pledge("stdio tty inet dns", NULL)) + die("pledge"); + + user = getlogin(); + while ((c = getopt(argc, argv, "kp:qstu:")) >= 0) { + switch (c) { + case 'k': + keep = 1; + break; + case 'p': + port = optarg; + break; + case 'q': + query = 1; + break; + case 's': + usestdin = 1; + break; + case 't': + trace = 1; + break; + case 'u': + user = optarg; + break; + default: + usage(NULL); + } + } + argc -= optind; + argv += optind; + + if (argc > 1) + usage("Too many arguments"); + if (argc < 1) + usage("No host"); + host = argv[0]; + + fp = pop_open(host, port); + if (fp == NULL) + die("'%s'", host); + + if (auth(fp, user, usestdin)) + die("unable to authenticate for '%s'", user); + + if (pledge("stdio", NULL)) + die("pledge"); + + if (query ? stat(fp) : mbox(fp, !keep)) + die("unable to complete transaction"); + return !pop_quit(fp); +}