diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-23 21:15:13 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-23 21:15:13 +0200 |
| commit | fabcf080652c6e5d57bf234be5e100769a9b965b (patch) | |
| tree | 0de4222c1e2aab58c38d34c9e0e3c37c68298cc8 /docs/superpowers/specs | |
| parent | c50bea78e036518ce1a2a3eb899bbb5e305affea (diff) | |
| parent | ddcae8d02ef46db522b3cf6c228196c7a66a6432 (diff) | |
| download | qtmaildir-fabcf080652c6e5d57bf234be5e100769a9b965b.tar.gz qtmaildir-fabcf080652c6e5d57bf234be5e100769a9b965b.zip | |
Merge branch 'compose-and-send': composing and sending mail
Item 123, built over 2026-08-20 to 2026-08-23 in thirteen tasks against
docs/superpowers/specs/2026-08-20-compose-and-send-design.md.
The application writes mail now. A composer window per message, markdown as
the body, drafts autosaving into the account's Maildir, and sending through a
per-account command on stdin rather than any network protocol of this
program's own. A countdown with an Undo stands between pressing Send and the
command running.
Two things came in alongside it. The notmuch auto-tagging hooks moved here
from the retiring `mailctl` project and learned that mail this application
files itself never arrived, so sent mail and drafts stop appearing in the
inbox. And the v1/v2 language is retired: semver on the user-visible surface
is the rule, and those labels described a split that composing made obsolete.
Hand tested against a fake send command rather than a real one, deliberately:
New, Reply and Forward all produce correct messages, a forwarded attachment
survives intact, and the sent copy is filed. That testing found the two
defects fixed on this branch, and both were invisible to the suite: a composer
orphaned by quitting the main window, and every sent message tagged `inbox`.
Twenty-two defects were found in the plan document's own draft code while
building it, which is why CLAUDE.md says to treat every code block in a plan
as a draft.
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-08-20-compose-and-send-design.md | 39 |
1 files changed, 35 insertions, 4 deletions
diff --git a/docs/superpowers/specs/2026-08-20-compose-and-send-design.md b/docs/superpowers/specs/2026-08-20-compose-and-send-design.md index aade2d1..9533602 100644 --- a/docs/superpowers/specs/2026-08-20-compose-and-send-design.md +++ b/docs/superpowers/specs/2026-08-20-compose-and-send-design.md @@ -430,7 +430,7 @@ Two structs cross boundaries, in `types.h` beside the existing ones. | `originalPath` | the `.eml` being replied to or forwarded; empty for New | | `inReplyTo` | Message-ID of the original | | `references` | the original's References plus its Message-ID | -| `to`, `cc` | pre-filled recipients, the user's own addresses already stripped | +| `to`, `cc` | pre-filled recipients, the user's own addresses already stripped; a reply to the user's OWN message is addressed to that message's recipients instead of back to the user, mirroring its To/Cc split (see Replying to oneself) | | `subject` | `Re:` / `Fwd:` prefixed, an existing prefix not doubled | | `quotedBody` | the `>`-prefixed original; empty when the action does not quote | | `seedHtml` | did the original carry a `text/html` part | @@ -447,6 +447,13 @@ Two structs cross boundaries, in `types.h` beside the existing ones. | `attachments` | local paths | | `inReplyTo`, `references` | carried through unchanged | +Message-ids are carried BARE, without angle brackets, matching what GMime hands +back when `MimeParser` reads a `Message-ID`. `MessageBuilder` adds the brackets +when it writes the header, in one place rather than in each caller: they are wire +syntax, and GMime writes an EMPTY header for a bare addr-spec rather than +complaining, so a caller that forgets them ships a reply that threads nowhere +while nothing looks wrong locally. + `In-Reply-To` and `References` are not optional. Without them a reply appears as an orphan thread in the sender's own client. @@ -505,15 +512,38 @@ Six, each needing the five places `CLAUDE.md` enumerates: `knownActions()`, | Action | Meaning | Scope | |---|---|---| | `compose` | New message | none needed | -| `reply` | Reply to the displayed message, quoted | sender only | +| `reply` | Reply to the displayed message, quoted | sender only, except when the sender is the user (see below) | | `reply_all` | Reply to all, quoted | sender + To + Cc, own addresses removed | -| `reply_no_quote` | Reply with an empty body | sender only | +| `reply_no_quote` | Reply with an empty body | sender only, same exception | | `forward` | Forward, body quoted inline, attachments carried | none | | `save_message` | Write the raw `.eml` to a chosen path | any message | `reply_all_no_quote` is deliberately absent. Six actions is already a large menu and the combination is reached by deleting the quote. +### Replying to oneself + +A reply whose sender is entirely the user's own addresses is addressed to that +message's **original recipients** rather than to the sender. A plain reply takes +its To and Cc together, having no Cc field of its own to mirror into. A +reply-all MIRRORS THE SPLIT: the original's To becomes To and its Cc becomes Cc, +because To means "addressed to you" and Cc "for information", and promoting a +Cc'd party to To is a change every recipient can see. +This is an ordinary gesture rather than an edge case: it is reached from the +Sent view, from a follow-up on mail that went unanswered, and from any thread +whose selected row is the user's own message. Addressing the sender there +addresses the user, so the reply reaches nobody it was meant for. + +"Own" means EVERY parsed sender address is the user's. A message the user sent +together with somebody else is still a reply to that co-sender, and takes the +ordinary sender-only path. + +Mail the user sent to THEMSELVES alone leaves nothing after own addresses are +removed, and there the sender is restored: the user is the correct recipient of +their own note. The rejected alternative was to strip the sender and leave To +empty, which silently drops every recipient while the message still looks +sendable. + **Every action acts on the displayed message**, resolved with `messageScopeFor()` semantics: a thread row means the one message its card shows, a reply row means itself. Not `threadFor()`. Replying to a thread is @@ -660,7 +690,8 @@ Cases: `multipart/alternative` when `sendHtml` is on and `text/plain` alone when off; `multipart/mixed` nesting with attachments; each enabled extension rendering, and tables and raw HTML **not** rendering; RFC 2047 encoding of a non-ASCII subject and display name; quoted-printable for an accented body; -`In-Reply-To` and `References` carried; `Re:` and `Fwd:` not doubling. +`In-Reply-To` and `References` carried; `Re:` and `Fwd:` not doubling, in the +non-English spellings and counted forms as well as the English ones. **`test_messagesender`** uses stub commands, not msmtp: one exiting 0, one exiting non-zero with stderr, one that does not exist. The stub writes stdin to |
