diff --git a/src/ui/readerview.cpp b/src/ui/readerview.cpp
index a66a6bc..90c17f0 100644
--- a/src/ui/readerview.cpp
+++ b/src/ui/readerview.cpp
@@ -472,7 +472,8 @@ void ReaderView::setupBodyViewer() {
m_bodyLayout->setContentsMargins(16, 16, 16, 16);
m_bodyLayout->setSpacing(0);
- m_bodyViewer = new QTextBrowser();
+ m_bodyViewer = new ImageTextBrowser();
+ m_bodyViewer->setNetworkManager(m_network);
m_bodyViewer->setOpenExternalLinks(false); // We'll handle link clicks securely
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
m_bodyViewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
@@ -659,67 +660,6 @@ void ReaderView::saveTrustedSenders()
s.sync();
}
-QString ReaderView::processImages(QString html, bool allow)
-{
- if (!allow) return html;
-
- // Find all external (http/https) image URLs in the html.
- QRegularExpression re("(?i)
]*\\bsrc\\s*=\\s*[\"'])(https?://[^\"'>\\s]+)([\"'][^>]*)>");
- QStringList urls;
- auto mit = re.globalMatch(html);
- while (mit.hasNext()) {
- auto m = mit.next();
- urls << m.captured(2);
- }
-
- if (urls.isEmpty()) return html;
-
- // Download each unique URL and substitute with a data: URI (QTextBrowser
- // cannot fetch http images itself). Synchronous within reason; a short
- // per-request timeout keeps the UI responsive.
- QStringList done;
- for (const QString &url : urls) {
- if (done.contains(url)) continue;
- done << url;
- QUrl u(url);
- if (!u.isValid() || (u.scheme() != "http" && u.scheme() != "https")) continue;
-
- QNetworkRequest req(u);
- req.setTransferTimeout(8000);
- QNetworkReply *reply = m_network->get(req);
- QEventLoop loop;
- QTimer timer; timer.setSingleShot(true);
- connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
- connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
- timer.start(8000);
- loop.exec();
-
- QString replacement;
- if (reply->error() == QNetworkReply::NoError) {
- const QByteArray data = reply->readAll();
- if (!data.isEmpty()) {
- const QByteArray mime = reply->header(QNetworkRequest::ContentTypeHeader)
- .toByteArray().split(';').first().trimmed();
- const QByteArray b64 = data.toBase64();
- replacement = QString("data:%1;base64,%2").arg(QString::fromLatin1(mime.isEmpty() ? "image/png" : mime),
- QString::fromLatin1(b64));
- }
- } else {
- qDebug() << "[ReaderView] image load failed:" << url << reply->errorString();
- }
- reply->deleteLater();
-
- if (!replacement.isEmpty()) {
- // Replace this exact src URL (the first occurrence only risk is fine here because
- // we iterate unique URLs and each appears verbatim).
- html.replace(QUrl::fromPercentEncoding(url.toUtf8()), replacement);
- // Also try replacing the raw-encoded form if slightly different.
- html.replace(url, replacement);
- }
- }
- return html;
-}
-
QString ReaderView::sanitizeHtml(const QString &html) {
if (html.isEmpty()) return "
(Mensaje vacío)
";
@@ -741,7 +681,10 @@ QString ReaderView::sanitizeHtml(const QString &html) {
// An image is "allowed" if the user clicked "Cargar imágenes", or if this sender is trusted
// ("Mostrar siempre imágenes de este remitente" persisted choice).
const bool trusted = !m_currentSender.isEmpty() && isSenderTrusted(m_currentSender);
- if (!m_allowExternalImages && !trusted) {
+ const bool allowImages = m_allowExternalImages || trusted;
+ if (m_bodyViewer) m_bodyViewer->setExternalImagesAllowed(allowImages);
+
+ if (!allowImages) {
// Match images with http/https/data URLs in src attribute
QRegularExpression extImgRegex("(?i)
]*src\\s*=\\s*[\"'](?:https?|data):[^\"']*[\"'][^>]*)>");
int beforeCount = result.count("data-blocked=\"true\"");
@@ -756,11 +699,10 @@ QString ReaderView::sanitizeHtml(const QString &html) {
if (m_loadImagesButton) m_loadImagesButton->setVisible(hasBlocked);
if (m_alwaysLoadImagesButton) m_alwaysLoadImagesButton->setVisible(hasBlocked);
} else {
- // Images are allowed for this view: hide both buttons and download+render
- // the external images as data: URIs (QTextBrowser cannot fetch http itself).
+ // Images are allowed: leave the http(s) src URLs intact in the HTML.
+ // ImageTextBrowser::loadResource() will download them via QNetworkAccessManager.
if (m_loadImagesButton) m_loadImagesButton->setVisible(false);
if (m_alwaysLoadImagesButton) m_alwaysLoadImagesButton->setVisible(false);
- result = processImages(result, true);
}
// Add dark mode class if needed
diff --git a/src/ui/readerview.h b/src/ui/readerview.h
index 337455e..f395e0b 100644
--- a/src/ui/readerview.h
+++ b/src/ui/readerview.h
@@ -13,10 +13,78 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
#include
#include "core/mailitem.h"
+/// QTextBrowser that can fetch external (http/https) images. Qt's QTextBrowser
+/// cannot load remote images by itself; overriding loadResource() lets us download
+/// them (with a cache) when images are allowed.
+class ImageTextBrowser : public QTextBrowser
+{
+ Q_OBJECT
+public:
+ explicit ImageTextBrowser(QWidget *parent = nullptr) : QTextBrowser(parent) {}
+
+ void setNetworkManager(QNetworkAccessManager *net) { m_net = net; }
+ void setExternalImagesAllowed(bool on) { m_allowImages = on; }
+ bool externalImagesAllowed() const { return m_allowImages; }
+
+protected:
+ QVariant loadResource(int type, const QUrl &name) override
+ {
+ if (!m_allowImages || type != QTextDocument::ImageResource) {
+ // Not an image we are allowed to fetch; let base handle (usually nothing).
+ return QTextBrowser::loadResource(type, name);
+ }
+ if (!m_net) return QVariant();
+
+ const QString key = name.toString();
+ if (m_cache.contains(key)) return m_cache.value(key);
+
+ // Only fetch http(s); skip data:, file:, qrc:, about:blob etc.
+ const QUrl u(name);
+ if (u.scheme() != "http" && u.scheme() != "https")
+ return QTextBrowser::loadResource(type, name);
+
+ // Synchronous download with a short timeout (keeps render deterministic).
+ QNetworkRequest req(u);
+ req.setTransferTimeout(8000);
+ QNetworkReply *reply = m_net->get(req);
+ QEventLoop loop;
+ QTimer timer; timer.setSingleShot(true);
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ timer.start(8000);
+ loop.exec();
+
+ QVariant result;
+ if (reply->error() == QNetworkReply::NoError) {
+ const QByteArray data = reply->readAll();
+ QPixmap pm;
+ if (!data.isEmpty() && pm.loadFromData(data)) {
+ result = pm;
+ m_cache.insert(key, pm);
+ }
+ } else {
+ qDebug() << "[ReaderView] loadResource failed:" << name.toString() << reply->errorString();
+ }
+ reply->deleteLater();
+ return result;
+ }
+
+private:
+ QNetworkAccessManager *m_net = nullptr;
+ bool m_allowImages = false;
+ QHash m_cache;
+};
+
class ReaderView : public QWidget {
Q_OBJECT
@@ -73,7 +141,7 @@ private:
// Body
QScrollArea *m_scrollArea;
- QTextBrowser *m_bodyViewer;
+ ImageTextBrowser *m_bodyViewer;
QWidget *m_bodyContainer;
QVBoxLayout *m_bodyLayout;
qreal m_zoomFactor = 1.0;
@@ -114,7 +182,6 @@ private:
static QString trustKey(const QString &sender) { return sender.trimmed().toLower(); }
void loadTrustedSenders();
void saveTrustedSenders();
- /// Post-process html: if images allowed (via button or trusted sender), replace
- /// external http(s) image src with downloaded data: URIs so QTextBrowser shows them.
- QString processImages(QString html, bool allow);
+ /// Post-process html: if images allowed (via button or trusted sender), keep the
+ /// http(s) src URLs so ImageTextBrowser::loadResource() downloads them.
};