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. };