/* 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 "compat.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifndef NO_READPASSPHRASE_H #include #endif #include "imap.h" #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 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 DATE_FORMAT "%a, %e %b %Y %H:%M:%S %z" static char *format(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) ); char subj[78 - (sizeof("Subject: ") - 1)] = {0}; for (size_t i = 0; i < sizeof(subj) - 1; ++i) { if (!path[i]) break; subj[i] = (path[i] & 0x80 ? '?' : path[i]); } #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(subj) + 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, subj, 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'; return 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': imapVerbose = 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); size_t cap = 0; char *entry = NULL; char *uuid = NULL; char *note = NULL; uint32_t seq = 0; char *message = NULL; enum Atom login = 0; enum Atom next = atom("next"); enum Atom create = atom("create"); enum Atom replace = atom("replace"); FILE *imapRead, *imap; imapOpen(&imapRead, &imap, host, port); for ( struct Resp resp; resp = imapResp(imapRead), resp.resp != AtomBye; respFree(resp) ) { if (resp.resp == AtomNo || resp.resp == AtomBad) { errx(EX_CONFIG, "%s: %s", Atoms[resp.resp], resp.text); } if (!login) { login = atom("login"); fprintf( imap, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass ); } if (resp.tag == login) { fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[next], mailbox); } ssize_t len; if (resp.tag == next) { next: len = getline(&entry, &cap, map); if (ferror(map)) err(EX_IOERR, "%s", path); if (len < 1) { fprintf(imap, "ayy LOGOUT\r\n"); continue; } 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[AtomSearch], uuid ); continue; } if (resp.resp == AtomSearch) { if (resp.data.len > 1) { errx(EX_CONFIG, "multiple messages matching %s", uuid); } if (resp.data.len) { seq = dataCheck(resp.data.ptr[0], Number).number; fprintf( imap, "%s FETCH %" PRIu32 " ENVELOPE\r\n", Atoms[AtomFetch], seq ); } else { message = format(user, uuid, note); fprintf( imap, "%s APPEND %s (\\Seen) {%zu}\r\n", Atoms[create], mailbox, strlen(message) ); } } if (resp.resp == AtomFetch) { if (!resp.data.len) errx(EX_PROTOCOL, "missing fetch data"); struct List items = dataCheck(resp.data.ptr[0], List).list; if (items.len < 2) errx(EX_PROTOCOL, "missing fetch data items"); enum Atom item = dataCheck(items.ptr[0], Atom).atom; if (item != AtomEnvelope) continue; struct List envelope = dataCheck(items.ptr[1], List).list; if (envelope.len < 1) errx(EX_PROTOCOL, "missing envelope date"); struct tm date = {0}; char *rest = strptime( dataCheck(envelope.ptr[0], String).string, DATE_FORMAT, &date ); if (!rest) errx(EX_PROTOCOL, "invalid envelope date format"); 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; } message = format(user, uuid, note); fprintf( imap, "%s APPEND %s (\\Seen) {%zu}\r\n", Atoms[replace], mailbox, strlen(message) ); } if (resp.tag == AtomContinue) { fprintf(imap, "%s\r\n", message); free(message); } if (resp.tag == create) { printf("+ %s\n", note); goto next; } if (resp.tag == replace) { printf("~ %s\n", note); fprintf( imap, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n", Atoms[next], seq ); } } fclose(imapRead); 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; }