MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - ExternalContentInterceptor.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 95.6 % 90 86
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 12 12
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 60.3 % 136 82

             Branch data     Line data    Source code
       1                 :             : #include "ExternalContentInterceptor.h"
       2                 :             : 
       3                 :             : #include <QLoggingCategory>
       4                 :             : #include <QMutexLocker>
       5                 :             : #include <QUrl>
       6                 :             : 
       7   [ +  -  +  -  :           2 : Q_LOGGING_CATEGORY(lcInterceptor, "mailjd.interceptor")
             +  -  -  - ]
       8                 :             : 
       9                 :          26 : ExternalContentInterceptor::ExternalContentInterceptor(QObject *parent)
      10                 :          26 :     : QWebEngineUrlRequestInterceptor(parent) {}
      11                 :             : 
      12                 :          18 : void ExternalContentInterceptor::setBlockExternal(bool block) {
      13                 :          18 :   QMutexLocker locker(&m_mutex);
      14                 :          18 :   m_blockExternal = block;
      15                 :          18 :   m_hasBlocked = false;
      16                 :          18 :   m_blockedDomains.clear(); // T-122: Reset on new mail
      17                 :          18 : }
      18                 :             : 
      19                 :           3 : bool ExternalContentInterceptor::isBlocking() const {
      20                 :           3 :   QMutexLocker locker(&m_mutex);
      21                 :           6 :   return m_blockExternal;
      22                 :           3 : }
      23                 :             : 
      24                 :             : // T-122: Whitelist support
      25                 :          57 : void ExternalContentInterceptor::setWhitelist(const QStringList &domains,
      26                 :             :                                                const QStringList &senders) {
      27                 :             :   Q_UNUSED(senders);
      28                 :          57 :   QMutexLocker locker(&m_mutex);
      29                 :          57 :   m_whitelistedDomains.clear();
      30         [ +  + ]:          85 :   for (const auto &d : domains)
      31   [ +  -  +  - ]:          28 :     m_whitelistedDomains.insert(d.toLower());
      32                 :          57 : }
      33                 :             : 
      34                 :           3 : void ExternalContentInterceptor::setSenderEmail(const QString &email) {
      35                 :             :   Q_UNUSED(email);
      36                 :           3 : }
      37                 :             : 
      38                 :          13 : bool ExternalContentInterceptor::isAllowedByWhitelist(
      39                 :             :     const QUrl &requestUrl) const {
      40                 :          13 :   QMutexLocker locker(&m_mutex);
      41         [ +  - ]:          26 :   return isAllowedByWhitelistLocked(requestUrl);
      42                 :          13 : }
      43                 :             : 
      44                 :          47 : void ExternalContentInterceptor::resetBlockedDomains() {
      45                 :          47 :   QMutexLocker locker(&m_mutex);
      46                 :          47 :   m_blockedDomains.clear();
      47                 :          47 : }
      48                 :             : 
      49                 :           6 : QSet<QString> ExternalContentInterceptor::blockedDomains() const {
      50                 :           6 :   QMutexLocker locker(&m_mutex);
      51                 :           6 :   return m_blockedDomains;
      52                 :           6 : }
      53                 :             : 
      54                 :          53 : void ExternalContentInterceptor::interceptRequest(
      55                 :             :     QWebEngineUrlRequestInfo &info) {
      56                 :          53 :   bool emitBlocked = false;
      57                 :          53 :   bool emitLink = false;
      58         [ +  - ]:          53 :   QUrl signalUrl;
      59         [ +  - ]:          53 :   QUrl blockedUrl;
      60                 :             : 
      61                 :             :   {
      62                 :             :     // T-616/SEC-15: Guard shared state access — this runs on Chromium IO thread.
      63                 :             :     // Signals are emitted after the mutex is released; connected UI slots may
      64                 :             :     // synchronously call getters such as blockedDomains().
      65                 :          53 :     QMutexLocker locker(&m_mutex);
      66                 :             : 
      67                 :             :     // T-350: Intercept link click navigations (main frame only).
      68                 :             :     // Block the navigation and emit signal so MailView opens it in the
      69                 :             :     // system browser. This replaces MailWebEnginePage::acceptNavigationRequest
      70                 :             :     // and eliminates the need for setPage() (which caused a freeze).
      71   [ +  -  +  +  :          66 :     if (info.navigationType() == QWebEngineUrlRequestInfo::NavigationTypeLink &&
                   -  + ]
      72   [ +  -  -  + ]:          13 :         info.resourceType() == QWebEngineUrlRequestInfo::ResourceTypeMainFrame) {
      73         [ #  # ]:           0 :       info.block(true);
      74         [ #  # ]:           0 :       signalUrl = info.requestUrl();
      75                 :           0 :       emitLink = true;
      76                 :             :     } else {
      77         [ +  + ]:          53 :       if (!m_blockExternal)
      78                 :          51 :         return;
      79                 :             : 
      80   [ +  -  +  -  :          34 :       QString scheme = info.requestUrl().scheme().toLower();
                   +  - ]
      81   [ +  +  +  -  :          97 :       if (scheme != QStringLiteral("http") && scheme != QStringLiteral("https"))
          +  +  +  +  +  
             -  +  -  +  
                      + ]
      82                 :          29 :         return;
      83                 :             : 
      84                 :             :       // T-122/MED-16: Only the requested resource domain is trusted here.
      85                 :             :       // The displayed From sender/domain is spoofable without message auth
      86                 :             :       // results, so it must not auto-unblock external content.
      87   [ +  -  +  -  :          10 :       QString requestDomain = info.requestUrl().host().toLower();
                   +  - ]
      88   [ +  -  +  -  :           5 :       if (isAllowedByWhitelistLocked(info.requestUrl()))
                   +  + ]
      89                 :           3 :         return;
      90                 :             : 
      91                 :             :       // Block and track
      92         [ +  - ]:           2 :       info.block(true);
      93         [ +  - ]:           2 :       m_blockedDomains.insert(requestDomain); // T-122: Track blocked domain
      94         [ +  - ]:           2 :       blockedUrl = info.requestUrl();
      95                 :             : 
      96                 :             :       // Signal once per page load to show the info bar
      97         [ +  - ]:           2 :       if (!m_hasBlocked) {
      98                 :           2 :         m_hasBlocked = true;
      99                 :           2 :         emitBlocked = true;
     100                 :             :       }
     101   [ +  +  +  + ]:          37 :     }
     102         [ +  + ]:          53 :   }
     103                 :             : 
     104         [ -  + ]:           2 :   if (emitLink)
     105         [ #  # ]:           0 :     emit linkClicked(signalUrl);
     106                 :             : 
     107   [ +  -  +  - ]:           2 :   if (!blockedUrl.isEmpty())
     108   [ +  -  +  -  :           4 :     qCInfo(lcInterceptor) << "Blocked external request:"
             +  -  +  + ]
     109   [ +  -  +  - ]:           2 :                           << blockedUrl.toString();
     110                 :             : 
     111         [ +  - ]:           2 :   if (emitBlocked)
     112         [ +  - ]:           2 :     emit externalContentBlocked();
     113   [ +  +  +  + ]:         104 : }
     114                 :             : 
     115                 :             : #ifdef MAILJD_UNIT_TEST
     116                 :           1 : void ExternalContentInterceptor::simulateBlockedExternalRequestForTest(
     117                 :             :     const QUrl &requestUrl) {
     118                 :           1 :   bool emitBlocked = false;
     119                 :             :   {
     120                 :           1 :     QMutexLocker locker(&m_mutex);
     121   [ +  -  +  -  :           1 :     m_blockedDomains.insert(requestUrl.host().toLower());
                   +  - ]
     122         [ +  - ]:           1 :     if (!m_hasBlocked) {
     123                 :           1 :       m_hasBlocked = true;
     124                 :           1 :       emitBlocked = true;
     125                 :             :     }
     126                 :           1 :   }
     127                 :             : 
     128         [ +  - ]:           1 :   if (emitBlocked)
     129                 :           1 :     emit externalContentBlocked();
     130                 :           1 : }
     131                 :             : #endif
     132                 :             : 
     133                 :          18 : bool ExternalContentInterceptor::isAllowedByWhitelistLocked(
     134                 :             :     const QUrl &requestUrl) const {
     135   [ +  -  +  - ]:          18 :   const QString requestDomain = requestUrl.host().toLower();
     136         [ +  + ]:          18 :   if (requestDomain.isEmpty())
     137                 :           2 :     return false;
     138         [ +  + ]:          16 :   if (m_whitelistedDomains.contains(requestDomain))
     139                 :           4 :     return true;
     140                 :             :   // 67.A4: A whitelist entry also covers its subdomains — the user who
     141                 :             :   // allowed "example.com" expects images from "img.example.com" / a CDN
     142                 :             :   // host of the same domain to load. The dot boundary prevents
     143                 :             :   // "evilexample.com" from matching the entry "example.com".
     144                 :             :   // MED-16 still holds: only user-curated whitelist entries are
     145                 :             :   // consulted; the spoofable From sender never auto-unblocks anything.
     146         [ +  + ]:          18 :   for (const auto &entry : m_whitelistedDomains) {
     147   [ +  -  +  -  :           9 :     if (requestDomain.endsWith(QLatin1Char('.') + entry))
                   +  + ]
     148                 :           3 :       return true;
     149                 :             :   }
     150                 :           9 :   return false;
     151                 :          18 : }
        

Generated by: LCOV version 2.0-1