From 9f586a8e79c73cba5267aa1848c72123ded37f9b Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 3 Aug 2026 11:30:06 +0200 Subject: Initial commit: agent-safe notmuch wrapper mailctl is a deliberately narrow CLI wrapper around notmuch, built so an AI coding agent can search and organize local mail with no ability to send. There is no SMTP, reply, or compose code path in the tool. Safety model: - reads run freely, mutations are gated - tag changes are dry-run until --apply - cross-account mutations need an explicit --all-accounts - destructive tags need --apply and --confirm-destructive - bulk mutations are capped by --max-messages - every applied mutation is audited to a local log Accounts are not in the source. They load from ~/.config/mailctl/accounts.json and are validated against both the schema and the actual maildirs on disk at import time, so a typo cannot produce a query matching nothing or a draft under the wrong identity. Ships with mailsync.sh (a separate mbsync + notmuch new driver, meant for cron) and mail-organize, a Claude Code skill that makes mailctl the only sanctioned interface to the user's mail. Licensed GPLv2-only. Co-Authored-By: Claude Opus 5 --- mailsync.sh | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100755 mailsync.sh (limited to 'mailsync.sh') diff --git a/mailsync.sh b/mailsync.sh new file mode 100755 index 0000000..1ea2aa2 --- /dev/null +++ b/mailsync.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# ~/bin/mailsync.sh +# +# Copyright (C) 2026 Danilo M. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# Defensive: don't rely on cron/systemd/whatever invokes this to have +# set these correctly. Explicit beats inferred, especially after the +# HOME-not-set failure we hit once already. Fall back to the invoking +# user's home from passwd rather than a hardcoded path. +export HOME="${HOME:-$(getent passwd "$(id -u)" | cut -d: -f6)}" +export GNUPGHOME="${GNUPGHOME:-$HOME/.gnupg}" + +LOCKFILE="/tmp/mbsync.lock" +LOGFILE="$HOME/.local/state/mailsync.log" +MAX_LOG_BYTES=$((10 * 1024 * 1024)) # rotate past 10MB, see note below + +exec 200>"$LOCKFILE" +if ! flock -n 200; then + echo "$(date -Iseconds) === SKIPPED: previous run still in progress ===" >> "$LOGFILE" + exit 1 +fi + +# Simple rotation: if the log's gotten big, keep the last run's worth +# and move the rest aside rather than letting it grow forever. +if [ -f "$LOGFILE" ] && [ "$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)" -gt "$MAX_LOG_BYTES" ]; then + mv "$LOGFILE" "${LOGFILE}.1" +fi + +START_TS="$(date -Iseconds)" +{ + echo "===== RUN START: $START_TS =====" + + # Timestamp every line of mbsync/notmuch output as it streams, + # rather than only marking run boundaries, this is what actually + # lets you tell which errors are from which run at a glance. + mbsync -a 2>&1 | while IFS= read -r line; do + echo "$(date '+%H:%M:%S') $line" + done + MBSYNC_STATUS=${PIPESTATUS[0]} + + notmuch new 2>&1 | while IFS= read -r line; do + echo "$(date '+%H:%M:%S') $line" + done + NOTMUCH_STATUS=${PIPESTATUS[0]} + + END_TS="$(date -Iseconds)" + if [ "$MBSYNC_STATUS" -eq 0 ] && [ "$NOTMUCH_STATUS" -eq 0 ]; then + echo "===== RUN END: $END_TS status=OK =====" + else + echo "===== RUN END: $END_TS status=FAILED mbsync=$MBSYNC_STATUS notmuch=$NOTMUCH_STATUS =====" + fi +} >> "$LOGFILE" + +exit 0 -- cgit v1.2.3