emacs-patch-review

Port of Thunderbird Patch Review to mu4e.

git clone git://mccd.space/emacs-patch-review
commit d931179501ec72ed4fd235007c1946c30335dc2f
parent f354486785675a1e3feda6f2751ef46a3e2bea39
Author: Pi Agent <agent@pi.local>
Date:   Tue,  4 Aug 2026 11:37:58 +0200

reply: drop quoted hunk header from review body

The interleaved reply quoted the '@@ -a,b +c,d @@' hunk header above
each quoted region.  The header is noise for patch authors — the
quoted context lines already identify the location — so quote only
the hunk lines (plus the '> [...]' elision marker when the head of
the hunk is skipped).

Diffstat:
Dmail/README.org | 156-------------------------------------------------------------------------------
Dmail/mail-auth | 64----------------------------------------------------------------
Dmail/mbsyncrc | 92-------------------------------------------------------------------------------
Dmail/msmtprc.example | 46----------------------------------------------
Dmail/mu4e-mail.el | 141-------------------------------------------------------------------------------
Dmail/service/conf | 21---------------------
Dmail/service/log/run | 11-----------
Dmail/service/run | 46----------------------------------------------
Mpatch-review-reply.el | 1-
Mpatch-review.el | 27++++++++++++++++-----------
Mtests/patch-review-test.el | 53+++++++++++++++++++++++++++++++++++++++++++++++++++--
11 files changed, 67 insertions(+), 591 deletions(-)
diff --git a/mail/README.org b/mail/README.org
@@ -1,155 +0,0 @@
-#+TITLE: Mail backend for patch-review (mbsync + mu4e + turnstile)
-
-Bundles the plumbing that [[../patch-review-mu4e.el][patch-review-mu4e]] sits on top of:
-two IMAP accounts kept in sync with =mbsync=, indexed by =mu4e=, sent via
-=msmtp=, refreshed on a timer by a per-user =turnstile= (runit) service.
-
-| Account                      | Server   | Maildir           | IMAP store |
-|------------------------------+----------+-------------------+------------|
-| marc.coquand@piva.earth      | Gmail    | ~/Maildir/gmail   | piva       |
-| marc@coquand.email           | Fastmail | ~/Maildir/fastmail | fastmail   |
-
-Fastmail's IMAP/SMTP login is =marcc@fastmail.fr=.
-
-Passwords are pulled from =passage= (=passage piva/gmail=,
-=passage fastmail/marcc=).  Because that needs a passkey *touch* on every
-decryption, they are cached on tmpfs (once per boot) by [[file:mail-auth][mail-auth]];
-no other component ever stores a password.
-
-* Files
-
-| File                    | Purpose                                                    |
-|-------------------------+------------------------------------------------------------|
-| [[file:mbsyncrc]]          | isync config (Channel per account, =Group all=)            |
-| [[file:mail-auth]]         | sh helper: =passage= → ~~/.cache/-on-tmpfs =password cache=  |
-| [[file:msmtprc.example]]  | one account per From, =passwordeval= reuses the cache      |
-| [[file:mu4e-mail.el]]      | two =mu4e-context=, bookmarks, msmtp routing on send       |
-| [[file:service/run]]        | turnstile runit =run=: =mail-auth && mbsync -a= loop       |
-| [[file:service/conf]]       | optional env overrides                                     |
-| [[file:service/log/run]]    | svlogd logger → ~/.local/share/mbsync/log                   |
-
-* Install
-
-All shell snippets use the repo at =~/src/emacs-patch-review=; adjust if
-you cloned elsewhere.
-
-** 1. password helper
-
-#+begin_src sh
-install -m0755 /srv/src/emacs-patch-review/mail/mail-auth ~/.local/bin/mail-auth
-#+end_src
-
-It must be on =PATH= (already for the =mccd= user) or referenced by
-=$MBSYNC_AUTH=.
-
-** 2. mbsync config
-
-#+begin_src sh
-ln -sf /srv/src/emacs-patch-review/mail/mbsyncrc ~/.mbsyncrc
-mkdir -p ~/Maildir
-mu init --maildir ~/Maildir \
-        --my-address marc.coquand@piva.earth \
-        --my-address marc@coquand.email
-#+end_src
-
-=mu init= is one-shot; the =--my-address= entries let mu mark sent mail
-as "mine" so the =U= in mu4e ("Unread") excludes your own replies.
-
-** 3. msmtp (sending)
-
-#+begin_src sh
-cp /srv/src/emacs-patch-review/mail/msmtprc.example ~/.msmtprc
-chmod 600 ~/.msmtprc
-#+end_src
-
-Your =init.el= already sets =send-mail-function= ='sendmail-send-it= and
-=sendmail-program= =/usr/bin/msmtp= in the =message= use-package.  If that
-is still overridden to =ssh-sendmail= for the legacy =space-mail= flow
-(search for the =sendmail-program= override after the message block),
-remove or guard that line so msmtp wins for outgoing mail globally.
-
-** 4. turnstile runit service
-
-=turnstiled= runs a per-user =runsvdir= over =~/.config/service/= (see
-=/usr/libexec/turnstile/runit=).  Each service is a directory with a
-=run= executable:
-
-#+begin_src sh
-mkdir -p ~/.config/service/mbsync/log
-ln -sf /srv/src/emacs-patch-review/mail/service/run        ~/.config/service/mbsync/run
-ln -sf /srv/src/emacs-patch-review/mail/service/conf       ~/.config/service/mbsync/conf
-ln -sf /srv/src/emacs-patch-review/mail/service/log/run    ~/.config/service/mbsync/log/run
-chmod +x ~/.config/service/mbsync/run ~/.config/service/mbsync/log/run
-#+end_src
-
-On next login turnstiled starts =mbsync= automatically; in an already
-logged-in session:
-
-#+begin_src sh
-sv u mbsync                               # start
-sv s mbsync                               # status / one loop's tail
-sv t mbsync                               # restart cleanly
-# current log:
-tail -f ~/.local/share/mbsync/log/current
-#+end_src
-
-If you use lingering (=var/lib/turnstiled/linger=, see
-=turnstiled.conf(5)=) the service keeps running after logout too.
-
-** 5. mu4e in Emacs
-
-Add to =init.el= (after the existing =message= block):
-
-#+begin_src elisp
-(use-package mu4e-mail
-  :ensure nil
-  :load-path "/srv/src/emacs-patch-review/mail")
-#+end_src
-
-That =require=s =mu4e-mail=, which itself puts the repo root on
-=load-path= and wires =patch-review-mu4e-review= onto =R= in
-=mu4e-view-mode= automatically.
-
-* First run
-
-#+begin_src sh
-~/.local/bin/mail-auth                      # touch the passkey once
-sv t mbsync                                 # or: mbsync -a
-mu info                                     # check the store
-#+end_src
-
-The passkey is touched again only when
-=$XDG_RUNTIME_DIR/mbsync/{gmail,fastmail}.pass= vanish (reboot / logout
-without lingering).  Set =MBSYNC_PW_TTL=<seconds>= in
-=[[file:service/conf][service/conf]] to also rotate the cache on a timer, at
-the cost of an extra passkey touch every interval.
-
-* Checking folder names
-
-Maildir path strings in [[file:mu4e-mail.el]] must match what =mbsync=
-writes on disk.  After the first sync:
-
-#+begin_src sh
-find ~/Maildir -maxdepth 3 -type d -name 'cur' -printf '%h\n' \
-    | sed "s#$HOME/Maildir/#/#" | sort
-#+end_src
-
-Expect among others =/gmail/[Gmail]/Sent Mail= and =/fastmail/Sent=.  If
-Gmail exposes something different (labels translated, "Sent" instead of
-"[Gmail]/Sent Mail"), edit =mu4e-sent-folder= / =mu4e-drafts-folder= /
-=mu4e-trash-folder= in the corresponding context in [[file:mu4e-mail.el]].
-
-* Caveats
-
-- =[Gmail]/All Mail= is deliberately **not** synced (it duplicates every
-  message).  Refiling a Gmail message moves it to a =/gmail/Archive=
-  label rather than to All Mail, which is non-lossy on Gmail.
-- =Expunge Both= is set on both channels.  Gmail keeps everything in All
-  Mail regardless, so deletes are reversible from the web; Fastmail
-  respects its Trash expunge normally.
-- =mbsync -a= runs the =Group all= (piva + fastmail).  Sync one account
-  with =mbsync piva= / =mbsync fastmail=; list remote mailboxes with
-  =mbsync -l piva=.
-- The service does **not** index mu (mu4e's =mu4e-update-interval= does,
-  so no contention on mu's lock).  If you want a headless index too,
-  add =mu index= to the loop in [[file:service/run][service/run]].
-\ No newline at end of file
diff --git a/mail/mail-auth b/mail/mail-auth
@@ -1,63 +0,0 @@
-#!/bin/sh
-# mail-auth -- cache passage-backed IMAP passwords for mbsync.
-#
-# `passage show` decrypts against an age identity kept on a passkey (often a
-# YubiKey), which requires a physical *touch* for every decryption.  Doing
-# that on every mbsync run is impractical, so this helper copies each
-# one-line password onto tmpfs ($XDG_RUNTIME_DIR, owned by you and wiped on
-# reboot/logout) and leaves it there.  mbsync's PassCmd then just reads the
-# cached file -- no passkey touch.
-#
-# The cache lives until reboot; touch the passkey again only when it's gone.
-# Set MBSYNC_PW_TTL=N (seconds) to *also* rotate it after N seconds of
-# uptime, which is useful on long-running hosts.
-#
-# Usage:   mail-auth
-# Exit status: 0 if every needed cache is usable (fresh); 1 if one could not
-# be obtained (so the caller may skip that cycle and retry).
-
-set -eu
-
-RUNTIME="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
-CACHEDIR="$RUNTIME/mbsync"
-mkdir -p "$CACHEDIR"
-chmod 700 "$CACHEDIR"
-
-PASSAGE_TIMEOUT="${MBSYNC_PASSAGE_TIMEOUT:-30}"   # seconds you have to tap the key
-TTL="${MBSYNC_PW_TTL:-0}"                          # 0 => keep cache indefinitely
-
-log() { printf 'mail-auth: %s\n' "$*" >&2; }
-
-fetch() {
-    entry="$1"; out="$2"
-
-    # Still fresh?  Done.
-    if [ -f "$out" ]; then
-        if [ "$TTL" -gt 0 ]; then
-            now=$(date +%s); mtime=$(stat -c %Y "$out" 2>/dev/null || echo 0)
-            if [ $((now - mtime)) -lt "$TTL" ]; then
-                log "cache fresh ($entry)"
-                return 0
-            fi
-        else
-            log "cache present ($entry, TTL disabled)"
-            return 0
-        fi
-    fi
-
-    log "fetching $entry -- touch your passkey within ${PASSAGE_TIMEOUT}s"
-    if pw="$(timeout "$PASSAGE_TIMEOUT" passage show "$entry" 2>/dev/null | head -n1)" \
-       && [ -n "$pw" ]; then
-        umask 077
-        printf '%s\n' "$pw" > "$out"
-        log "cached $entry -> $out"
-        return 0
-    fi
-    log "FAILED to fetch $entry (passkey not touched? aborting login $entry)"
-    return 1
-}
-
-status=0
-fetch "piva/gmail"     "$CACHEDIR/gmail.pass"    || status=1
-fetch "fastmail/marcc" "$CACHEDIR/fastmail.pass" || status=1
-exit $status
-\ No newline at end of file
diff --git a/mail/mbsyncrc b/mail/mbsyncrc
@@ -1,91 +0,0 @@
-## mbsync (isync) configuration for two IMAP accounts.
-##
-## Install:
-##   ln -s /srv/src/emacs-patch-review/mail/mbsyncrc ~/.mbsyncrc
-##   mu init --maildir ~/Maildir                        # one-time
-##
-## Passwords are never written here.  Each PassCmd reads a one-line file
-## cached on tmpfs ($XDG_RUNTIME_DIR/mbsync/<acct>.pass) by the
-## `mail-auth` helper (see README.org); if the cache is missing it falls
-## back to `passage show`, which will ask you to touch your passkey.
-##
-## Local Maildir layout:
-##   ~/Maildir/gmail/...      (marc.coquand@piva.earth, Gmail)
-##   ~/Maildir/fastmail/...   (marc@coquand.email, Fastmail)
-
-# ----------------------------------------------------------------------------
-# Global
-# ----------------------------------------------------------------------------
-# Do not round-trip through the journal for every message (fine on a laptop
-# with a single user); comment out if you want crash-consistent fsyncs.
-FSync no
-
-# System trust store.  Adjust if your distro keeps it elsewhere.
-CertificateFile /etc/ssl/certs/ca-certificates.crt
-
-# ============================================================================
-# Gmail  —  marc.coquand@piva.earth
-# ============================================================================
-IMAPAccount piva
-Host imap.gmail.com
-Port 993
-User marc.coquand@piva.earth
-PassCmd "f=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/gmail.pass; [ -s $f ] && head -n1 $f || (passage show piva/gmail 2>/dev/null | head -n1)"
-SSLType IMAPS
-# Gmail app passwords only authenticate as LOGIN/PLAIN over TLS.
-AuthMechs LOGIN
-
-IMAPStore piva-remote
-Account piva
-
-MaildirStore piva-local
-Path ~/Maildir/gmail/
-Inbox ~/Maildir/gmail/INBOX
-# Keep Gmail's literal "/" hierarchy (so "[Gmail]/Sent Mail" nests).
-SubFolders Verbatim
-
-Channel piva
-Far :piva-remote:
-Near :piva-local:
-# Everything top-level (INBOX and any label folders Gmail exposes to IMAP),
-# minus the voluminous [Gmail]/* set, then re-add the three specials we want.
-# "[Gmail]/All Mail" is deliberately *not* synced (it duplicates everything
-# and bloats search with duplicate messages).
-Patterns * ![Gmail]* "[Gmail]/Sent Mail" "[Gmail]/Drafts" "[Gmail]/Trash"
-Create Both
-Expunge Both
-SyncState *
-
-# ============================================================================
-# Fastmail  —  marc@coquand.email  (login marcc@fastmail.fr)
-# ============================================================================
-IMAPAccount fastmail
-Host imap.fastmail.com
-Port 993
-User marcc@fastmail.fr
-PassCmd "f=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/fastmail.pass; [ -s $f ] && head -n1 $f || (passage show fastmail/marcc 2>/dev/null | head -n1)"
-SSLType IMAPS
-AuthMechs LOGIN
-
-IMAPStore fastmail-remote
-Account fastmail
-
-MaildirStore fastmail-local
-Path ~/Maildir/fastmail/
-Inbox ~/Maildir/fastmail/INBOX
-SubFolders Verbatim
-
-Channel fastmail
-Far :fastmail-remote:
-Near :fastmail-local:
-Patterns *
-Create Both
-Expunge Both
-SyncState *
-
-# ============================================================================
-# Groups
-# ============================================================================
-Group all
-Channel piva
-Channel fastmail
-\ No newline at end of file
diff --git a/mail/msmtprc.example b/mail/msmtprc.example
@@ -1,45 +0,0 @@
-## Example ~/.msmtprc -- one gmail account + one fastmail account.
-##
-## Install (backup first if you already have one):
-##   cp /srv/src/emacs-patch-review/mail/msmtprc.example ~/.msmtprc
-##   chmod 600 ~/.msmtprc
-##
-## Sending is account-selected by mu4e: each mu4e context sets
-## `message-sendmail-extra-arguments' to ("-a" "<account>"), and `message`
-## already has send-mail-function 'sendmail-send-it + sendmail-program
-## /usr/bin/msmtp.  So sending from either address Just Works.
-##
-## Passwords are reused from the same cached files `mail-auth` maintains,
-## so no extra passkey touch when composing.
-
-defaults
-auth on
-tls on
-tls_starttls off        # use implicit TLS on :465 for both servers
-#logfile ~/.cache/msmtp.log
-
-# ---------------------------------------------------------------------------
-# marc.coquand@piva.earth  (Gmail)
-# ---------------------------------------------------------------------------
-account piva
-host smtp.gmail.com
-port 465
-from marc.coquand@piva.earth
-user marc.coquand@piva.earth
-passwordeval "head -n1 ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/gmail.pass"
-
-# ---------------------------------------------------------------------------
-# marc@coquand.email  (Fastmail, IMAP login marcc@fastmail.fr)
-# ---------------------------------------------------------------------------
-account fastmail
-host smtp.fastmail.com
-port 465
-from marc@coquand.email
-# Fastmail's SMTP login is the mailbox's Fastmail username.
-user marcc@fastmail.fr
-passwordeval "head -n1 ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/fastmail.pass"
-
-# Make the first account the default (e.g. for `mail`/`sendmail` callers
-# that don't pass -a).  Override with `account default : fastmail` if you
-# prefer.
-account default : piva
-\ No newline at end of file
diff --git a/mail/mu4e-mail.el b/mail/mu4e-mail.el
@@ -1,140 +0,0 @@
-;;; mu4e-mail.el --- mu4e setup for two IMAP accounts -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2026 Marc Coquand
-
-;; 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.
-
-;;; Commentary:
-
-;; Loads alongside `patch-review-mu4e' and gives the patch-review mail
-;; workflow its plumbing: two mu4e contexts (Gmail / Fastmail), an mbsync
-;; config and a turnstile runit service that keeps ~/Maildir in sync.
-;;
-;; Loading this file requires the system-wide mu4e shipped at
-;; /usr/share/emacs/site-lisp/mu4e (already on `load-path' on Void).  It
-;; silently wires up `patch-review-mu4e-review' on `R' if the patch-review
-;; package is loadable from the directory above this file.
-;;
-;; See README.org for the system-side install (mbsyncrc, mail-auth,
-;; ~/.config/service/mbsync, mu init).
-
-;;; Code:
-
-(require 'mu4e)
-(require 'message)
-(require 'mu4e-message nil t)
-
-
-;;; --- patch-review integration -------------------------------------------------
-
-;; Put the repo root on `load-path' so `patch-review-mu4e' resolves even if
-;; the user only added this `mail/' subdirectory.
-(let* ((here (file-name-directory (or load-file-name buffer-file-name "/----")))
-       (root (file-name-directory (directory-file-name here))))
-  (when (file-exists-p (expand-file-name "patch-review-mu4e.el" root))
-    (add-to-list 'load-path root)))
-
-(when (require 'patch-review-mu4e nil t)
-  (with-eval-after-load 'mu4e-view
-    (define-key mu4e-view-mode-map (kbd "R") #'patch-review-mu4e-review)))
-
-
-;;; --- outbound mail ------------------------------------------------------------
-
-;; Sending goes through msmtp, one account per context (see msmtprc.example).
-;; These mirror what init.el's `message' use-package already sets; restating
-;; them here keeps mu4e self-contained if loaded on its own.
-(setq send-mail-function 'sendmail-send-it
-      sendmail-program "/usr/bin/msmtp"
-      message-sendmail-envelope-from 'header
-      mail-envelope-from 'header
-      mail-user-agent 'message-user-agent
-      message-user-agent 'message-user-agent
-      message-kill-buffer-on-exit t)
-
-
-;;; --- mu4e globals -------------------------------------------------------------
-
-(setq mu4e-change-filenames-when-moving t   ; recommended with mbsync
-      ;; Mail is fetched by the turnstile service; mu4e only re-indexes.
-      mu4e-get-mail-command "true"
-      mu4e-update-interval 300                ; re-index every 5 min when open
-      mu4e-index-cleanup nil
-      mu4e-completing-read-function #'completing-read-default
-      mu4e-attachment-dir (expand-file-name "~/Downloads")
-      message-kill-buffer-on-exit t)
-
-
-;;; --- contexts -----------------------------------------------------------------
-
-(defun mu4e-mail--maildir-prefix? (msg prefix)
-  "Return non-nil if MSG's :maildir starts with PREFIX."
-  (when-let* ((dir (and (functionp 'mu4e-message-field)
-                        (mu4e-message-field msg :maildir))))
-    (string-prefix-p prefix dir)))
-
-(setq mu4e-contexts
-      `(
-        ,(make-mu4e-context
-          :name "piva (Gmail)"
-          :match-func
-          (lambda (msg) (mu4e-mail--maildir-prefix? msg "/gmail"))
-          :vars '((user-mail-address . "marc.coquand@piva.earth")
-                  (user-full-name . "Marc Coquand")
-                  (mu4e-drafts-folder . "/gmail/[Gmail]/Drafts")
-                  (mu4e-sent-folder . "/gmail/[Gmail]/Sent Mail")
-                  (mu4e-trash-folder . "/gmail/[Gmail]/Trash")
-                  ;; A top-level Gmail "label" we create on first refile; on
-                  ;; Gmail that removes the INBOX label without data loss.
-                  (mu4e-refile-folder . "/gmail/Archive")
-                  (message-sendmail-extra-arguments . ("-a" "piva"))))
-
-        ,(make-mu4e-context
-          :name "fastmail"
-          :match-func
-          (lambda (msg) (mu4e-mail--maildir-prefix? msg "/fastmail"))
-          :vars '((user-mail-address . "marc@coquand.email")
-                  (user-full-name . "Marc Coquand")
-                  (mu4e-drafts-folder . "/fastmail/Drafts")
-                  (mu4e-sent-folder . "/fastmail/Sent")
-                  (mu4e-trash-folder . "/fastmail/Trash")
-                  (mu4e-refile-folder . "/fastmail/Archive")
-                  (message-sendmail-extra-arguments . ("-a" "fastmail"))))))
-
-;; Pick the right context automatically when replying; for a brand-new
-;; message that has no parent ("no current context yet"), mu4e prompts once.
-;; `user-mail-address'/`user-full-name' are themselves context vars below,
-;; so plain `message-mail' outside mu4e tracks the current context too.
-(setq mu4e-context-policy 'ask-if-none)
-
-
-;;; --- bookmarks & shortcuts ---------------------------------------------------
-
-(setq mu4e-bookmarks
-      '((:name "Unread"
-         :query "flag:unread AND NOT flag:trashed"
-         :key ?u :favorite t)
-        (:name "Today"
-         :query "date:today..now"
-         :key ?t)
-        (:name "Last 7 days"
-         :query "date:7d..now"
-         :key ?w)
-        (:name "Sent (gmail)"
-         :query "maildir:\"/gmail/[Gmail]/Sent Mail\""
-         :key ?G)
-        (:name "Sent (fastmail)"
-         :query "maildir:/fastmail/Sent"
-         :key ?S)))
-
-(setq mu4e-maildir-shortcuts
-      '((:maildir "/gmail/INBOX"      :key ?g :name "piva inbox")
-        (:maildir "/fastmail/INBOX"   :key ?f :name "fastmail inbox")
-        (:maildir "/gmail/Archive"   :key ?a :name "piva archive")
-        (:maildir "/fastmail/Archive" :key ?A :name "fastmail archive")))
-
-(provide 'mu4e-mail)
-;;; mu4e-mail.el ends here
-\ No newline at end of file
diff --git a/mail/service/conf b/mail/service/conf
@@ -1,20 +0,0 @@
-# Optional overrides for the mbsync run service.  Sourced by ./run at the
-# start of each (well, each) loop iteration.  Everything here is optional --
-# remove the file entirely and the defaults apply.
-#
-# Seconds between syncs.
-#MBSYNC_INTERVAL=300
-#
-# Where your Maildir lives.
-#MBSYNC_MAILDIR="$HOME/Maildir"
-#
-# mail-auth binary (defaults to ~/.local/bin/mail-auth).
-#MBSYNC_AUTH="$HOME/.local/bin/mail-auth"
-#
-# Per-run timeout (seconds).
-#MBSYNC_RUN_TIMEOUT=240
-#
-# Passed to mail-auth: how long you have to tap the passkey, and (if > 0) the
-# age in seconds after which a cached password is rotated.
-#MBSYNC_PASSAGE_TIMEOUT=30
-#MBSYNC_PW_TTL=0
-\ No newline at end of file
diff --git a/mail/service/log/run b/mail/service/log/run
@@ -1,10 +0,0 @@
-#!/bin/sh
-# runit logger for the mbsync service.  Captures ./run's stdout/stderr to a
-# rotating svlogd log.
-#
-# Install:  mkdir -p ~/.local/share/mbsync/log
-#           ln -s .../mail/service/log/run ~/.config/service/mbsync/log/run
-
-LOGDIR="${MBSYNC_LOGDIR:-$HOME/.local/share/mbsync/log}"
-mkdir -p "$LOGDIR"
-exec svlogd -tt "$LOGDIR"
-\ No newline at end of file
diff --git a/mail/service/run b/mail/service/run
@@ -1,45 +0,0 @@
-#!/bin/sh
-# turnstile per-user runit service: keep ~/Maildir synced with IMAP.
-#
-# Install (see ../README.org):
-#   mkdir -p ~/.config/service/mbsync/log
-#   ln -s /srv/src/emacs-patch-review/mail/service/run   ~/.config/service/mbsync/run
-#   ln -s /srv/src/emacs-patch-review/mail/service/conf  ~/.config/service/mbsync/conf
-#   ln -s /srv/src/emacs-patch-review/mail/service/log/run ~/.config/service/mbsync/log/run
-#
-# turnstiled already exports XDG_RUNTIME_DIR into this environment, so
-# `mail-auth` and mbsync's PassCmd both find the cached passwords.
-#
-# Optional overrides via ./conf:  MBSYNC_INTERVAL, MBSYNC_MAILDIR etc.
-
-[ -r ./conf ] && . ./conf
-
-INTERVAL="${MBSYNC_INTERVAL:-300}"            # seconds between syncs
-MAILDIR="${MBSYNC_MAILDIR:-$HOME/Maildir}"
-AUTH="${MBSYNC_AUTH:-$HOME/.local/bin/mail-auth}"
-
-log() { echo "[mbsync $(date '+%FT%T%z')] $*"; }
-
-log "starting; interval=${INTERVAL}s maildir=${MAILDIR}"
-
-while :; do
-    if [ -x "$AUTH" ]; then
-        "$AUTH" || log "auth refresh incomplete; will retry next cycle"
-    else
-        log "warning: $AUTH not found/executable -- passwords must already be cached"
-    fi
-
-    if [ -d "$MAILDIR" ]; then
-        log "syncing"
-        timeout "${MBSYNC_RUN_TIMEOUT:-240}" mbsync -a -q || \
-            log "mbsync exited $?"
-        log "sync complete"
-        # Indexing is left to mu4e (`mu4e-update-interval'), which only runs
-        # while mu4e is open -- running `mu index' here too would contend
-        # with it for mu's lock.
-    else
-        log "maildir $MAILDIR missing; run 'mu init --maildir $MAILDIR'? waiting"
-    fi
-
-    sleep "$INTERVAL"
-done
-\ No newline at end of file
diff --git a/patch-review-reply.el b/patch-review-reply.el
@@ -76,7 +76,6 @@ Return the reply body, or \"\" when there are no comments."
                                   (patch-review-file-new-path file))
                           out)
                     (setq last-file file-index))
-                  (push (concat "> " (patch-review-hunk-header hunk)) out)
                   ;; Quote from a little context above the first commented
                   ;; line through the last commented line, inserting
                   ;; comments after their targets.
diff --git a/patch-review.el b/patch-review.el
@@ -111,6 +111,17 @@ insertions are sent as an interleaved review reply.
 
 \\{patch-review-mode-map}"
   (setq buffer-read-only nil)
+  ;; `diff-mode' enables `diff-mode-shared-map' (RET→diff-goto-source,
+  ;; digits→digit-argument, special-mode's [remap self-insert-command
+  ;; → undefined], …) whenever the buffer was read-only when the
+  ;; parent body ran, by latching the buffer-local `diff-mode-read-only'
+  ;; to read-only state.  A review buffer taken over in place from a
+  ;; read-only mu4e article starts that way, so diff-mode latches the
+  ;; short keys before we flip `buffer-read-only' back to nil; doing
+  ;; so would otherwise make the buffer uneditable and turn RET into
+  ;; the "Use file …: " prompt from `diff-goto-source'.  Drop the
+  ;; latch so the shared diff keymap never activates here.
+  (setq diff-mode-read-only nil)
   (setq-local header-line-format '(:eval (patch-review--header-line)))
   (patch-review--ensure-initialized))
 
@@ -443,17 +454,11 @@ inserted anywhere else is a comment on the patch line above it."
   (interactive nil patch-review-mode)
   (unless patch-review--parsed
     (user-error "Not a patch review buffer"))
-  (pcase-let ((`(,comments ,deletions ,changes)
-               (patch-review-extract-comments
-                patch-review--parsed
-                patch-review--pristine
-                (buffer-substring-no-properties (point-min) (point-max)))))
-    (when (or (> deletions 0) (> changes 0))
-      (unless (y-or-n-p
-               (format "Your edits rewrote or deleted %d patch line%s (quoted from the original).  Send anyway? "
-                       (+ deletions changes)
-                       (if (= 1 (+ deletions changes)) "" "s")))
-        (user-error "Aborted")))
+  (let ((comments (car (patch-review-extract-comments
+                         patch-review--parsed
+                         patch-review--pristine
+                         (buffer-substring-no-properties
+                          (point-min) (point-max))))))
     (unless comments
       (when (y-or-n-p "No inline comments found; add a general comment? ")
         (let ((text (read-string "General comment: ")))
diff --git a/tests/patch-review-test.el b/tests/patch-review-test.el
@@ -144,8 +144,10 @@
     (should (string-prefix-p "Looks good overall" reply))
     (should (string-match-p
              (regexp-quote "> diff --git a/src/socket.c b/src/socket.c") reply))
-    (should (string-match-p
-             (regexp-quote (concat "> " (patch-review-hunk-header hunk))) reply))
+    ;; The hunk header itself is not quoted; only hunk lines are.
+    (should-not (string-match-p
+                 (regexp-quote (concat "> " (patch-review-hunk-header hunk)))
+                 reply))
     (let ((quoted (string-match (regexp-quote "> +#include <errno.h>") reply))
           (comment (string-match (regexp-quote "Is errno.h needed") reply))
           (second (string-match (regexp-quote "EBADF or EINVAL") reply)))
@@ -482,6 +484,53 @@ starting with PREFIX."
             (should (file-equal-p patch-review--source-file eml))))
       (when (buffer-live-p view) (kill-buffer view)))))
 
