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/plans/2026-08-20-compose-and-send.md | |
| 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/plans/2026-08-20-compose-and-send.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-20-compose-and-send.md | 53 |
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: |
