Line data Source code
1 : #pragma once
2 :
3 : #include <QWebEngineView>
4 :
5 : #include "data/Models.h"
6 : #include "ui/HtmlSanitizer.h"
7 :
8 : class QClipboard;
9 : class QContextMenuEvent;
10 : class QFrame;
11 : class QLabel;
12 : class QMenu;
13 : class QPushButton;
14 : class QStackedWidget;
15 : class QTextBrowser;
16 : class QVBoxLayout;
17 : class QHBoxLayout;
18 : class QWebEngineProfile;
19 : class AttachmentBar;
20 : class ExternalContentInterceptor;
21 : class MailCache;
22 : class PdfViewerWidget;
23 :
24 : // T-351: QWebEngineView subclass that overrides contextMenuEvent to suppress
25 : // Chromium's built-in context menu and emit our own signal instead.
26 : class MailWebEngineView : public QWebEngineView {
27 : Q_OBJECT
28 : public:
29 : using QWebEngineView::QWebEngineView;
30 : signals:
31 : void mailContextMenuRequested(const QPoint &globalPos);
32 : protected:
33 : void contextMenuEvent(QContextMenuEvent *event) override;
34 : };
35 :
36 : // Composite email viewer widget.
37 : //
38 : // Layout (top to bottom):
39 : // 1. Header frame (background block):
40 : // a. Subject label (bold heading)
41 : // b. Meta label (Von/An/Datum on separate lines)
42 : // 2. AttachmentBar: clickable attachment chips
43 : // 3. Info bar: external content warning + view mode toggle
44 : // 4. QStackedWidget: page 0 = QTextBrowser (plain), page 1 = QWebEngineView
45 : class MailView : public QWidget {
46 504 : Q_OBJECT
47 : #ifdef MAILJD_UNIT_TEST
48 : friend class TestMailView;
49 : #endif
50 :
51 : public:
52 : explicit MailView(QWidget *parent = nullptr);
53 :
54 : void displayMail(const MailHeader &header, const MailBody &body);
55 : void refreshLabels(const QStringList &labels);
56 : void clear();
57 : void toggleViewMode();
58 : void loadExternalContent();
59 : void showSource();
60 :
61 : // T-122: Inject MailCache for whitelist access
62 : void setCache(MailCache *cache);
63 :
64 : // T-544: Reload whitelist from MailCache into ExternalContentInterceptor
65 : void reloadWhitelist();
66 :
67 :
68 :
69 :
70 121 : AttachmentBar *attachmentBar() const { return m_attachmentBar; }
71 :
72 : signals:
73 : void externalContentLoadRequested();
74 : void whitelistChanged(); // Emitted after addWhitelistEntry()
75 :
76 : // T-351: Context menu actions (connected by MainWindow)
77 : void replyRequested();
78 : void replyAllRequested();
79 : void forwardRequested();
80 : void moveRequested();
81 : void archiveRequested();
82 : void deleteRequested();
83 :
84 : private:
85 : void ensureWebEngine();
86 : void renderCurrentBody();
87 : void showHtmlSanitizingPlaceholder();
88 : void applyExternalContentPolicy();
89 : bool applySanitizedHtml(const QString &sanitizedHtml, const QString &csp,
90 : quint64 generation);
91 : void finishHtmlLoad(bool ok);
92 : void retranslateUi();
93 :
94 : // Sprint 75: named handler for the plain-text viewer's
95 : // customContextMenuRequested signal. The signal delivers viewport-
96 : // local coordinates; this maps them to global screen coordinates via
97 : // m_textBrowser->viewport()->mapToGlobal() before forwarding to
98 : // showMailContextMenu(). showMailContextMenu() then receives global
99 : // coordinates from BOTH callers (the WebEngine path already emits
100 : // event->globalPos()), so its body no longer mixes coordinate
101 : // systems.
102 : void onPlainTextContextMenuRequested(const QPoint &localPos);
103 :
104 : // T-351: Context menu. Both callers normalize to global screen
105 : // coordinates before invoking this (QTextBrowser via
106 : // onPlainTextContextMenuRequested, WebEngine via contextMenuEvent).
107 : void showMailContextMenu(const QPoint &globalPos);
108 :
109 : // T-71.6: Open an attachment inline in the PDF viewer (page 2 of the
110 : // stack). Validates the MIME type, lazy-loads the BLOB via MailCache, and
111 : // switches the stack. leavePdfViewer() restores the previous page.
112 : void showAttachmentInline(qint64 attachmentId);
113 : void leavePdfViewer();
114 :
115 : // T-608/SEC-04: DOMPurify-based HTML sanitizer (replaces old regex sanitizer)
116 : HtmlSanitizer *m_htmlSanitizer = nullptr;
117 :
118 : protected:
119 : void changeEvent(QEvent *event) override;
120 :
121 : // Components
122 : QVBoxLayout *m_mainLayout;
123 :
124 : // Header container (subject + meta with background)
125 : QFrame *m_headerFrame;
126 : QLabel *m_avatarLabel; // 67.B4: sender initials avatar
127 : QLabel *m_subjectLabel;
128 : QLabel *m_metaLabel;
129 : QHBoxLayout *m_labelsLayout = nullptr;
130 :
131 : // Info bar
132 : QFrame *m_infoBar;
133 : QLabel *m_infoLabel;
134 : QPushButton *m_loadExternalBtn;
135 : QMenu *m_externalMenu = nullptr;
136 : QPushButton *m_toggleBtn;
137 : QPushButton *m_sourceBtn;
138 :
139 : // Content
140 : AttachmentBar *m_attachmentBar;
141 : QStackedWidget *m_stack;
142 : QTextBrowser *m_textBrowser;
143 : MailWebEngineView *m_webView = nullptr;
144 : QWebEngineProfile *m_webProfile = nullptr;
145 : ExternalContentInterceptor *m_interceptor = nullptr;
146 : PdfViewerWidget *m_pdfView = nullptr; // T-71.6: inline PDF (stack page 2)
147 : int m_previousStackIndex = 0; // T-71.6: page to return to from PDF
148 :
149 : // State
150 : MailHeader m_currentHeader;
151 : MailBody m_currentBody;
152 : bool m_showHtml = false;
153 : bool m_externalContentOverride = false; // T-073: user clicked "Load"
154 : quint64 m_renderGeneration = 0;
155 : quint64 m_pendingHtmlGeneration = 0;
156 :
157 : // Sprint 75: opt-in exec-seam for unit tests. When m_interceptMenuExec
158 : // is true, showMailContextMenu() stores the received global position
159 : // in m_lastMenuExecGlobalPos and returns WITHOUT calling
160 : // QMenu::exec() — so a unit test can deterministically assert the
161 : // exec coordinate without blocking in the popup event loop.
162 : // Production and the default test path keep the real exec().
163 : bool m_interceptMenuExec = false;
164 : QPoint m_lastMenuExecGlobalPos;
165 :
166 : // T-122: Whitelist integration
167 : MailCache *m_cache = nullptr;
168 : static QString extractEmail(const QString &fromField);
169 :
170 : // 67.B4: up to two initials from a From field (protected — tests
171 : // reach it through a subclass, like extractEmail)
172 : static QString initialsForSender(const QString &fromField);
173 :
174 : // 67.B4: 40x40 initials disc, deterministic color per sender
175 : QPixmap renderAvatar(const QString &fromField) const;
176 : void buildExternalContentMenu();
177 : };
|