diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 31 | ||||
-rw-r--r-- | README.7 | 45 | ||||
-rw-r--r-- | compat.c | 89 | ||||
-rw-r--r-- | compat.h | 94 | ||||
-rwxr-xr-x | configure | 50 | ||||
-rw-r--r-- | getservinfo.c | 122 | ||||
-rw-r--r-- | git-notemap.sh | 90 | ||||
-rw-r--r-- | imap.c | 169 | ||||
-rw-r--r-- | imap.h | 34 | ||||
-rw-r--r-- | notemap.1 | 157 | ||||
-rw-r--r-- | notemap.c | 368 |
12 files changed, 763 insertions, 487 deletions
diff --git a/.gitignore b/.gitignore index 7dbdb7e..9e9cfb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.o config.mk +git-notemap notemap diff --git a/Makefile b/Makefile index caaa9ac..5475b97 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,34 @@ -PREFIX = /usr/local -MANDIR = ${PREFIX}/share/man -LIBRESSL_PREFIX = /usr/local +PREFIX ?= /usr/local +MANDIR ?= ${PREFIX}/share/man CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -CFLAGS += ${LIBRESSL_PREFIX:%=-I%/include} -LDFLAGS += ${LIBRESSL_PREFIX:%=-L%/lib} -LDLIBS = -lcrypto -ltls +LDLIBS = -ltls -include config.mk +BINS = notemap git-notemap + +OBJS += getservinfo.o OBJS += imap.o OBJS += notemap.o +all: ${BINS} + notemap: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ -${OBJS}: compat.h imap.h +${OBJS}: imap.h clean: - rm -f notemap ${OBJS} + rm -f ${BINS} ${OBJS} -install: notemap notemap.1 - install -d ${PREFIX}/bin ${MANDIR}/man1 - install notemap ${PREFIX}/bin - gzip -c notemap.1 > ${MANDIR}/man1/notemap.1.gz +install: ${BINS} notemap.1 + install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1 + install ${BINS} ${DESTDIR}${PREFIX}/bin + install -m 644 notemap.1 ${DESTDIR}${MANDIR}/man1 + ln -fs notemap.1 ${DESTDIR}${MANDIR}/man1/git-notemap.1 uninstall: - rm -f ${PREFIX}/bin/notemap ${MANDIR}/man1/notemap.1.gz + rm -f ${BINS:%=${DESTDIR}${PREFIX}/bin/%} + rm -f ${DESTDIR}${MANDIR}/man1/notemap.1 + rm -f ${DESTDIR}${MANDIR}/man1/git-notemap.1 diff --git a/README.7 b/README.7 index e995e57..ae50df3 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd May 5, 2020 +.Dd December 16, 2020 .Dt README 7 .Os "Causal Agency" . @@ -16,39 +16,18 @@ easily accessible from the phone. . .Pp .Nm -requires LibreSSL -.Pq Fl ltls -and either -.Xr dig 1 -or -.Xr drill 1 -for automatic IMAP server configuration. +requires +.Sy libtls , +provided by either +.Lk https://git.causal.agency/libretls/about LibreTLS +(for OpenSSL) +or by LibreSSL. . -.Pp -Build configuration can be written to -.Pa config.mk . -The install prefix of LibreSSL is set by -.Va LIBRESSL_PREFIX . -The path of -.Xr dig 1 -or -.Xr drill 1 -can be set by defining the C preprocessor macro -.Va DIG_PATH -or -.Va DRILL_PATH , -respectively. -The default is to set -.Va DRILL_PATH -to -.Pa /usr/bin/drill -on -.Fx -and set -.Va DIG_PATH -to -.Pa dig -everywhere else. +.Bd -literal -offset indent +\&./configure +make +sudo make install +.Ed . .Sh CONTRIBUTING The upstream URL of this project is diff --git a/compat.c b/compat.c new file mode 100644 index 0000000..f220499 --- /dev/null +++ b/compat.c @@ -0,0 +1,89 @@ +/* Copyright (C) 2019 June McEnroe <june@causal.agency> + * + * 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 <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with LibreSSL (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 LibreSSL used as well as that of the + * covered work. + */ + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags) { + if (flags) { + if (!fgets(buf, bufsiz, stdin)) return NULL; + return strsep(&buf, "\n"); + } + return getpass(prompt); +} + +typedef int Read(void *, char *, int); +typedef int Write(void *, const char *, int); +typedef fpos_t Seek(void *, fpos_t, int); +typedef int Close(void *); + +struct Funopen { + void *cookie; + Read *readfn; + Write *writefn; + Close *closefn; +}; + +static ssize_t cookieRead(void *cookie, char *buf, size_t size) { + struct Funopen *funopen = cookie; + return funopen->readfn(funopen->cookie, buf, size); +} + +static ssize_t cookieWrite(void *cookie, const char *buf, size_t size) { + struct Funopen *funopen = cookie; + return funopen->writefn(funopen->cookie, buf, size); +} + +static int cookieClose(void *cookie) { + struct Funopen *funopen = cookie; + int ret = 0; + if (funopen->closefn) ret = funopen->closefn(funopen->cookie); + free(cookie); + return ret; +} + +FILE *funopen( + const void *cookie, + Read *readfn, Write *writefn, Seek *seekfn, Close *closefn +) { + struct Funopen *funopen = malloc(sizeof(*funopen)); + if (!funopen) return NULL; + funopen->cookie = (void *)cookie; + funopen->readfn = readfn; + funopen->writefn = writefn; + assert(!seekfn); + funopen->closefn = closefn; + cookie_io_functions_t fns = { + .read = (readfn ? cookieRead : NULL), + .write = (writefn ? cookieWrite : NULL), + .close = cookieClose, + }; + return fopencookie(funopen, (!readfn ? "w" : !writefn ? "r" : "r+"), fns); +} diff --git a/compat.h b/compat.h deleted file mode 100644 index ac7cc3f..0000000 --- a/compat.h +++ /dev/null @@ -1,94 +0,0 @@ -/* Copyright (C) 2019 C. McEnroe <june@causal.agency> - * - * 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 <https://www.gnu.org/licenses/>. - */ - -#ifdef __linux__ - -#define _GNU_SOURCE - -#include <assert.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -// Defined in libcrypto: -void arc4random_buf(void *buf, size_t nbytes); - -#define NO_READPASSPHRASE_H -#define RPP_STDIN 1 -static inline char * -readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags) { - if (flags) { - if (!fgets(buf, bufsiz, stdin)) return NULL; - return strsep(&buf, "\n"); - } - return getpass(prompt); -} - -typedef int _readfn(void *, char *, int); -typedef int _writefn(void *, const char *, int); -typedef fpos_t _seekfn(void *, fpos_t, int); -typedef int _closefn(void *); - -struct _funopen { - void *cookie; - _readfn *readfn; - _writefn *writefn; - _closefn *closefn; -}; - -static inline ssize_t -_cookie_read(void *cookie, char *buf, size_t size) { - struct _funopen *funopen = cookie; - assert((size_t)(int)size == size); - return funopen->readfn(funopen->cookie, buf, size); -} - -static inline ssize_t -_cookie_write(void *cookie, const char *buf, size_t size) { - struct _funopen *funopen = cookie; - assert((size_t)(int)size == size); - return funopen->writefn(funopen->cookie, buf, size); -} - -static inline int -_cookie_close(void *cookie) { - struct _funopen *funopen = cookie; - int ret = (funopen->closefn ? funopen->closefn(funopen->cookie) : 0); - free(cookie); - return ret; -} - -static const cookie_io_functions_t _cookie_fns = { - .read = _cookie_read, - .write = _cookie_write, - .close = _cookie_close, -}; - -static inline FILE * -funopen( - const void *cookie, - _readfn *readfn, _writefn *writefn, _seekfn *seekfn, _closefn *closefn -) { - struct _funopen *funopen = malloc(sizeof(*funopen)); - if (!funopen) return NULL; - assert(!seekfn); - *funopen = (struct _funopen) { (void *)cookie, readfn, writefn, closefn }; - const char *mode = (!readfn ? "w" : !writefn ? "r" : "r+"); - return fopencookie(funopen, mode, _cookie_fns); -} - -#endif /* __linux__ */ diff --git a/configure b/configure new file mode 100755 index 0000000..57d878a --- /dev/null +++ b/configure @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +cflags() { + echo "CFLAGS += $*" +} +ldlibs() { + echo "LDLIBS ${o:-}= $*" + o=+ +} +config() { + pkg-config --print-errors "$@" + cflags $(pkg-config --cflags "$@") + ldlibs $(pkg-config --libs "$@") +} +defstr() { + cflags "-D'$1=\"$2\"'" +} +defvar() { + defstr "$1" "$(pkg-config --variable=$3 $2)${4:-}" +} + +exec >config.mk + +for opt; do + case "${opt}" in + (--prefix=*) echo "PREFIX = ${opt#*=}" ;; + (--mandir=*) echo "MANDIR = ${opt#*=}" ;; + (*) echo "warning: unsupported option ${opt}" >&2 ;; + esac +done + +case "$(uname)" in + (OpenBSD) + ldlibs -ltls + ;; + (Linux) + cflags -D_GNU_SOURCE -DDECLARE_RPP + ldlibs -lresolv + config libtls + echo 'OBJS += compat.o' + ;; + (Darwin) + ldlibs -lresolv + config libtls + ;; + (*) + config libtls + ;; +esac diff --git a/getservinfo.c b/getservinfo.c new file mode 100644 index 0000000..4bc4b34 --- /dev/null +++ b/getservinfo.c @@ -0,0 +1,122 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * 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 <https://www.gnu.org/licenses/>. + * + * 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 <netdb.h> +#include <netinet/in.h> +#include <resolv.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifndef EAI_BADHINTS +#define EAI_BADHINTS EAI_BADFLAGS +#endif + +#ifndef EAI_PROTOCOL +#define EAI_PROTOCOL EAI_BADFLAGS +#endif + +/* A wrapper around getaddrinfo(3) which first performs SRV record (RFC 2782) + * lookup. hints must be provided and hints->ai_protocol must be set. SRV + * lookup is skipped if servname is numerical. If SRV lookup is successful, the + * ai_canonname field of the first addrinfo structure returned is set to the + * target name. Only the first SRV record is used. Priority and weight are + * ignored. + */ +int getservinfo( + const char *hostname, const char *servname, + const struct addrinfo *hints, struct addrinfo **res +) { + if (!hints) return EAI_BADHINTS; + if (!hints->ai_protocol) return EAI_PROTOCOL; + + char *rest; + strtoul(servname, &rest, 10); + if (!*rest || hints->ai_flags & (AI_NUMERICHOST | AI_NUMERICSERV)) { + return getaddrinfo(hostname, servname, hints, res); + } + + struct protoent *proto = getprotobynumber(hints->ai_protocol); + if (!proto) return EAI_PROTOCOL; + + char dname[256]; + int len = snprintf( + dname, sizeof(dname), "_%s._%s.%s", + servname, proto->p_name, hostname + ); + if ((size_t)len >= sizeof(dname)) return EAI_OVERFLOW; + + uint8_t msg[512]; + len = res_query(dname, 1 /* IN */, 33 /* SRV */, msg, sizeof(msg)); + if (len < 12) return getaddrinfo(hostname, servname, hints, res); + uint8_t *ptr = &msg[12]; + +#define NAME_SKIP \ + for (uint8_t n; ptr < &msg[len] && (n = *ptr++); ptr += n) { \ + if (n & 0xC0) { \ + ptr++; \ + break; \ + } \ + } + + uint16_t qdcount = msg[4] << 8 | msg[5]; + for (uint16_t q = 0; ptr < &msg[len] && q < qdcount; ++q) { + NAME_SKIP; // QNAME + ptr += 4; // QTYPE, QCLASS + } + + NAME_SKIP; // NAME + ptr += 14; // TYPE, CLASS, TTL, RDLENGTH, Priority, Weight + if (&msg[len] < ptr + 3) { + return getaddrinfo(hostname, servname, hints, res); + } + + char port[sizeof("65535")]; + snprintf(port, sizeof(port), "%d", ptr[0] << 8 | ptr[1]); + ptr += 2; + + // Name compression is not used for Target. + if (!ptr[0]) return EAI_NONAME; + hostname = (const char *)&ptr[1]; + for (uint8_t n; ptr < &msg[len] && (n = *ptr); ptr += n) { + *ptr++ = '.'; + } + + struct addrinfo myHints = *hints; + myHints.ai_flags |= AI_NUMERICSERV; + myHints.ai_flags &= ~AI_CANONNAME; + int error = getaddrinfo(hostname, port, &myHints, res); + if (error) return error; + + (*res)->ai_canonname = strdup(hostname); + if (!(*res)->ai_canonname) { + freeaddrinfo(*res); + return EAI_MEMORY; + } + return 0; +} diff --git a/git-notemap.sh b/git-notemap.sh new file mode 100644 index 0000000..5fd4d7c --- /dev/null +++ b/git-notemap.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Copyright (C) 2020 June McEnroe <june@causal.agency> +# +# 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 <https://www.gnu.org/licenses/>. + +set -u + +add= +force= +verbose= +host=$(git config notemap.imapServer) +port=$(git config notemap.imapServerPort) +user=$(git config notemap.imapUser) +pass=$(git config notemap.imapPass) +mailbox=$(git config notemap.imapMailbox) +path=$(git config notemap.mapFile) + +OPTS_SPEC="\ +git notemap [<options>] [<args>...] +-- +M,mailbox=! mirror notes to mailbox +a,add! add new notes to map file +f,force! overwite modified notes in mailbox +h,host=! connect to IMAP on host +m,map-file=! set path to map file +p,port=! connect to IMAP on port +u,user=! log in to IMAP as user +v,verbose! log IMAP protocol to standard error +" +eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" + +while [ $# -gt 0 ]; do + opt=$1 + shift + case "${opt}" in + (-M) mailbox=$1; shift;; + (-a) add=yes;; + (-f) force=yes;; + (-h) host=$1; shift;; + (-m) path=$1; shift;; + (-p) port=$1; shift;; + (-u) user=$1; shift;; + (-v) verbose=yes;; + (--) break;; + esac +done +if [ -z "${user}" ]; then + echo "${0}: username required" >&2 + exit 1 +fi + +description() { + cat <<-EOF + protocol=imaps + host=${host:-${user#*@}} + username=${user%@*} + ${pass:+password=${pass}} + EOF +} + +if [ -z "${pass}" ]; then + pass=$(description | git credential fill | grep '^password=') + pass=${pass#*=} +fi + +printf '%s' "${pass}" | notemap -w \ + ${add:+-a} ${force:+-f} ${verbose:+-v} \ + ${path:+-m "${path}"} \ + ${host:+-h "${host}"} \ + ${port:+-p "${port}"} \ + ${mailbox:+-M "${mailbox}"} \ + "${user}" "$@" +status=$? +if [ $status -ne 78 ]; then + description | git credential approve +else + description | git credential reject +fi +exit $status diff --git a/imap.c b/imap.c index 7984e0e..c24fd42 100644 --- a/imap.c +++ b/imap.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * 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 @@ -12,20 +12,46 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * 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 "compat.h" - #include <err.h> +#include <netdb.h> +#include <netinet/in.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/socket.h> #include <sysexits.h> #include <tls.h> +#include <unistd.h> + +FILE *funopen( + const void *cookie, + int (*readfn)(void *, char *, int), + int (*writefn)(void *, const char *, int), + fpos_t (*seekfn)(void *, fpos_t, int), + int (*closefn)(void *) +); #include "imap.h" +int getservinfo( + const char *hostname, const char *servname, + const struct addrinfo *hints, struct addrinfo **res +); + const char *Atoms[AtomCap] = { #define X(id, str) [id] = str, ENUM_ATOM @@ -64,7 +90,7 @@ static int imapClose(void *_tls) { return error; } -void imapOpen(FILE **read, FILE **write, const char *host, const char *port) { +struct IMAP imapOpen(const char *host, const char *port) { struct tls *client = tls_client(); if (!client) errx(EX_SOFTWARE, "tls_client"); @@ -75,99 +101,128 @@ void imapOpen(FILE **read, FILE **write, const char *host, const char *port) { 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)); + struct addrinfo *head; + struct addrinfo hints = { + .ai_family = PF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP, + }; + error = getservinfo(host, port, &hints, &head); + if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error)); - *read = funopen(client, imapRead, NULL, NULL, NULL); - *write = funopen(client, NULL, imapWrite, NULL, imapClose); - if (!*read || !*write) err(EX_SOFTWARE, "funopen"); + int sock = -1; + for (struct addrinfo *ai = head; ai; ai = ai->ai_next) { + sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock < 0) err(EX_OSERR, "socket"); - setlinebuf(*write); -} + error = connect(sock, ai->ai_addr, ai->ai_addrlen); + if (!error) break; -static size_t cap; -static char *buf; -static char *ptr; + close(sock); + sock = -1; + } + if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port); -static void imapLine(FILE *imap) { - ssize_t len = getline(&buf, &cap, imap); + error = tls_connect_socket( + client, sock, (head->ai_canonname ? head->ai_canonname : host) + ); + if (error) errx(EX_SOFTWARE, "tls_connect_socket: %s", tls_error(client)); + freeaddrinfo(head); + + struct IMAP imap = { + .sock = sock, + .r = funopen(client, imapRead, NULL, NULL, NULL), + .w = funopen(client, NULL, imapWrite, NULL, imapClose), + }; + if (!imap.r || !imap.w) err(EX_OSERR, "funopen"); + + setlinebuf(imap.w); + return imap; +} + +static void imapLine(struct IMAP *imap) { + ssize_t len = getline(&imap->buf, &imap->cap, imap->r); if (len < 0) errx(EX_PROTOCOL, "unexpected eof"); - if (len < 1 || buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF"); - if (len < 2 || buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR"); - buf[len - 2] = '\0'; - ptr = buf; + if (len < 1 || imap->buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF"); + if (len < 2 || imap->buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR"); + imap->buf[len - 2] = '\0'; + imap->ptr = imap->buf; } -static struct Data parseAtom(void) { - size_t len = (*ptr == '.' ? 1 : strcspn(ptr, " .()[]{\"")); +static struct Data parseAtom(struct IMAP *imap) { + size_t len = (*imap->ptr == '.' ? 1 : strcspn(imap->ptr, " .()[]{\"")); struct Data data = { .type = Atom, - .atom = atomn(ptr, len), + .atom = atomn(imap->ptr, len), }; - ptr += len; + imap->ptr += len; return data; } -static struct Data parseNumber(void) { +static struct Data parseNumber(struct IMAP *imap) { return (struct Data) { .type = Number, - .number = strtoull(ptr, &ptr, 10), + .number = strtoull(imap->ptr, &imap->ptr, 10), }; } -static struct Data parseQuoted(void) { - ptr++; - size_t len = strcspn(ptr, "\""); - if (ptr[len] != '"') errx(EX_PROTOCOL, "missing quoted string delimiter"); +static struct Data parseQuoted(struct IMAP *imap) { + imap->ptr++; + size_t len = strcspn(imap->ptr, "\""); + if (imap->ptr[len] != '"') { + errx(EX_PROTOCOL, "missing quoted string delimiter"); + } struct Data data = { .type = String, - .string = strndup(ptr, len), + .string = strndup(imap->ptr, len), }; if (!data.string) err(EX_OSERR, "strndup"); - ptr += len + 1; + imap->ptr += len + 1; return data; } -static struct Data parseLiteral(FILE *imap) { - ptr++; - size_t len = strtoull(ptr, &ptr, 10); - if (*ptr != '}') errx(EX_PROTOCOL, "missing literal prefix delimiter"); +static struct Data parseLiteral(struct IMAP *imap) { + imap->ptr++; + size_t len = strtoull(imap->ptr, &imap->ptr, 10); + if (*imap->ptr != '}') { + errx(EX_PROTOCOL, "missing literal prefix delimiter"); + } struct Data data = { .type = String, .string = malloc(len + 1), }; if (!data.string) err(EX_OSERR, "malloc"); - size_t n = fread(data.string, len, 1, imap); + size_t n = fread(data.string, len, 1, imap->r); if (!n) errx(EX_PROTOCOL, "truncated literal"); imapLine(imap); data.string[len] = '\0'; return data; } -static struct Data parseData(FILE *imap); +static struct Data parseData(struct IMAP *imap); -static struct Data parseList(FILE *imap, char close) { - if (*ptr) ptr++; +static struct Data parseList(struct IMAP *imap, char close) { + if (*imap->ptr) imap->ptr++; struct Data data = { .type = List }; - while (*ptr != close) { + while (*imap->ptr != close) { listPush(&data.list, parseData(imap)); } - if (*ptr) ptr++; + if (*imap->ptr) imap->ptr++; return data; } -static struct Data parseData(FILE *imap) { - if (*ptr == ' ') ptr++; - if (*ptr == '"') return parseQuoted(); - if (*ptr == '{') return parseLiteral(imap); - if (*ptr == '(') return parseList(imap, ')'); - if (*ptr == '[') return parseList(imap, ']'); - if (*ptr >= '0' && *ptr <= '9') return parseNumber(); - if (*ptr) return parseAtom(); +static struct Data parseData(struct IMAP *imap) { + if (*imap->ptr == ' ') imap->ptr++; + if (*imap->ptr == '"') return parseQuoted(imap); + if (*imap->ptr == '{') return parseLiteral(imap); + if (*imap->ptr == '(') return parseList(imap, ')'); + if (*imap->ptr == '[') return parseList(imap, ']'); + if (*imap->ptr >= '0' && *imap->ptr <= '9') return parseNumber(imap); + if (*imap->ptr) return parseAtom(imap); errx(EX_PROTOCOL, "unexpected eof"); } -struct Resp imapResp(FILE *imap) { +struct Resp imapResp(struct IMAP *imap) { struct Data data; struct Resp resp = {0}; imapLine(imap); @@ -176,8 +231,8 @@ struct Resp imapResp(FILE *imap) { if (data.type != Atom) errx(EX_PROTOCOL, "expected tag atom"); resp.tag = data.atom; if (resp.tag == AtomContinue) { - if (*ptr == ' ') ptr++; - resp.text = ptr; + if (*imap->ptr == ' ') imap->ptr++; + resp.text = imap->ptr; return resp; } @@ -196,13 +251,13 @@ struct Resp imapResp(FILE *imap) { resp.resp == AtomPreauth || resp.resp == AtomBye ) { - if (*ptr == ' ') ptr++; - if (*ptr == '[') { + if (*imap->ptr == ' ') imap->ptr++; + if (*imap->ptr == '[') { data = parseList(imap, ']'); resp.code = data.list; } - if (*ptr == ' ') ptr++; - resp.text = ptr; + if (*imap->ptr == ' ') imap->ptr++; + resp.text = imap->ptr; } else { data = parseList(imap, '\0'); resp.data = data.list; diff --git a/imap.h b/imap.h index 8b6b717..3b3bfc7 100644 --- a/imap.h +++ b/imap.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * 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 @@ -12,6 +12,17 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * 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. */ #ifndef IMAP_H @@ -143,13 +154,30 @@ struct Resp { const char *text; }; +static inline struct Resp respOk(struct Resp resp) { + if (resp.resp == AtomNo || resp.resp == AtomBad || resp.resp == AtomBye) { + errx(EX_CONFIG, "%s: %s", Atoms[resp.tag], resp.text); + } + return resp; +} + static inline void respFree(struct Resp resp) { listFree(resp.code); listFree(resp.data); } extern bool imapVerbose; -void imapOpen(FILE **read, FILE **write, const char *host, const char *port); -struct Resp imapResp(FILE *imapRead); + +struct IMAP { + int sock; + FILE *r; + FILE *w; + size_t cap; + char *buf; + char *ptr; +}; + +struct IMAP imapOpen(const char *host, const char *port); +struct Resp imapResp(struct IMAP *imap); #endif /* IMAP_H */ diff --git a/notemap.1 b/notemap.1 index 5f28ca5..0ba9adc 100644 --- a/notemap.1 +++ b/notemap.1 @@ -1,9 +1,10 @@ -.Dd May 5, 2020 +.Dd December 16, 2020 .Dt NOTEMAP 1 .Os . .Sh NAME -.Nm notemap +.Nm notemap , +.Nm git-notemap .Nd mirror notes to IMAP . .Sh SYNOPSIS @@ -13,6 +14,16 @@ .Op Fl h Ar host .Op Fl m Ar file .Op Fl p Ar port +.Ar user +.Op Ar +. +.Nm git +.Cm notemap +.Op Fl afv +.Op Fl M Ar mailbox +.Op Fl h Ar host +.Op Fl m Ar file +.Op Fl p Ar port .Op Fl u Ar user .Op Ar . @@ -21,14 +32,27 @@ The .Nm utility mirrors text notes -to an IMAP mailbox. +to an IMAP mailbox +appropriate for display +by the Apple Notes application. Files are mapped to IMAP messages -using a map file. -If no files are given as arguments, +using a map file, +by default +.Pa .notemap +in the current directory. +If no files are specified, all mapped files are mirrored. +New files are added +to the map file using +.Fl a . . .Pp -IMAP over TLS without STARTTLS is assumed. +IMAP over TLS is assumed. +The IMAP host and port +are automatically discovered +through SRV record lookup +on the domain portion of +.Ar user . The password is read from .Pa /dev/tty , or standard input if @@ -36,6 +60,15 @@ or standard input if is used. . .Pp +The +.Nm git-notemap +wrapper uses +.Xr git-config 1 +and +.Xr gitcredentials 7 +for defaults and authentication. +. +.Pp The arguments are as follows: .Bl -tag -width Ds .It Fl M Ar mailbox @@ -43,86 +76,85 @@ Mirror notes to .Ar mailbox . The default is .Sy Notes . -. .It Fl a Add new notes to the map file. -. .It Fl f -Overwrite notes which have changed in the mailbox. -. +Overwrite modified notes in the mailbox. .It Fl h Ar host Connect to IMAP on .Ar host . -The default host is determined -by SRV record lookup on the domain of -.Ar user , -or simply the domain name -if no SRV record exists. -Lookup requires -.Xr dig 1 . -. .It Fl m Ar file -Set the location of the map file. +Set the path of the map file. The default is .Pa .notemap . -. .It Fl p Ar port Connect to IMAP on .Ar port . -If the -.Fl h -option is used, -the default port is -.Sy imaps -(993). -Otherwise, -the port is determined -in the same fashion as the host. -. -.It Fl u Ar user -Log in to IMAP as -.Ar user . -The IMAP connection information -is inferred from the username unless -.Fl h -is used. -. .It Fl v Log IMAP protocol to standard error. -. .It Fl w Read the password from standard input. .El . +.Pp +The +.Nm git-notemap +wrapper loads defaults +from the following +.Xr git-config 1 +options: +.Cm notemap.imapServer , +.Cm notemap.imapServerPort , +.Cm notemap.imapUser , +.Cm notemap.imapPass , +.Cm notemap.imapMailbox , +.Cm notemap.mapFile . +If +.Cm notemap.imapPass +is unset, +the password is obtained through +.Xr gitcredentials 7 . +. .Sh EXAMPLES .Bd -literal -notemap -a -u june@causal.agency note.txt +notemap -a june@causal.agency note.txt +git config notemap.imapUser june@causal.agency +git notemap note.txt .Ed . .Sh STANDARDS .Bl -item .It .Rs +.%A N. Borenstein +.%A N. Freed +.%T MIME Part One: Format of Internet Message Bodies +.%I IETF +.%R RFC 2045 +.%U https://tools.ietf.org/html/rfc2045 +.%D November 1996 +.Re +.It +.Rs .%A M. Crispin -.%Q University of Washington .%T INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 .%I IETF -.%N RFC 3501 -.%D March 2003 +.%R RFC 3501 .%U https://tools.ietf.org/html/rfc3501 +.%Q University of Washington +.%D March 2003 .Re -. .It .Rs -.%A N. Freed -.%A N. Borenstein -.%T MIME Part One: Format of Internet Message Bodies +.%A L. Esibov +.%A A. Gulbrandsen +.%A P. Vixie +.%T A DNS RR for specifying the location of services (DNS SRV) .%I IETF -.%N RFC 2045 -.%D November 1996 -.%U https://tools.ietf.org/html/rfc2045 +.%R RFC 2782 +.%U https://tools.ietf.org/html/rfc2782 +.%D February 2000 .Re -. .It .Rs .%A P. Leach @@ -130,25 +162,32 @@ notemap -a -u june@causal.agency note.txt .%A R. Salz .%T A Universally Unique IDentifier (UUID) URN Namespace .%I IETF -.%N RFC 4122 -.%D July 2005 +.%R RFC 4122 .%U https://tools.ietf.org/html/rfc4122 +.%D July 2005 +.Re +.It +.Rs +.%A P. Mockapetris +.%T DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION +.%I IETF +.%R RFC 1035 +.%U https://tools.ietf.org/html/rfc1035 +.%D November 1987 .Re -. .It .Rs .%A P. Resnick, Ed. -.%Q Qualcomm Incorporated .%T Internet Message Format .%I IETF -.%N RFC 5322 -.%D October 2008 +.%R RFC 5322 .%U https://tools.ietf.org/html/rfc5322 +.%D October 2008 .Re .El . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency . .Sh CAVEATS Notes are assumed to be plain UTF-8 text @@ -160,4 +199,4 @@ Send mail to or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . diff --git a/notemap.c b/notemap.c index 61112e5..a7e79c2 100644 --- a/notemap.c +++ b/notemap.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * 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 @@ -12,10 +12,19 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * 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 "compat.h" - #include <err.h> #include <inttypes.h> #include <stdbool.h> @@ -29,91 +38,26 @@ #include <time.h> #include <unistd.h> -#ifndef NO_READPASSPHRASE_H -#include <readpassphrase.h> -#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 +#ifdef __APPLE__ +#include <sys/random.h> #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); +#ifdef DECLARE_RPP +#define RPP_STDIN 1 +char * readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags); #else - execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL); - err(EX_CONFIG, "%s", DIG_PATH); +#include <readpassphrase.h> #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)); - } +#include "imap.h" - 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"; - } -} +typedef unsigned char byte; static const char *uuidGen(void) { byte uuid[16]; - arc4random_buf(uuid, sizeof(uuid)); + int error = getentropy(uuid, sizeof(uuid)); + if (error) err(EX_OSERR, "getentropy"); + uuid[6] &= 0x0F; uuid[6] |= 0x40; uuid[8] &= 0x3F; @@ -153,33 +97,24 @@ static char *format(const char *from, const char *uuid, const char *path) { localtime(&status.st_mtime) ); -#define HEADERS \ - "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?" -#define HEADERS_END "?=\r\n\r\n" - - size_t max = sizeof(HEADERS) - + strlen(from) - + strlen(date) - + strlen(uuid) - + 3 * strlen(path) - + sizeof(HEADERS_END) - + 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, date, uuid); - + 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); @@ -189,7 +124,7 @@ static char *format(const char *from, const char *uuid, const char *path) { fprintf(msg, "%c", *ch); } } - fprintf(msg, HEADERS_END); + fprintf(msg, "?=\r\n\r\n"); int ch; int len = 0; @@ -213,9 +148,10 @@ static char *format(const char *from, const char *uuid, const char *path) { } if (ferror(note)) err(EX_IOERR, "%s", path); fclose(note); - fclose(msg); - buf[max - 1] = '\0'; + error = fclose(msg); + if (error) err(EX_IOERR, "fclose"); + return buf; } @@ -231,7 +167,7 @@ int main(int argc, char *argv[]) { int rppFlags = 0; int opt; - while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) { + while (0 < (opt = getopt(argc, argv, "M:afh:m:p:vw"))) { switch (opt) { break; case 'M': mailbox = optarg; break; case 'a': add = true; @@ -245,14 +181,15 @@ int main(int argc, char *argv[]) { break; default: return EX_USAGE; } } - if (!user) errx(EX_USAGE, "username required"); + if (optind < argc) user = argv[optind++]; argv += optind; argc -= 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]); + host = strchr(user, '@'); + if (!host) errx(EX_USAGE, "no domain in username"); + host++; } char buf[1024]; @@ -277,153 +214,128 @@ int main(int argc, char *argv[]) { 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); - } + struct Resp resp; + struct IMAP imap = imapOpen(host, port); + respFree(respOk(imapResp(&imap))); - if (!login) { - login = atom("login"); - fprintf( - imap, "%s LOGIN \"%s\" \"%s\"\r\n", - Atoms[login], user, pass - ); - } + 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); - if (resp.tag == login) { - fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[next], mailbox); - } + 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); - 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'; + 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'; - note = entry; - uuid = strsep(¬e, " "); - if (!note || !uuid || !uuidCheck(uuid)) { - errx(EX_CONFIG, "invalid map entry: %s", entry); - } + 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) goto next; + if (argc) { + int i; + for (i = 0; i < argc; ++i) { + if (!argv[i]) continue; + if (strcmp(argv[i], note)) continue; + argv[i] = NULL; + break; } - - fprintf( - imap, - "%s SEARCH HEADER X-Universally-Unique-Identifier \"%s\"\r\n", - Atoms[AtomSearch], uuid - ); - continue; + if (i == argc) continue; } - if (resp.resp == AtomSearch) { + 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; - 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) { + 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 < 1) errx(EX_PROTOCOL, "missing envelope date"); - - struct tm date = {0}; - char *rest = strptime( + 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"); - - 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) + } + 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; } - if (resp.tag == AtomContinue) { - fprintf(imap, "%s\r\n", message); - free(message); +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 (resp.tag == create) { + if (!seq) { printf("+ %s\n", note); - goto next; + continue; } - if (resp.tag == replace) { - printf("~ %s\n", note); - fprintf( - imap, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n", - Atoms[next], seq - ); - } + 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); } - fclose(imapRead); - fclose(imap); + 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) { |