aboutsummaryrefslogtreecommitdiffstats
path: root/src/composewindow.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-22 11:19:22 +0200
committerDanilo M. <danix@danix.xyz>2026-08-22 12:00:32 +0200
commita9d1cf73a91b5eef79a9722ec9921c97b9ec5c81 (patch)
tree91f45693edc9098dfd9c35d17eedd1ff8ab7066e /src/composewindow.h
parent141bc9b98213b9c4ba9f3bcba041c3e3108c12a3 (diff)
downloadqtmaildir-a9d1cf73a91b5eef79a9722ec9921c97b9ec5c81.tar.gz
qtmaildir-a9d1cf73a91b5eef79a9722ec9921c97b9ec5c81.zip
feat(compose): wire the composer into the main window, item 123compose-and-send
The reply family is disabled on mail that arrived at an account with no send_command, behind a ribbon in MessageView naming the account and the key to add. save_message is deliberately never disabled: it is the escape hatch for exactly that case. The ribbon is a WIDGET in the pane's layout, never markup inside the web view. Composing HTML from configuration into the one document that renders input from strangers is the wrong direction, and the header row is already a widget for the same reason. Compose itself is disabled only when NO account can send, and that state is not warned about at startup: an installation with no send_command anywhere is a valid read-only installation. Every reply resolves through messageScopeFor(), not threadFor(): a thread row means the one message its card shows. Replying to a thread is meaningless; a reply answers a message. The context is built from the DATABASE rather than the model, the rule Restore already follows, because a row whose state has not been re-queried carries stale values and a reply built from one would carry the wrong recipients. The mail root crosses from the worker as its own signal. There was no route for it at all: mailRootOf() is file-static in notmuchworker.cpp, and item 124 records that composing a destination from database.path writes into the Xapian tree under a split index. The test uses NotmuchFixture::splitIndex(), the only layout where the two accessors disagree. A thread row's path is RELATIVE to the mail root while a message row's is absolute, so the account lookup matched nothing and the reply family was dead on mail from an account that could send. Found by the positive guard test rather than the negative one, which passed throughout for the wrong reason. The quit path checks the failed-save case FIRST. In the ordinary case nothing is lost by saving; there, saving is what is already not working, so the dialog says plainly that quitting loses that text rather than offering a save that will fail again. Both dialogs name the composers, and the ordinary one asks once whatever the count, because three modals in a row is worse than a coarse answer. Its wording says drafts already saved stay in the folder, so Discard cannot read as 'delete my three messages'. The Save loop holds QPointers, not raw pointers. A deleteLater() posted while a nested exec() runs IS processed by that nested loop, measured in a standalone program: the guard nulls before the modal returns. Closing a composer while the quit dialog is up therefore freed a window the loop then called saveDraftNow() on, crashing at the exact moment the application promised to preserve that text. A compose request that matches nothing clears itself and says so. It was cleared only on a match, so a message deleted between selection and Reply left the request armed for the session: Reply did nothing, and the next ordinary click on that message opened a composer nobody asked for while the pane stayed blank. Forward carries the original's attachments, which the context has always had a field for and nothing ever filled, and seeds its HTML toggle from [compose] send_html. Only Reply seeds that from the original. save_message keeps its filename inside the chosen directory and no longer overwrites a file already there. The check was correct and untested: the test asserted through Attachment's helpers rather than through the function production calls, so deleting the containment check outright left it green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvwDptMWxjqhbCmjxwcSZ2
Diffstat (limited to 'src/composewindow.h')
-rw-r--r--src/composewindow.h44
1 files changed, 44 insertions, 0 deletions
diff --git a/src/composewindow.h b/src/composewindow.h
index 99803d7..af50be6 100644
--- a/src/composewindow.h
+++ b/src/composewindow.h
@@ -21,6 +21,8 @@
#include <QMainWindow>
#include <QStringList>
+#include <memory>
+
#include "config.h"
#include "formattoolbar.h" // MarkdownFormat::Edit is used by value below, and
// a type nested in a namespace cannot be
@@ -36,6 +38,7 @@ class QListWidget;
class QPlainTextEdit;
class QTimer;
class QToolBar;
+class QTemporaryDir;
class QWidget;
class MessageSender;
@@ -74,6 +77,12 @@ public:
ComposeWindow(const ComposeContext &context, const Config &config,
const QString &mailRoot, QWidget *parent = nullptr);
+ /// Defined in the .cpp, not defaulted here. m_forwardedParts is a
+ /// unique_ptr to a forward-declared QTemporaryDir, whose deleter needs the
+ /// complete type; an implicit destructor would be generated here, where it
+ /// is still incomplete.
+ ~ComposeWindow() override;
+
/// True when the buffer has changed since the last successful autosave.
/// The quit path asks every open composer this.
bool hasUnsavedEdits() const { return m_dirty; }
@@ -139,6 +148,20 @@ private:
void buildUi();
void buildFormatToolbar();
void seedFields();
+
+ /// Extracts a forwarded message's parts into m_forwardedParts and appends
+ /// their paths to m_attachments.
+ ///
+ /// The spec requires Forward to carry attachments, and they have to become
+ /// FILES because MessageBuilder reads every attachment by path. Extraction
+ /// happens here rather than in MainWindow so the files and the directory
+ /// that owns them are created together and die together.
+ ///
+ /// A part that cannot be written is SKIPPED with a banner rather than
+ /// failing the forward: some of the attachments is better than none, and
+ /// MessageBuilder refuses a build naming any path that later vanishes, so
+ /// a silently wrong send is not among the outcomes.
+ void extractForwardedAttachments();
void seedBody();
void refreshAttachmentList();
void setInputsEnabled(bool enabled);
@@ -155,6 +178,27 @@ private:
QString m_mailRoot;
QStringList m_attachments;
+ /// Holds the parts a Forward extracted, for exactly as long as this window.
+ ///
+ /// Owned HERE rather than by MainWindow, because the lifetime that makes
+ /// sense is the composer's: MessageBuilder reads every attachment by PATH
+ /// at build time (messagebuilder.cpp:212), on each autosave and again at
+ /// send, so the files must outlive every build this window performs and
+ /// nothing after it. QTemporaryDir's destructor removes the tree, so
+ /// closing without sending cleans up rather than leaking.
+ ///
+ /// A draft does not depend on it. Autosave writes a COMPLETE MIME message
+ /// with the bytes embedded, so a saved draft stays valid after these files
+ /// are gone; and DraftStore is write-only, with no reopen path anywhere in
+ /// this codebase, so the "reopened next session pointing at a dead temp
+ /// path" hazard cannot arise. Should a reopen path ever be added, it must
+ /// read attachments back out of the draft's own MIME rather than trusting
+ /// a stored path.
+ ///
+ /// Null unless a Forward actually extracted something. unique_ptr because
+ /// QTemporaryDir is neither copyable nor movable.
+ std::unique_ptr<QTemporaryDir> m_forwardedParts;
+
QLineEdit *m_to = nullptr;
QLineEdit *m_cc = nullptr;
QLineEdit *m_bcc = nullptr;