From 5570d0e7495a42a90acf951d396c9185b0319eb9 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 27 Aug 2026 13:05:17 +0200 Subject: feat: forward an HTML message with its formatting Item 171. A forward carried only the plain-text version of the original, so formatting was lost; and an original with no plain-text part at all (30 of 342 sampled inbox messages, ~9%) forwarded as an empty quote with its content silently gone. A forward now sends ONE part chosen by the Send-as-HTML toggle: the original's markup when on, the text quote when off. Not a multipart/alternative, at the user's decision: a forward's shape is already decided by that toggle, and sending both hands the choice to the recipient's client. The toggle is honoured even for an HTML-only original, which then forwards as a text fallback. HtmlSanitiser strips remote content from the forwarded markup, checked by default with a per-forward opt-out. This is the security-critical part: the markup leaves this process and is rendered by the recipient's client, where none of MessageView's protections apply, so forwarding a tracking pixel forwards the tracking. It is an ALLOW-LIST, unlike HtmlBuilder::namespaceCids(), because a missed rewrite is a broken image while a missed strip is a beacon reaching the recipient. An HTML forward does not seed a text quote into the editor. The first build did, then subtracted it when building the HTML part, so the user could edit a quote whose edits were discarded; what the composer shows must be what gets sent. The forwarded message appears in a read-only pane beside the editor instead, a QSplitter at 60/40 with a toggle in the Format menu. A plain forward is unchanged. ComposeContextBuilder::quoteBody() renders htmlBody down to text when there is no plain part, so the plain path never emits an empty quote. Design in docs/superpowers/specs/2026-08-27-forward-html-design.md. Two tests repaired for the splitter: the 60/40 assertion reads stretch factors rather than pixels, since the offscreen platform gives the splitter no width and reports 49/49 whatever the code asks; and theComposerSplitsItsToolbarByScope looked for the body directly in the composer's column. Not yet hand-tested in this arrangement. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AtUzfNjMD8fiYfamDd3ywW --- src/composewindow.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'src/composewindow.h') diff --git a/src/composewindow.h b/src/composewindow.h index c8cc12a..9affce1 100644 --- a/src/composewindow.h +++ b/src/composewindow.h @@ -34,6 +34,7 @@ class QAction; class QCheckBox; +class QSplitter; class QComboBox; class QLabel; class QLineEdit; @@ -223,6 +224,15 @@ private: /// MessageBuilder refuses a build naming any path that later vanishes, so /// a silently wrong send is not among the outcomes. void extractForwardedAttachments(); + + /// Reads the forwarded original's HTML, before the body is seeded. + void readForwardedHtml(); + + /// Creates the strip-remote-content checkbox, for a forward that needs it. + void buildStripRemoteControl(); + + /// Creates the read-only preview of what an HTML forward will carry. + void buildForwardPreview(); void seedBody(); /// Applies \p name to the buffer, replacing whatever is there. @@ -300,6 +310,27 @@ private: QComboBox *m_from = nullptr; QPlainTextEdit *m_body = nullptr; QToolButton *m_sendHtml = nullptr; + + /// Item 171. Strips remote content from the forwarded original, checked by + /// default. Only created for a Forward whose original carries remote + /// content, so an ordinary message gains no control. + QCheckBox *m_stripRemote = nullptr; + + /// Read-only view of the original an HTML forward will carry. Item 171: + /// the buffer holds the user's note only, so this is what makes the rest + /// of the message visible without pretending it can be edited. + QWidget *m_forwardPreview = nullptr; + + /// Holds the editor, and the forward preview beside it when there is one. + QSplitter *m_split = nullptr; + + /// Shows and hides the forwarded-message pane. Only for a forward. + QAction *m_showForwardAction = nullptr; + + /// The forwarded original's HTML, as read from `originalPath` at + /// construction. Held raw; the stripping happens at currentMessage(), + /// so toggling the control does not need a re-parse. + QString m_forwardedHtmlRaw; QToolButton *m_signatureSwitch = nullptr; QString m_signatureDir; QString m_signatureName; ///< The selected signature, empty for None. -- cgit v1.2.3