/* Copyright (C) 2020 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 #ifndef NO_READPASSPHRASE_H #include #endif #if !defined(DIG_PATH) && !defined(DRILL_PATH) # ifdef __FreeBSD__ # define DRILL_PATH "/usr/bin/drill" # else # define DIG_PATH "dig" # endif #endif typedef unsigned char byte; 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; } 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 const char *uuidGen(void) { byte uuid[16]; arc4random_buf(uuid, sizeof(uuid)); uuid[6] &= 0x0F; uuid[6] |= 0x40; uuid[8] &= 0x3F; uuid[8] |= 0x80; static char str[sizeof("00000000-0000-0000-0000-000000000000")]; snprintf( str, sizeof(str), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15] ); return str; } static bool uuidCheck(const char *uuid) { if (strlen(uuid) != 36) return false; if (strspn(uuid, "0123456789abcdef-") != 36) return false; return true; } #define ENUM_ATOM \ X(Unknown, "") \ X(Untagged, "*") \ X(Ok, "OK") \ X(No, "NO") \ X(Bad, "BAD") \ X(Bye, "BYE") \ X(Login, "LOGIN") \ X(Search, "SEARCH") \ X(Fetch, "FETCH") \ X(Append, "APPEND") \ X(Next, "next") 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; } #define DATE_FORMAT "%a, %e %b %Y %H:%M:%S %z" static void append( FILE *imap, const char *mailbox, const char *from, const char *uuid, const char *path ) { FILE *note = fopen(path, "r"); if (!note) err(EX_NOINPUT, "%s", path); struct stat status; int error = fstat(fileno(note), &status); if (error) err(EX_IOERR, "%s", path); char date[sizeof("Mon, 00 Jan 0000 00:00:00 -0000")]; strftime( date, sizeof(date), DATE_FORMAT, localtime(&status.st_mtime) ); #define HEADERS \ "From: <%s>\r\n" \ "Subject: %s\r\n" \ "Date: %s\r\n" \ "X-Universally-Unique-Identifier: %s\r\n" \ "X-Uniform-Type-Identifier: com.apple.mail-note\r\n" \ "X-Mailer: notemap\r\n" \ "MIME-Version: 1.0\r\n" \ "Content-Type: text/plain; charset=\"utf-8\"\r\n" \ "Content-Transfer-Encoding: quoted-printable\r\n" \ "\r\n" size_t max = sizeof(HEADERS) + strlen(from) + strlen(path) + strlen(date) + strlen(uuid) + 3 * status.st_size + 3 * status.st_size / 76; char *buf = malloc(max); if (!buf) err(EX_OSERR, "malloc"); FILE *msg = fmemopen(buf, max, "w"); if (!msg) err(EX_OSERR, "fmemopen"); fprintf(msg, HEADERS, from, path, date, uuid); #undef HEADERS int ch; int len = 0; bool ws = false; while (EOF != (ch = fgetc(note))) { if (len == 75 && ch != '\n') { fprintf(msg, "=\r\n"); len = 0; } if (ch == '\n') { fprintf(msg, "%s\r\n", (ws ? "=\r\n" : "")); len = 0; } else if (ch == '\t' || (ch >= ' ' && ch <= '~' && ch != '=')) { fprintf(msg, "%c", ch); len++; } else { fprintf(msg, "=%02X", ch); len += 3; } ws = (ch == '\t' || ch == ' '); } if (ferror(note)) err(EX_IOERR, "%s", path); fclose(note); fclose(msg); buf[max - 1] = '\0'; fprintf( imap, "%s APPEND %s (\\Seen) {%zu}\r\n", Atoms[Append], mailbox, strlen(buf) ); if (fgetc(imap) == '+') { ungetc('+', imap); fprintf(imap, "%s\r\n", buf); } free(buf); } int main(int argc, char *argv[]) { const char *path = ".notemap"; bool add = false; bool force = false; const char *user = NULL; const char *host = NULL; const char *port = "imaps"; const char *mailbox = "Notes"; int rppFlags = 0; int opt; while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) { switch (opt) { break; case 'M': mailbox = optarg; break; case 'a': add = true; break; case 'f': force = true; break; case 'h': host = optarg; break; case 'm': path = optarg; break; case 'p': port = optarg; break; case 'u': user = optarg; break; case 'v': verbose = true; break; case 'w': rppFlags |= RPP_STDIN; break; default: return EX_USAGE; } } if (!user) errx(EX_USAGE, "username required"); argv += optind; argc -= optind; 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"); if (add) { FILE *map = fopen(path, "a"); if (!map) err(EX_CANTCREAT, "%s", path); for (int i = 0; i < argc; ++i) { if (access(argv[i], R_OK)) err(EX_NOINPUT, "%s", argv[i]); fprintf(map, "%s %s\n", uuidGen(), argv[i]); if (ferror(map)) err(EX_IOERR, "%s", path); } int error = fclose(map); if (error) err(EX_IOERR, "%s", path); } FILE *map = fopen(path, "r"); if (!map) err(EX_NOINPUT, "%s", path); 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); char *line = NULL; size_t lineCap = 0; char *entry = NULL; size_t entryCap = 0; char *uuid = NULL; char *note = NULL; int seq = 0; bool login = false; while (0 < getline(&line, &lineCap, 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 SELECT \"%s\"\r\n", Atoms[Next], mailbox); } break; case Next: Next: { ssize_t len; len = getline(&entry, &entryCap, map); if (ferror(map)) err(EX_IOERR, "%s", path); if (len < 1) goto done; if (entry[len - 1] == '\n') entry[len - 1] = '\0'; note = entry; uuid = strsep(¬e, " "); if (!note || !uuid || !uuidCheck(uuid)) { errx(EX_CONFIG, "invalid map entry: %s", entry); } if (argc) { int i; for (i = 0; i < argc; ++i) { if (!argv[i]) continue; if (strcmp(argv[i], note)) continue; argv[i] = NULL; break; } if (i == argc) goto Next; } fprintf( imap, "%s SEARCH HEADER X-Universally-Unique-Identifier %s\r\n", Atoms[Search], uuid ); } break; case Search: { if (!seq) goto Fetch; fprintf(imap, "%s FETCH %d ENVELOPE\r\n", Atoms[Fetch], seq); } break; case Fetch: Fetch: { append(imap, mailbox, user, uuid, note); } break; case Append: { printf("%c %s\n", (seq ? '~' : '+'), note); if (!seq) goto Next; fprintf( imap, "%s STORE %d +FLAGS.SILENT (\\Deleted)\r\n", Atoms[Next], seq ); } break; default:; } if (resp == Search) { if (rest) { seq = strtol(rest, &rest, 10); if (*rest) { errx(EX_CONFIG, "multiple messages matching %s", uuid); } } else { seq = 0; } } if (resp == Fetch) { if (strncmp(rest, "(ENVELOPE", 9)) continue; struct tm date = {0}; rest = strptime( rest, "(ENVELOPE (\"" DATE_FORMAT "\"", &date ); if (!rest) errx(EX_PROTOCOL, "invalid envelope date"); struct stat status; int error = stat(note, &status); if (error) err(EX_NOINPUT, "%s", note); if (!force && status.st_mtime < mktime(&date)) { errx( EX_TEMPFAIL, "%s: note modified in mailbox; use -f to overwrite", note ); } else if (status.st_mtime == mktime(&date)) { goto Next; } } } done: fprintf(imap, "ayy LOGOUT\r\n"); fclose(imap); int ret = EX_OK; for (int i = 0; i < argc; ++i) { if (!argv[i]) continue; warnx("%s: unmapped note; use -a to add", argv[i]); ret = EX_CONFIG; } return ret; }