aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-20-compose-and-send.md
AgeCommit message (Collapse)AuthorFilesLines
4 daysfeat(compose): hand outgoing mail to the send command, item 123Danilo M.1-0/+28
MessageSender runs the configured command with the message on stdin and judges the result by its exit status alone. Nothing here waits on the event loop, so a send does not block the GUI thread; a 1.6MB payload was probed through a reading stub without deadlocking the pipe buffer. The command is split and passed to QProcess as a program and an argument list, never through a shell. A test asserts that by giving the command shell metacharacters and checking that the marker file a shell would have created does not exist, so the property fails a mutation rather than resting on a comment. Four corrections to the plan's draft. splitCommand handles double quotes only, so a single-quoted argument splits wrongly and the header now says so. A crashing command delivers finished(11, CrashExit) and would have been reported as "exited with status 11", so a crash branch was added. A command that exits without draining a large stdin emits WriteError before finished(), which the draft handled correctly and by luck, untested. And an empty send_command is checked after trimming. Two contract gaps found in review, both about what this class promises rather than what it does. The exactly-once guarantee covers the EMIT, not what a caller receives: a long-lived sender plus a connect() inside each send accumulates receivers, and the second result then runs the first send's lambda too, filing a sent copy of the wrong message. The header now scopes the promise and requires Qt::SingleShotConnection. The plan's Task 11 call site already had that flag, sixty-nine lines below the connect and outside anything a reader would see, so the plan gained a note where someone retyping it will read it. And destruction mid-send killed the command with no report, announced only by a Qt warning: a live SMTP conversation abandoned, possibly partially delivered, while the user believes it was cancelled. The destructor now closes stdin, waits a bounded five seconds, and only then kills. It emits nothing either way, because the outcome after a kill is genuinely unknown and reporting "not sent" for a message that may have gone out is the mailsync.sh mistake pointing the other way. Claiming m_reported before kill() is what makes that true, since kill() delivers finished(CrashExit), which would otherwise emit exactly that untruth. No timeout on the send itself: killing a slow but working send is worse than waiting. Task 10 owns the popup, and deliberately offers no cancel after commit, so this class promises none either. Also refreshes the translations Task 5 left out. That gap was invisible because test_translations builds its rows from the .ts file, so a string that never entered it is never asserted on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QP2g3b3kuLx6AYFCNEz6UR
5 daysdocs: record that build() blocks the caller, item 123Danilo M.1-0/+10
Task 4's code review measured it: a large attachment is read and base64 encoded on the calling thread, and autosave calls build() from a GUI-thread timer. The directory hang that review found is fixed, but the blocking read is by design and will be felt in the composer. Recorded against Task 11 rather than fixed, because nothing in the composer crosses the worker boundary and adding a second threading model for one call is worse than the stall.
5 daysdocs: document the new config keys in the plan's close-out, item 123Danilo M.1-0/+15
Task 2's code review found that the README's sample config documents every other key, including recently added ones, and has nothing for send_command or the [compose] section. Without it those keys ship undiscoverable: a user has no way to learn that sending exists at all. That is a gap in the plan rather than a deviation by the task, since no task claimed the README, so it becomes a step in the close-out where the rest of the documentation is written.
5 daysdocs: implementation plan for compose and send, item 123Danilo M.1-0/+4923
Thirteen tasks, ninety-nine steps, against the spec committed earlier on this branch. Written on master so it is readable from either branch; the implementation goes on compose-and-send. Every API assumption was verified against this machine rather than written from memory, which found five things the spec had wrong or unstated: libcmark-gfm-extensions ships NO pkg-config file although libcmark-gfm does, so CMake needs find_library beside pkg_check_modules. All three enabled extensions live in that second library, so finding only the first yields a build that compiles and silently renders plain CommonMark. GMime defaults to iso-8859-1, emits no Date or Message-ID unless asked, and g_mime_text_part_set_text() encodes with whatever charset is set when it is called, so setting the charset afterwards produces a part labelled utf-8 carrying latin-1 bytes. All three fail only on accented text, which for this user is every message. The plan builds the content stream directly and carries a working probe's output as evidence. MessageNode has no body or date field, so quoting takes a ParsedMessage. ThreadListModel::messageScopeFor() takes a QModelIndexList rather than a single index. There is no Config::maildirPath(): the mail root comes from notmuch_config_get(NOTMUCH_CONFIG_MAIL_ROOT) via a file-static helper in the worker, and item 124 records that composing a destination from the wrong root would write into the Xapian tree. Two spec statements are corrected in the plan rather than followed. It calls for a new top-level Message menu and one already exists at mainwindow.cpp:1156. And it requires a shortcut per action, which item 132 changed while this was being planned, so save_message ships without one.