aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-23 21:15:13 +0200
committerDanilo M. <danix@danix.xyz>2026-08-23 21:15:13 +0200
commitfabcf080652c6e5d57bf234be5e100769a9b965b (patch)
tree0de4222c1e2aab58c38d34c9e0e3c37c68298cc8 /docs/superpowers/specs
parentc50bea78e036518ce1a2a3eb899bbb5e305affea (diff)
parentddcae8d02ef46db522b3cf6c228196c7a66a6432 (diff)
downloadqtmaildir-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.md39
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