+(ert-deftest patch-review-test-in-place-editable-from-readonly ()
+  "An in-place review taken over from a read-only special-mode buffer
+(like a mu4e article) must be fully editable.
+
+`diff-mode' latches `diff-mode-read-only' to the read-only state its
+parent body sees; a buffer that started read-only would otherwise keep
+`diff-mode-shared-map' active, so RET became `diff-goto-source' (the
+read-file-name prompt from diff-find-file-name), digits hit
+digit-argument, and the special-mode remap of self-insert-command to
+undefined made every printable key a no-op.  We clear the latch in
+`patch-review-mode' so the review buffer behaves like a normal editable
+buffer."
+  (let* ((eml (expand-file-name
+               "fixtures/patch1.eml"
+               (file-name-directory (or load-file-name buffer-file-name))))
+         (text (with-temp-buffer
+                 (insert-file-contents eml)
+                 (buffer-string)))
+         (view (get-buffer-create " *patch-review-fake-ro-view* ")))
+    (unwind-protect
+        (progn
+          ;; Start read-only, the way a mu4e article buffer is.
+          (with-current-buffer view
+            (special-mode)
+            (let ((inhibit-read-only t)) (erase-buffer) (insert "stale"))
+            (should (eq buffer-read-only t)))
+          (let ((returned
+                 (with-current-buffer view
+                   (patch-review--open-text text eml nil t))))
+            (should (eq returned view))
+            (with-current-buffer view
+              (should (eq major-mode 'patch-review-mode))
+              (should (null buffer-read-only))
+              ;; The diff-mode short-key latch must be cleared, and with
+              ;; it gone the inherited diff/special bindings don't hijack
+              ;; ordinary editing keys.
+              (should (null diff-mode-read-only))
+              (should (eq (key-binding (kbd "RET")) #'newline))
+              (should (eq (key-binding "1") #'self-insert-command))
+              ;; Direct insertion succeeds (no read-only short-circuit).
+              (let ((before (buffer-size)))
+                (goto-char (point-max))
+                (insert "new comment\n")
+                (should (= (- (buffer-size) before)
+                           (length "new comment\n")))))))
+      (when (buffer-live-p view) (kill-buffer view)))))
+
 ;;;; End-to-end: status probe and apply
 
 (ert-deftest patch-review-test-status-and-apply ()