commit b27996f61e27d1416dcd11ee3143146f4fdeb8b8
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date: Mon, 7 Mar 2022 07:42:39 +0000
Add pop3 client
Diffstat:
A | Makefile | | | 36 | ++++++++++++++++++++++++++++++++++++ |
A | pop3.1 | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | pop3.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);
+}