/* Copyright (C) 2019 C. McEnroe * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(DIG_PATH) && !defined(DRILL_PATH) # ifdef __FreeBSD__ # define DRILL_PATH "/usr/bin/drill" # else # define DIG_PATH "dig" # endif #endif static void compile(regex_t *regex, const char *pattern) { if (regex->re_nsub) return; int error = regcomp(regex, pattern, REG_EXTENDED | REG_NEWLINE); if (!error) return; char buf[256]; regerror(error, regex, buf, sizeof(buf)); errx(EX_SOFTWARE, "regcomp: %s: %s", buf, pattern); } static void printLines(const char *lines) { static regex_t fromRegex; compile(&fromRegex, "^>*From "); while (*lines) { size_t len = strcspn(lines, "\r\n"); regmatch_t match; if (!regexec(&fromRegex, lines, 1, &match, 0) && !match.rm_so) { printf(">%.*s\n", (int)len, lines); } else { printf("%.*s\n", (int)len, lines); } lines += len; if (*lines == '\r') lines++; if (*lines == '\n') lines++; } } static void mboxrd(const char *headers, const char *body) { #define MATCH(str, match) \ (int)((match).rm_eo - (match).rm_so), &(str)[(match).rm_so] static regex_t fromRegex, dateRegex; compile(&fromRegex, "^From: .*<([^>]+)>"); regmatch_t from[2]; int error = regexec(&fromRegex, headers, 2, from, 0); if (error) errx(EX_DATAERR, "missing From header"); printf("From %.*s ", MATCH(headers, from[1])); // Day, Date Month Year Time -> Day Month Date Time Year compile(&dateRegex, "^Date: (...), (..) (...) (....) (.{8})"); regmatch_t date[6]; error = regexec(&dateRegex, headers, 6, date, 0); if (error) errx(EX_DATAERR, "missing Date header"); printf( "%.*s %.*s %.*s %.*s %.*s\n", MATCH(headers, date[1]), MATCH(headers, date[3]), MATCH(headers, date[2]), MATCH(headers, date[5]), MATCH(headers, date[4]) ); printLines(headers); printLines(body); printf("\n"); #undef MATCH } static void lookup(const char **host, const char **port, const char *domain) { static char buf[1024]; snprintf(buf, sizeof(buf), "_imaps._tcp.%s", domain); int rw[2]; int error = pipe(rw); if (error) err(EX_OSERR, "pipe"); pid_t pid = fork(); if (pid < 0) err(EX_OSERR, "fork"); if (!pid) { close(rw[0]); dup2(rw[1], STDOUT_FILENO); dup2(rw[1], STDERR_FILENO); close(rw[1]); #ifdef DRILL_PATH execlp(DRILL_PATH, DRILL_PATH, buf, "SRV", NULL); err(EX_CONFIG, "%s", DRILL_PATH); #else execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL); err(EX_CONFIG, "%s", DIG_PATH); #endif } int status; pid = wait(&status); if (pid < 0) err(EX_OSERR, "wait"); close(rw[1]); FILE *pipe = fdopen(rw[0], "r"); if (!pipe) err(EX_IOERR, "fdopen"); fgets(buf, sizeof(buf), pipe); if (ferror(pipe)) err(EX_IOERR, "fgets"); if (!WIFEXITED(status) || WEXITSTATUS(status)) { fprintf(stderr, "%s", buf); exit(WEXITSTATUS(status)); } char *ptr = buf; #ifdef DRILL_PATH for (;;) { char *line = fgets(buf, sizeof(buf), pipe); if (!line || !strcmp(line, ";; ANSWER SECTION:\n")) break; } fgets(buf, sizeof(buf), pipe); if (ferror(pipe)) err(EX_IOERR, "fgets"); ptr = strrchr(buf, '\t'); ptr = (ptr ? ptr + 1 : buf); #endif fclose(pipe); char *dot = strrchr(ptr, '.'); if (dot) *dot = '\0'; strsep(&ptr, " \n"); // priority strsep(&ptr, " \n"); // weight *port = strsep(&ptr, " \n"); *host = strsep(&ptr, " \n"); if (!*host) { *host = domain; *port = "imaps"; } } static bool verbose; int tlsRead(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 (verbose) fprintf(stderr, "%.*s", (int)ret, ptr); return ret; } int tlsWrite(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 (verbose) fprintf(stderr, "%.*s", (int)ret, ptr); return ret; } int tlsClose(void *_tls) { struct tls *tls = _tls; int error = tls_close(tls); if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls)); return error; } #define ENUM_ATOM \ X(Unknown, "") \ X(Untagged, "*") \ X(Ok, "OK") \ X(No, "NO") \ X(Bad, "BAD") \ X(Bye, "BYE") \ X(Login, "LOGIN") \ X(Examine, "EXAMINE") \ X(Search, "SEARCH") \ X(Fetch, "FETCH") enum Atom { #define X(id, _) id, ENUM_ATOM #undef X AtomsLen, }; static const char *Atoms[AtomsLen] = { #define X(id, str) [id] = str, ENUM_ATOM #undef X }; static enum Atom atom(const char *str) { if (!str) return Unknown; for (enum Atom i = 0; i < AtomsLen; ++i) { if (!strcmp(str, Atoms[i])) return i; } return Unknown; } static char *readLiteral(FILE *imap, const char *line) { char *prefix = strrchr(line, '{'); if (!prefix) errx(EX_PROTOCOL, "no literal prefix"); size_t size = strtoul(prefix + 1, NULL, 10); if (!size) errx(EX_PROTOCOL, "invalid literal size"); char *literal = malloc(size + 1); if (!literal) err(EX_OSERR, "malloc"); size_t count = fread(literal, size, 1, imap); if (!count) errx(EX_PROTOCOL, "could not read literal"); literal[size] = '\0'; return literal; } int main(int argc, char *argv[]) { const char *host = NULL; const char *port = "imaps"; const char *mailbox = "INBOX"; const char *subject = "[PATCH"; const char *from = NULL; const char *to = NULL; const char *cc = NULL; int rppFlags = 0; int opt; while (0 < (opt = getopt(argc, argv, "C:F:S:T:h:m:p:vw"))) { switch (opt) { break; case 'C': cc = optarg; break; case 'F': from = optarg; break; case 'S': subject = optarg; break; case 'T': to = optarg; break; case 'h': host = optarg; break; case 'm': mailbox = optarg; break; case 'p': port = optarg; break; case 'v': verbose = true; break; case 'w': rppFlags |= RPP_STDIN; break; default: return EX_USAGE; } } const char *user = argv[optind]; if (!user) errx(EX_USAGE, "username required"); if (!host) { const char *domain = strchr(user, '@'); if (!domain) errx(EX_USAGE, "no domain in username"); lookup(&host, &port, &domain[1]); } char buf[1024]; char *pass = readpassphrase( (rppFlags & RPP_STDIN ? "" : "Password: "), buf, sizeof(buf), rppFlags ); if (!pass) err(EX_UNAVAILABLE, "readpassphrase"); 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); error = tls_connect(client, host, port); if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client)); FILE *imap = funopen(client, tlsRead, tlsWrite, NULL, tlsClose); if (!imap) err(EX_SOFTWARE, "funopen"); setlinebuf(imap); bool login = false; char *nums = NULL; char *line = NULL; size_t cap = 0; while (0 < getline(&line, &cap, imap)) { char *cr = strchr(line, '\r'); if (cr) *cr = '\0'; char *rest = line; enum Atom tag = atom(strsep(&rest, " ")); if (rest && isdigit(rest[0])) { strsep(&rest, " "); } enum Atom resp = atom(strsep(&rest, " ")); if (resp == No || resp == Bad || resp == Bye) { errx( EX_CONFIG, "%s: %s %s", Atoms[tag], Atoms[resp], (rest ? rest : "") ); } switch (tag) { break; case Untagged: { if (login) break; fprintf( imap, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[Login], user, pass ); login = true; } break; case Login: { fprintf(imap, "%s EXAMINE \"%s\"\r\n", Atoms[Examine], mailbox); } break; case Examine: { fprintf(imap, "%s SEARCH CHARSET UTF-8", Atoms[Search]); if (subject) fprintf(imap, " SUBJECT \"%s\"", subject); if (from) fprintf(imap, " FROM \"%s\"", from); if (to) fprintf(imap, " TO \"%s\"", to); if (cc) fprintf(imap, " CC \"%s\"", cc); fprintf(imap, "\r\n"); } break; case Search: { if (!nums) errx(EX_PROTOCOL, "no search response"); for (char *ch = nums; *ch; ++ch) { if (*ch == ' ') *ch = ','; } fprintf( imap, "%s FETCH %s (BODY[HEADER.FIELDS (" "Date From To Cc Subject Message-Id In-Reply-To " "Content-Transfer-Encoding" ")] BODY[TEXT])\r\n", Atoms[Fetch], nums ); free(nums); nums = NULL; } break; case Fetch: { fprintf(imap, "ayy LOGOUT\r\n"); fclose(imap); return EX_OK; } break; default:; } switch (resp) { break; case Search: { if (!rest) errx(EX_TEMPFAIL, "no matching messages"); nums = strdup(rest); if (!nums) err(EX_OSERR, "strdup"); } break; case Fetch: { char *headers = readLiteral(imap, rest); ssize_t len = getline(&line, &cap, imap); if (len <= 0) errx(EX_PROTOCOL, "unexpected eof after headers"); char *body = readLiteral(imap, line); len = getline(&line, &cap, imap); if (len <= 0) errx(EX_PROTOCOL, "unexpected eof after body"); if (strcmp(line, ")\r\n")) { errx(EX_PROTOCOL, "trailing data after headers and body"); } mboxrd(headers, body); free(headers); free(body); } break; default:; } } errx(EX_PROTOCOL, "unexpected eof"); }