File indexing completed on 2025-03-09 04:54:38

0001 /*
0002   SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 
0006 */
0007 #include "scamdetectionwebengine.h"
0008 #include "MessageViewer/ScamCheckShortUrl"
0009 #include "scamdetectiondetailsdialog.h"
0010 #include "settings/messageviewersettings.h"
0011 #include "webengineviewer/webenginescript.h"
0012 #include <WebEngineViewer/WebEngineManageScript>
0013 
0014 #include <KLocalizedString>
0015 
0016 #include <QPointer>
0017 #include <QRegularExpression>
0018 #include <QWebEnginePage>
0019 
0020 using namespace MessageViewer;
0021 
0022 template<typename Arg, typename R, typename C>
0023 struct InvokeWrapper {
0024     QPointer<R> receiver;
0025     void (C::*memberFunction)(Arg);
0026     void operator()(Arg result)
0027     {
0028         if (receiver) {
0029             (receiver->*memberFunction)(result);
0030         }
0031     }
0032 };
0033 
0034 template<typename Arg, typename R, typename C>
0035 
0036 InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFunction)(Arg))
0037 {
0038     InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFunction};
0039     return wrapper;
0040 }
0041 
0042 static QString addWarningColor(const QString &url)
0043 {
0044     const QString error = QStringLiteral("<font color=#FF0000>%1</font>").arg(url);
0045     return error;
0046 }
0047 
0048 class MessageViewer::ScamDetectionWebEnginePrivate
0049 {
0050 public:
0051     ScamDetectionWebEnginePrivate() = default;
0052 
0053     QString mDetails;
0054     QPointer<MessageViewer::ScamDetectionDetailsDialog> mDetailsDialog;
0055 };
0056 
0057 ScamDetectionWebEngine::ScamDetectionWebEngine(QObject *parent)
0058     : QObject(parent)
0059     , d(new MessageViewer::ScamDetectionWebEnginePrivate)
0060 {
0061 }
0062 
0063 ScamDetectionWebEngine::~ScamDetectionWebEngine() = default;
0064 
0065 void ScamDetectionWebEngine::scanPage(QWebEnginePage *page)
0066 {
0067     if (MessageViewer::MessageViewerSettings::self()->scamDetectionEnabled()) {
0068         page->runJavaScript(WebEngineViewer::WebEngineScript::findAllAnchorsAndForms(),
0069                             WebEngineViewer::WebEngineManageScript::scriptWordId(),
0070                             invoke(this, &ScamDetectionWebEngine::handleScanPage));
0071     }
0072 }
0073 
0074 void ScamDetectionWebEngine::handleScanPage(const QVariant &result)
0075 {
0076     bool foundScam = false;
0077 
0078     d->mDetails.clear();
0079     const QVariantList resultList = result.toList();
0080     if (resultList.count() != 1) {
0081         Q_EMIT resultScanDetection(foundScam);
0082         return;
0083     }
0084     static const QRegularExpression ip4regExp(QStringLiteral("\\b[0-9]{1,3}\\.[0-9]{1,3}(?:\\.[0-9]{0,3})?(?:\\.[0-9]{0,3})?"));
0085     const QVariantMap mapResult = resultList.at(0).toMap();
0086     const QList<QVariant> lst = mapResult.value(QStringLiteral("anchors")).toList();
0087     for (const QVariant &var : lst) {
0088         QMap<QString, QVariant> mapVariant = var.toMap();
0089         // qDebug()<<" mapVariant"<<mapVariant;
0090 
0091         // 1) detect if title has a url and title != href
0092         const QString title = mapVariant.value(QStringLiteral("title")).toString();
0093         QString href = mapVariant.value(QStringLiteral("src")).toString();
0094         if (!QUrl(href).toString().contains(QLatin1StringView("kmail:showAuditLog"))) {
0095             href = href.toLower();
0096         }
0097         const QUrl url(href);
0098         if (!title.isEmpty()) {
0099             if (title.startsWith(QLatin1StringView("http:")) || title.startsWith(QLatin1StringView("https:")) || title.startsWith(QLatin1StringView("www."))) {
0100                 if (title.startsWith(QLatin1StringView("www."))) {
0101                     const QString completUrl = url.scheme() + QLatin1StringView("://") + title;
0102                     if (completUrl != href && href != (completUrl + QLatin1Char('/'))) {
0103                         foundScam = true;
0104                     }
0105                 } else {
0106                     if (href != title) {
0107                         // http://www.kde.org == http://www.kde.org/
0108                         if (href != (title + QLatin1Char('/'))) {
0109                             foundScam = true;
0110                         }
0111                     }
0112                 }
0113                 if (foundScam) {
0114                     d->mDetails += QLatin1StringView("<li>")
0115                         + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is often the case in scam emails "
0116                                "to mislead the recipient",
0117                                addWarningColor(title),
0118                                addWarningColor(href))
0119                         + QLatin1StringView("</li>");
0120                 }
0121             }
0122         }
0123         if (!foundScam) {
0124             // 2) detect if url href has ip and not server name.
0125             const QString hostname = url.host();
0126             if (hostname.contains(ip4regExp) && !hostname.contains(QLatin1StringView("127.0.0.1"))) { // hostname
0127                 d->mDetails += QLatin1StringView("<li>")
0128                     + i18n("This email contains a link which points to a numerical IP address (%1) instead of a typical textual website address. This is often "
0129                            "the case in scam emails.",
0130                            addWarningColor(hostname))
0131                     + QLatin1StringView("</li>");
0132                 foundScam = true;
0133             } else if (hostname.contains(QLatin1Char('%'))) { // Hexa value for ip
0134                 d->mDetails += QLatin1StringView("<li>")
0135                     + i18n("This email contains a link which points to a hexadecimal IP address (%1) instead of a typical textual website address. This is "
0136                            "often the case in scam emails.",
0137                            addWarningColor(hostname))
0138                     + QLatin1StringView("</li>");
0139                 foundScam = true;
0140             } else if (url.toString().contains(QLatin1StringView("url?q="))) { // 4) redirect url.
0141                 d->mDetails += QLatin1StringView("<li>") + i18n("This email contains a link (%1) which has a redirection", addWarningColor(url.toString()))
0142                     + QLatin1StringView("</li>");
0143                 foundScam = true;
0144             } else if ((url.toString().count(QStringLiteral("http://")) > 1)
0145                        || (url.toString().count(QStringLiteral("https://")) > 1)) { // 5) more that 1 http in url.
0146                 if (!url.toString().contains(QLatin1StringView("kmail:showAuditLog"))) {
0147                     d->mDetails += QLatin1StringView("<li>")
0148                         + i18n("This email contains a link (%1) which contains multiple http://. This is often the case in scam emails.",
0149                                addWarningColor(url.toString()))
0150                         + QLatin1StringView("</li>");
0151                     foundScam = true;
0152                 }
0153             }
0154         }
0155         // Check shortUrl
0156         if (!foundScam) {
0157             if (ScamCheckShortUrl::isShortUrl(url)) {
0158                 d->mDetails += QLatin1StringView("<li>")
0159                     + i18n("This email contains a shorturl (%1). It can redirect to another server.", addWarningColor(url.toString()))
0160                     + QLatin1StringView("</li>");
0161                 foundScam = true;
0162             }
0163         }
0164         if (!foundScam) {
0165             QUrl displayUrl = QUrl(mapVariant.value(QStringLiteral("text")).toString());
0166             // Special case if https + port 443 it will return url without port
0167             QString text = (displayUrl.port() == 443 && displayUrl.scheme() == QLatin1StringView("https"))
0168                 ? displayUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments | QUrl::RemovePort)
0169                 : displayUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
0170             if (text.endsWith(QLatin1StringView("%22"))) {
0171                 text.chop(3);
0172             }
0173             const QUrl normalizedHrefUrl = QUrl(href.toLower());
0174             QString normalizedHref = normalizedHrefUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
0175             if (text != normalizedHref) {
0176                 if (normalizedHref.contains(QStringLiteral("%5C"))) {
0177                     normalizedHref.replace(QStringLiteral("%5C"), QStringLiteral("/"));
0178                 }
0179             }
0180             if (normalizedHref.endsWith(QLatin1StringView("%22"))) {
0181                 normalizedHref.chop(3);
0182             }
0183             // qDebug() << "text " << text << " href "<<href << " normalizedHref " << normalizedHref;
0184 
0185             if (!text.isEmpty()) {
0186                 if (text.startsWith(QLatin1StringView("http:/")) || text.startsWith(QLatin1StringView("https:/"))) {
0187                     if (text.toLower() != normalizedHref.toLower()) {
0188                         if (text != normalizedHref) {
0189                             if (normalizedHref != (text + QLatin1Char('/'))) {
0190                                 if (normalizedHref.toHtmlEscaped() != text) {
0191                                     if (QString::fromUtf8(QUrl(text).toEncoded()) != normalizedHref) {
0192                                         if (QUrl(normalizedHref).toDisplayString() != text) {
0193                                             const bool qurlqueryequal = displayUrl.query() == normalizedHrefUrl.query();
0194                                             const QString displayUrlWithoutQuery =
0195                                                 displayUrl.toDisplayString(QUrl::RemoveQuery | QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
0196                                             const QString hrefUrlWithoutQuery =
0197                                                 normalizedHrefUrl.toDisplayString(QUrl::RemoveQuery | QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
0198                                             // qDebug() << "displayUrlWithoutQuery "  << displayUrlWithoutQuery << " hrefUrlWithoutQuery " <<
0199                                             // hrefUrlWithoutQuery << " text " << text;
0200                                             if (qurlqueryequal && (displayUrlWithoutQuery + QLatin1Char('/') != hrefUrlWithoutQuery)) {
0201                                                 d->mDetails += QLatin1StringView("<li>")
0202                                                     + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is "
0203                                                            "often "
0204                                                            "the case in scam emails to mislead the recipient",
0205                                                            addWarningColor(text),
0206                                                            addWarningColor(normalizedHref))
0207                                                     + QLatin1StringView("</li>");
0208                                                 foundScam = true;
0209                                             }
0210                                         }
0211                                     }
0212                                 }
0213                             }
0214                         }
0215                     }
0216                 }
0217             }
0218         }
0219     }
0220     if (mapResult.value(QStringLiteral("forms")).toInt() > 0) {
0221         d->mDetails +=
0222             QLatin1StringView("<li></b>") + i18n("Message contains form element. This is often the case in scam emails.") + QLatin1StringView("</b></li>");
0223         foundScam = true;
0224     }
0225     if (foundScam) {
0226         d->mDetails.prepend(QLatin1StringView("<b>") + i18n("Details:") + QLatin1StringView("</b><ul>"));
0227         d->mDetails += QLatin1StringView("</ul>");
0228         Q_EMIT messageMayBeAScam();
0229     }
0230     Q_EMIT resultScanDetection(foundScam);
0231 }
0232 
0233 void ScamDetectionWebEngine::showDetails()
0234 {
0235     if (!d->mDetailsDialog) {
0236         d->mDetailsDialog = new MessageViewer::ScamDetectionDetailsDialog;
0237     }
0238     d->mDetailsDialog->setDetails(d->mDetails);
0239     d->mDetailsDialog->show();
0240 }
0241 
0242 #include "moc_scamdetectionwebengine.cpp"