aboutsummaryrefslogtreecommitdiffstats
path: root/src/messageview.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 09:09:43 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:52:46 +0200
commit2caf15d2d032ec6fff0ca732f6ba4759ee91685d (patch)
tree4df9f0aa02b95c22851c1e8580e41a43d948aec5 /src/messageview.h
parente50c76cd786e474daf5ab883b28c4b6067a918d3 (diff)
downloadqtmaildir-2caf15d2d032ec6fff0ca732f6ba4759ee91685d.tar.gz
qtmaildir-2caf15d2d032ec6fff0ca732f6ba4759ee91685d.zip
feat: add MessageView with locked-down web engine profile
Off-the-record profile, JavaScript off, deny-by-default interceptor, and a page subclass that hands link clicks to the system browser so a message can never navigate the pane. Honours the obligation task 5 recorded: the interceptor trusts exactly one qtmaildir: URL and fails closed otherwise, so setHtml() and setDocumentUrl() must agree or the pane renders nothing. Rather than pairing those calls at each site, every load goes through one setDocument() and the URL comes from a single documentUrl() accessor. Verified against the real interceptor that this URL is allowed while siblings, subpaths, remote and file: are not. Three fixes against the drafted version: - showError() called setHtml() with a base URL but never setDocumentUrl(), so an error card would have rendered blank. Now impossible to repeat. - clear() and showError() left the previous thread's inline parts in the scheme handler and its cids in the interceptor. Both now empty the policy, so no thread's parts outlive it. - MessagePage trusted the whole qtmaildir: scheme for typed navigations, which is the same blanket-trust mistake task 5 removed from the interceptor. It now matches the exact document URL. The parts-flattening is extracted into buildThreadCidMap() so it can be tested without a live profile, and a cidPrefix containing '!' is sanitized rather than trusted, since Q_ASSERT is compiled out in release and this map decides which bytes a message can name. The sanitizer escapes '_' before replacing '!', because a plain replace would map "m0!x" and "m0_x" onto one key and merge two messages, which is the very collision the namespacing exists to prevent. Mutation-verified: the naive replace fails the distinctness test, and dropping the sanitizer trips the assert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/messageview.h')
-rw-r--r--src/messageview.h66
1 files changed, 66 insertions, 0 deletions
diff --git a/src/messageview.h b/src/messageview.h
new file mode 100644
index 0000000..9f2edb3
--- /dev/null
+++ b/src/messageview.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <QList>
+#include <QUrl>
+#include <QWidget>
+
+#include "htmlbuilder.h"
+#include "mimeparser.h"
+
+class QLabel;
+class QPushButton;
+class QWebEngineView;
+class QWebEngineProfile;
+class CidSchemeHandler;
+class RequestInterceptor;
+
+/// The message pane: thread header, body, attachment bar.
+///
+/// A whole thread renders into one web view. A newsletter thread can hold
+/// dozens of messages, and one view per message would spawn one Chromium
+/// render process per message.
+class MessageView : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit MessageView(QWidget *parent = nullptr);
+ ~MessageView() override;
+
+ /// The base URL every document in this pane is loaded with, and the only
+ /// qtmaildir: URL the interceptor trusts. Defined once so setHtml() and
+ /// setDocumentUrl() cannot drift apart: if they ever disagree, the
+ /// interceptor fails closed and the pane renders nothing at all.
+ static QUrl documentUrl() { return QUrl(QStringLiteral("qtmaildir://message")); }
+
+ /// Renders a whole thread, oldest first. Items whose expanded flag is
+ /// false collapse to a one-line stub.
+ void showThread(const QList<ThreadRenderItem> &items);
+
+ void showError(const QString &text, const QString &filePath);
+ void clear();
+
+public slots:
+ void toggleHtml();
+ void loadRemoteContent();
+
+signals:
+ void statusMessage(const QString &text);
+
+private:
+ void render();
+ void updateHeader();
+ void setDocument(const QString &html);
+
+ QList<ThreadRenderItem> m_items;
+ bool m_preferHtml = true;
+
+ QWebEngineProfile *m_profile = nullptr;
+ QWebEngineView *m_view = nullptr;
+ RequestInterceptor *m_interceptor = nullptr;
+ CidSchemeHandler *m_cidHandler = nullptr;
+
+ QLabel *m_headerLabel = nullptr;
+ QLabel *m_blockedLabel = nullptr;
+ QPushButton *m_loadRemoteButton = nullptr;
+ QWidget *m_attachmentBar = nullptr;
+};