/* Copyright (C) 2020 June 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 . * * Additional permission under GNU GPL version 3 section 7: * * If you modify this Program, or any covered work, by linking or * combining it with OpenSSL (or a modified version of that library), * containing parts covered by the terms of the OpenSSL License and the * original SSLeay license, the licensors of this Program grant you * additional permission to convey the resulting work. Corresponding * Source for a non-source form of such a combination shall include the * source code for the parts of OpenSSL used as well as that of the * covered work. */ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #ifdef DECLARE_RPP #define RPP_STDIN 1 char * readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags); #else #include #endif #include "imap.h" typedef unsigned char byte; static const char *uuidGen(void) { byte uuid[16]; int error = getentropy(uuid, sizeof(uuid)); if (error) err(EX_OSERR, "getentropy"); 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 *buf; size_t buflen; FILE *msg = open_memstream(&buf, &buflen); if (!msg) err(EX_OSERR, "open_memstream"); fprintf( msg, "From: <%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" "Subject: =?utf-8?Q?", from, date, uuid ); for (const char *ch = path; *ch; ++ch) { if ((uint8_t)*ch & 0x80) { fprintf(msg, "=%02hhX", (uint8_t)*ch); } else if (*ch == ' ') { fprintf(msg, "_"); } else { fprintf(msg, "%c", *ch); } } fprintf(msg, "?=\r\n\r\n"); 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); error = fclose(msg); if (error) err(EX_IOERR, "fclose"); 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: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 (optind < argc) user = argv[optind++]; argv += optind; argc -= optind; if (!user) errx(EX_USAGE, "username required"); if (!host) { host = strchr(user, '@'); if (!host) errx(EX_USAGE, "no domain in username"); host++; } 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 Resp resp; struct IMAP imap = imapOpen(host, port); respFree(respOk(imapResp(&imap))); enum Atom login = atom("login"); fprintf(imap.w, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass); for (; (resp = respOk(imapResp(&imap))).tag != login; respFree(resp)); respFree(resp); enum Atom select = atom("select"); fprintf(imap.w, "%s SELECT \"%s\"\r\n", Atoms[select], mailbox); for (; (resp = respOk(imapResp(&imap))).tag != select; respFree(resp)); respFree(resp); size_t cap = 0; char *entry = NULL; for (ssize_t len; 0 < (len = getline(&entry, &cap, map));) { if (entry[len - 1] == '\n') entry[len - 1] = '\0'; char *note = entry; char *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) continue; } uint32_t seq = 0; enum Atom search = atom("search"); fprintf( imap.w, "%s SEARCH HEADER X-Universally-Unique-Identifier \"%s\"\r\n", Atoms[search], uuid ); for (; (resp = respOk(imapResp(&imap))).tag != search; respFree(resp)) { if (resp.resp != AtomSearch) continue; 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; } } respFree(resp); if (!seq) goto append; struct tm date = {0}; enum Atom fetch = atom("fetch"); fprintf( imap.w, "%s FETCH %" PRIu32 " ENVELOPE\r\n", Atoms[fetch], seq ); for (; (resp = respOk(imapResp(&imap))).tag != fetch; respFree(resp)) { if (resp.resp != AtomFetch) continue; 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) errx(EX_PROTOCOL, "missing envelope date"); const char *rest = strptime( dataCheck(envelope.ptr[0], String).string, DATE_FORMAT, &date ); if (!rest) errx(EX_PROTOCOL, "invalid envelope date format"); } respFree(resp); struct stat status; int error = stat(note, &status); if (error) err(EX_NOINPUT, "%s", note); if (status.st_mtime < mktime(&date) && !force) { errx( EX_TEMPFAIL, "%s: note modified in mailbox; use -f to overwrite", note ); } else if (status.st_mtime == mktime(&date)) { continue; } append:; char *message = format(user, uuid, note); enum Atom append = atom("append"); fprintf( imap.w, "%s APPEND %s (\\Seen) {%zu}\r\n", Atoms[append], mailbox, strlen(message) ); for (; (resp = respOk(imapResp(&imap))).tag != append; respFree(resp)) { if (resp.tag == AtomContinue) fprintf(imap.w, "%s\r\n", message); } respFree(resp); free(message); if (!seq) { printf("+ %s\n", note); continue; } enum Atom delete = atom("delete"); fprintf( imap.w, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n", Atoms[delete], seq ); for (; (resp = respOk(imapResp(&imap))).tag != delete; respFree(resp)); respFree(resp); printf("~ %s\n", note); } if (ferror(map)) err(EX_IOERR, "%s", path); fprintf(imap.w, "ayy LOGOUT\r\n"); fclose(imap.r); fclose(imap.w); 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; }