aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-20-compose-and-send.md
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/plans/2026-08-20-compose-and-send.md
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/plans/2026-08-20-compose-and-send.md')
-rw-r--r--docs/superpowers/plans/2026-08-20-compose-and-send.md53
1 files changed, 53 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-20-compose-and-send.md b/docs/superpowers/plans/2026-08-20-compose-and-send.md
index 195dacc..7d5f6f1 100644
--- a/docs/superpowers/plans/2026-08-20-compose-and-send.md
+++ b/docs/superpowers/plans/2026-08-20-compose-and-send.md
@@ -3871,6 +3871,16 @@ popup between stages."
### Task 11: ComposeWindow
+**Found during Task 4's code review, and it lands here.** `MessageBuilder::build()`
+is SYNCHRONOUS and can block: a large attachment is read and base64-encoded on
+the calling thread. Autosave calls it on a timer, on the GUI thread, so a
+30-second debounce that hits a 25MB attachment stalls typing. The directory
+hang that review found is fixed in `MessageBuilder`, but the blocking read
+remains by design. Do not move it to a thread as part of this task, since
+nothing here crosses the worker boundary and adding a second threading model
+for one call is worse than the stall. Note it in a comment at the autosave call
+site so the next person measuring a freeze knows where to look.
+
The only unit here that owns widgets, and the one that composes the other four.
It contains no MIME and no process logic: a composer bug and a MIME bug are
found in different files.
@@ -3998,6 +4008,20 @@ private:
`src/composewindow.cpp`. The full file is long; these are the parts that carry
decisions, and the rest is ordinary widget assembly.
+**One thing in this block is load-bearing and easy to drop while retyping it:
+the `Qt::SingleShotConnection` on the `MessageSender::finished` connect inside
+the `committed` handler.** `m_sender` is a long-lived member, so a plain
+`connect()` beside a `send()` call leaks a receiver per send and the second
+result runs every earlier lambda, each still holding an earlier message's bytes
+by value: a sent copy of the wrong message, and `accept()` on a destroyed
+dialog. `MessageSender`'s own once-only guard cannot help, because that guards
+the emit and this is one emit reaching many receivers. The header for
+`MessageSender::finished` states the rule and
+`test_messagesender.cpp::aPerSendConnectionMustBeSingleShot` measures it (3
+deliveries for 2 sends without the flag, 2 with it). Noted here because the
+plan's code blocks are drafts and this is the line whose absence still
+compiles, still runs, and is wrong only on the second send.
+
```cpp
#include "composewindow.h"
@@ -4159,6 +4183,20 @@ void ComposeWindow::send()
connect(dialog, &SendDialog::committed, this, [this, dialog, built, account]() {
m_sender->send(account.sendCommand, built.bytes);
+ // Qt::SingleShotConnection IS REQUIRED HERE, and this line is the
+ // correction of a defect that was in this plan's draft (found while
+ // building Task 6, 2026-08-21). m_sender is a long-lived member, so a
+ // bare connect() beside each send() accumulates a permanent receiver
+ // per send. Send, fail, correct the recipient, send again, and the
+ // second result runs BOTH lambdas: the first still holds the FIRST
+ // message's `built` and `account` by value, so it files a sent copy of
+ // the wrong message and calls accept() on a dialog it already
+ // deleteLater()'d. MessageSender's m_reported guard cannot prevent
+ // this: it collapses two QProcess signals into one emit, and this is
+ // one emit reaching many receivers. Measured in
+ // test_messagesender.cpp::aPerSendConnectionMustBeSingleShot, where
+ // the bare shape delivers 3 results for 2 sends and the single-shot
+ // shape delivers 2.
connect(m_sender, &MessageSender::finished, this,
[this, dialog, built, account](bool sent, const QString &error) {
if (!sent) {
@@ -4787,6 +4825,21 @@ replace.
- Modify: `docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md`
- Modify: `docs/superpowers/specs/2026-08-20-compose-and-send-design.md`
+- [ ] **Step 0: Document the new keys in the README**
+
+Found during Task 2's code review and assigned here rather than there. The
+README's sample config at `README.md:150-215` documents EVERY other
+configuration key, including recently added ones, and has nothing for
+`send_command` or the `[compose]` section. Without this the keys ship
+undiscoverable: a user has no way to learn that sending exists.
+
+Take the block from the spec at
+`docs/superpowers/specs/2026-08-20-compose-and-send-design.md:552-560` and
+adapt it to the README's existing commented style, showing `send_command` in
+an account section and every `[compose]` key with its default. Say plainly
+that an account without `send_command` is receive-only, since that is the
+part no reader would guess.
+
- [ ] **Step 1: Add the changelog entry**
Under `## [Unreleased]`, in the existing `### Added` section or a new one: