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 805 : 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 119 : 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 : bool applySanitizedHtml(const QString &sanitizedHtml, const QString &csp,
88 : quint64 generation);
89 : void retranslateUi();
90 :
91 : // Sprint 75: named handler for the plain-text viewer's
92 : // customContextMenuRequested signal. The signal delivers viewport-
93 : // local coordinates; this maps them to global screen coordinates via
94 : // m_textBrowser->viewport()->mapToGlobal() before forwarding to
95 : // showMailContextMenu(). showMailContextMenu() then receives global
96 : // coordinates from BOTH callers (the WebEngine path already emits
97 : // event->globalPos()), so its body no longer mixes coordinate
98 : // systems.
99 : void onPlainTextContextMenuRequested(const QPoint &localPos);
100 :
101 : // T-351: Context menu. Both callers normalize to global screen
102 : // coordinates before invoking this (QTextBrowser via
103 : // onPlainTextContextMenuRequested, WebEngine via contextMenuEvent).
104 : void showMailContextMenu(const QPoint &globalPos);
105 :
106 : // T-71.6: Open an attachment inline in the PDF viewer (page 2 of the
107 : // stack). Validates the MIME type, lazy-loads the BLOB via MailCache, and
108 : // switches the stack. leavePdfViewer() restores the previous page.
109 : void showAttachmentInline(qint64 attachmentId);
110 : void leavePdfViewer();
111 :
112 : // T-608/SEC-04: DOMPurify-based HTML sanitizer (replaces old regex sanitizer)
113 : HtmlSanitizer *m_htmlSanitizer = nullptr;
114 :
115 : protected:
116 : void changeEvent(QEvent *event) override;
117 :
118 : // Components
119 : QVBoxLayout *m_mainLayout;
120 :
121 : // Header container (subject + meta with background)
122 : QFrame *m_headerFrame;
123 : QLabel *m_avatarLabel; // 67.B4: sender initials avatar
124 : QLabel *m_subjectLabel;
125 : QLabel *m_metaLabel;
126 : QHBoxLayout *m_labelsLayout = nullptr;
127 :
128 : // Info bar
129 : QFrame *m_infoBar;
130 : QLabel *m_infoLabel;
131 : QPushButton *m_loadExternalBtn;
132 : QMenu *m_externalMenu = nullptr;
133 : QPushButton *m_toggleBtn;
134 : QPushButton *m_sourceBtn;
135 :
136 : // Content
137 : AttachmentBar *m_attachmentBar;
138 : QStackedWidget *m_stack;
139 : QTextBrowser *m_textBrowser;
140 : MailWebEngineView *m_webView = nullptr;
141 : QWebEngineProfile *m_webProfile = nullptr;
142 : ExternalContentInterceptor *m_interceptor = nullptr;
143 : PdfViewerWidget *m_pdfView = nullptr; // T-71.6: inline PDF (stack page 2)
144 : int m_previousStackIndex = 0; // T-71.6: page to return to from PDF
145 :
146 : // State
147 : MailHeader m_currentHeader;
148 : MailBody m_currentBody;
149 : bool m_showHtml = false;
150 : bool m_externalContentOverride = false; // T-073: user clicked "Load"
151 : quint64 m_renderGeneration = 0;
152 :
153 : // Sprint 75: opt-in exec-seam for unit tests. When m_interceptMenuExec
154 : // is true, showMailContextMenu() stores the received global position
155 : // in m_lastMenuExecGlobalPos and returns WITHOUT calling
156 : // QMenu::exec() — so a unit test can deterministically assert the
157 : // exec coordinate without blocking in the popup event loop.
158 : // Production and the default test path keep the real exec().
159 : bool m_interceptMenuExec = false;
160 : QPoint m_lastMenuExecGlobalPos;
161 :
162 : // T-122: Whitelist integration
163 : MailCache *m_cache = nullptr;
164 : QString m_currentSenderEmail;
165 : static QString extractEmail(const QString &fromField);
166 :
167 : // 67.B4: up to two initials from a From field (protected — tests
168 : // reach it through a subclass, like extractEmail)
169 : static QString initialsForSender(const QString &fromField);
170 :
171 : // 67.B4: 40x40 initials disc, deterministic color per sender
172 : QPixmap renderAvatar(const QString &fromField) const;
173 : void buildExternalContentMenu();
174 : };
|