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

0001 /*
0002    SPDX-FileCopyrightText: 2013-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "headerstyle_util.h"
0008 #include "messageviewer_debug.h"
0009 
0010 #include "contactdisplaymessagememento.h"
0011 #include "kxface.h"
0012 
0013 #include "header/headerstyle.h"
0014 
0015 #include "settings/messageviewersettings.h"
0016 
0017 #include <MessageCore/StringUtil>
0018 #include <MimeTreeParser/NodeHelper>
0019 
0020 #include <MessageCore/MessageCoreSettings>
0021 
0022 #include <KEmailAddress>
0023 #include <KLocalizedString>
0024 
0025 #include <QBuffer>
0026 
0027 using namespace MessageCore;
0028 
0029 using namespace MessageViewer;
0030 //
0031 // Convenience functions:
0032 //
0033 HeaderStyleUtil::HeaderStyleUtil() = default;
0034 
0035 QString HeaderStyleUtil::directionOf(const QString &str) const
0036 {
0037     return str.isRightToLeft() ? QStringLiteral("rtl") : QStringLiteral("ltr");
0038 }
0039 
0040 QString HeaderStyleUtil::strToHtml(const QString &str, KTextToHTML::Options flags)
0041 {
0042     return KTextToHTML::convertToHtml(str, flags, 4096, 512);
0043 }
0044 
0045 // Prepare the date string
0046 QString HeaderStyleUtil::dateString(KMime::Message *message, HeaderStyleUtilDateFormat dateFormat)
0047 {
0048     return dateString(message->date()->dateTime(), dateFormat);
0049 }
0050 
0051 QString HeaderStyleUtil::dateString(const QDateTime &dateTime, HeaderStyleUtilDateFormat dateFormat)
0052 {
0053     if (!dateTime.isValid()) {
0054         qCDebug(MESSAGEVIEWER_LOG) << "Unable to parse date";
0055         return i18nc("Unknown date", "Unknown");
0056     }
0057 
0058     switch (dateFormat) {
0059     case ShortDate:
0060         return KMime::DateFormatter::formatDate(KMime::DateFormatter::Localized, dateTime.toLocalTime());
0061     case LongDate:
0062         return KMime::DateFormatter::formatDate(KMime::DateFormatter::CTime, dateTime.toLocalTime());
0063     case FancyShortDate:
0064         return KMime::DateFormatter::formatDate(KMime::DateFormatter::Fancy, dateTime.toLocalTime());
0065     case FancyLongDate:
0066         return KMime::DateFormatter::formatDate(KMime::DateFormatter::Fancy, dateTime.toLocalTime(), QString(), false);
0067     case CustomDate:
0068     default:
0069         return dateStr(dateTime);
0070     }
0071 }
0072 
0073 QString HeaderStyleUtil::subjectString(KMime::Message *message, KTextToHTML::Options flags) const
0074 {
0075     QString subjectStr;
0076     const KMime::Headers::Subject *const subject = message->subject(false);
0077     if (subject) {
0078         subjectStr = subject->asUnicodeString();
0079         if (subjectStr.isEmpty()) {
0080             subjectStr = i18n("No Subject");
0081         } else {
0082             subjectStr = strToHtml(subjectStr, flags);
0083         }
0084     } else {
0085         subjectStr = i18n("No Subject");
0086     }
0087     return subjectStr;
0088 }
0089 
0090 QString HeaderStyleUtil::subjectDirectionString(KMime::Message *message) const
0091 {
0092     QString subjectDir;
0093     if (message->subject(false)) {
0094         subjectDir = directionOf(MessageCore::StringUtil::cleanSubject(message));
0095     } else {
0096         subjectDir = directionOf(i18n("No Subject"));
0097     }
0098     return subjectDir;
0099 }
0100 
0101 QString HeaderStyleUtil::spamStatus(KMime::Message *message) const
0102 {
0103     QString spamHTML;
0104     const SpamScores scores = SpamHeaderAnalyzer::getSpamScores(message);
0105 
0106     for (SpamScores::const_iterator it = scores.constBegin(), end = scores.constEnd(); it != end; ++it) {
0107         spamHTML +=
0108             (*it).agent() + QLatin1Char(' ') + drawSpamMeter((*it).error(), (*it).score(), (*it).confidence(), (*it).spamHeader(), (*it).confidenceHeader());
0109     }
0110     return spamHTML;
0111 }
0112 
0113 QString
0114 HeaderStyleUtil::drawSpamMeter(SpamError spamError, double percent, double confidence, const QString &filterHeader, const QString &confidenceHeader) const
0115 {
0116     static const int meterWidth = 20;
0117     static const int meterHeight = 5;
0118     QImage meterBar(meterWidth, 1, QImage::Format_Indexed8 /*QImage::Format_RGB32*/);
0119     meterBar.setColorCount(24);
0120 
0121     meterBar.setColor(meterWidth + 1, qRgb(255, 255, 255));
0122     meterBar.setColor(meterWidth + 2, qRgb(170, 170, 170));
0123     if (spamError != noError) { // grey is for errors
0124         meterBar.fill(meterWidth + 2);
0125     } else {
0126         static const unsigned short gradient[meterWidth][3] = {{0, 255, 0},   {27, 254, 0},  {54, 252, 0},  {80, 250, 0},  {107, 249, 0},
0127                                                                {135, 247, 0}, {161, 246, 0}, {187, 244, 0}, {214, 242, 0}, {241, 241, 0},
0128                                                                {255, 228, 0}, {255, 202, 0}, {255, 177, 0}, {255, 151, 0}, {255, 126, 0},
0129                                                                {255, 101, 0}, {255, 76, 0},  {255, 51, 0},  {255, 25, 0},  {255, 0, 0}};
0130 
0131         meterBar.fill(meterWidth + 1);
0132         const int max = qMin(meterWidth, static_cast<int>(percent) / 5);
0133         for (int i = 0; i < max; ++i) {
0134             meterBar.setColor(i + 1, qRgb(gradient[i][0], gradient[i][1], gradient[i][2]));
0135             meterBar.setPixel(i, 0, i + 1);
0136         }
0137     }
0138 
0139     QString titleText;
0140     QString confidenceString;
0141     if (spamError == noError) {
0142         if (confidence >= 0) {
0143             confidenceString = QString::number(confidence) + QLatin1StringView("% &nbsp;");
0144             titleText = i18n(
0145                 "%1% probability of being spam with confidence %3%.\n\n"
0146                 "Full report:\nProbability=%2\nConfidence=%4",
0147                 QString::number(percent, 'f', 2),
0148                 filterHeader,
0149                 confidence,
0150                 confidenceHeader);
0151         } else { // do not show negative confidence
0152             confidenceString = QString() + QLatin1StringView("&nbsp;");
0153             titleText = i18n(
0154                 "%1% probability of being spam.\n\n"
0155                 "Full report:\nProbability=%2",
0156                 QString::number(percent, 'f', 2),
0157                 filterHeader);
0158         }
0159     } else {
0160         QString errorMsg;
0161         switch (spamError) {
0162         case errorExtractingAgentString:
0163             errorMsg = i18n("No Spam agent");
0164             break;
0165         case couldNotConverScoreToFloat:
0166             errorMsg = i18n("Spam filter score not a number");
0167             break;
0168         case couldNotConvertThresholdToFloatOrThresholdIsNegative:
0169             errorMsg = i18n("Threshold not a valid number");
0170             break;
0171         case couldNotFindTheScoreField:
0172             errorMsg = i18n("Spam filter score could not be extracted from header");
0173             break;
0174         case couldNotFindTheThresholdField:
0175             errorMsg = i18n("Threshold could not be extracted from header");
0176             break;
0177         default:
0178             errorMsg = i18n("Error evaluating spam score");
0179             break;
0180         }
0181         // report the error in the spam filter
0182         titleText = i18n(
0183             "%1.\n\n"
0184             "Full report:\n%2",
0185             errorMsg,
0186             filterHeader);
0187     }
0188     return QStringLiteral("<img src=\"%1\" width=\"%2\" height=\"%3\" style=\"border: 1px solid black;\" title=\"%4\" />")
0189                .arg(imgToDataUrl(meterBar), QString::number(meterWidth), QString::number(meterHeight), titleText)
0190         + confidenceString;
0191 }
0192 
0193 QString HeaderStyleUtil::imgToDataUrl(const QImage &image) const
0194 {
0195     QByteArray ba;
0196     QBuffer buffer(&ba);
0197     buffer.open(QIODevice::WriteOnly);
0198     image.save(&buffer, "PNG");
0199     return QStringLiteral("data:image/%1;base64,%2").arg(QStringLiteral("PNG"), QString::fromLatin1(ba.toBase64()));
0200 }
0201 
0202 QString HeaderStyleUtil::dateStr(const QDateTime &dateTime)
0203 {
0204     return KMime::DateFormatter::formatDate(static_cast<KMime::DateFormatter::FormatType>(MessageCore::MessageCoreSettings::self()->dateFormat()),
0205                                             dateTime.toLocalTime(),
0206                                             MessageCore::MessageCoreSettings::self()->customDateFormat());
0207 }
0208 
0209 QString HeaderStyleUtil::dateShortStr(const QDateTime &dateTime)
0210 {
0211     KMime::DateFormatter formatter(KMime::DateFormatter::Fancy);
0212     return formatter.dateString(dateTime);
0213 }
0214 
0215 QSharedPointer<KMime::Headers::Generics::MailboxList> mailboxesFromHeader(const KMime::Headers::Base *hrd)
0216 {
0217     QSharedPointer<KMime::Headers::Generics::MailboxList> mailboxList(new KMime::Headers::Generics::MailboxList());
0218     const QByteArray &data = hrd->as7BitString(false);
0219     mailboxList->from7BitString(data);
0220     return mailboxList;
0221 }
0222 
0223 QSharedPointer<KMime::Headers::Generics::MailboxList> HeaderStyleUtil::resentFromList(KMime::Message *message)
0224 {
0225     if (auto hrd = message->headerByType("Resent-From")) {
0226         return mailboxesFromHeader(hrd);
0227     }
0228     return nullptr;
0229 }
0230 
0231 QSharedPointer<KMime::Headers::Generics::MailboxList> HeaderStyleUtil::resentToList(KMime::Message *message)
0232 {
0233     if (auto hrd = message->headerByType("Resent-To")) {
0234         return mailboxesFromHeader(hrd);
0235     }
0236     return nullptr;
0237 }
0238 
0239 void HeaderStyleUtil::updateXFaceSettings(QImage photo, xfaceSettings &settings) const
0240 {
0241     if (!photo.isNull()) {
0242         settings.photoWidth = photo.width();
0243         settings.photoHeight = photo.height();
0244         // scale below 60, otherwise it can get way too large
0245         if (settings.photoHeight > 60) {
0246             double ratio = (double)settings.photoHeight / (double)settings.photoWidth;
0247             settings.photoHeight = 60;
0248             settings.photoWidth = (int)(60 / ratio);
0249             photo = photo.scaled(settings.photoWidth, settings.photoHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0250         }
0251         settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(photo);
0252     }
0253 }
0254 
0255 HeaderStyleUtil::xfaceSettings HeaderStyleUtil::xface(const MessageViewer::HeaderStyle *style, KMime::Message *message) const
0256 {
0257     xfaceSettings settings;
0258     bool useOtherPhotoSources = false;
0259 
0260     if (style->allowAsync()) {
0261         Q_ASSERT(style->nodeHelper());
0262         Q_ASSERT(style->sourceObject());
0263 
0264         ContactDisplayMessageMemento *photoMemento =
0265             dynamic_cast<ContactDisplayMessageMemento *>(style->nodeHelper()->bodyPartMemento(message, "contactphoto"));
0266         if (!photoMemento) {
0267             const QString email = QString::fromLatin1(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
0268             photoMemento = new ContactDisplayMessageMemento(email);
0269             style->nodeHelper()->setBodyPartMemento(message, "contactphoto", photoMemento);
0270             QObject::connect(photoMemento, SIGNAL(update(MimeTreeParser::UpdateMode)), style->sourceObject(), SLOT(update(MimeTreeParser::UpdateMode)));
0271 
0272             // clang-format off
0273             QObject::connect(photoMemento,
0274                              SIGNAL(changeDisplayMail(Viewer::DisplayFormatMessage,bool)),
0275                              style->sourceObject(),
0276                              SIGNAL(changeDisplayMail(Viewer::DisplayFormatMessage,bool)));
0277             // clang-format on
0278         }
0279 
0280         if (photoMemento->finished()) {
0281             useOtherPhotoSources = true;
0282             if (photoMemento->photo().isIntern()) {
0283                 // get photo data and convert to data: url
0284                 const QImage photo = photoMemento->photo().data();
0285                 updateXFaceSettings(photo, settings);
0286             } else if (!photoMemento->imageFromUrl().isNull()) {
0287                 updateXFaceSettings(photoMemento->imageFromUrl(), settings);
0288             } else if (!photoMemento->photo().url().isEmpty()) {
0289                 settings.photoURL = photoMemento->photo().url();
0290                 if (settings.photoURL.startsWith(QLatin1Char('/'))) {
0291                     settings.photoURL.prepend(QLatin1StringView("file:"));
0292                 }
0293             } else if (!photoMemento->gravatarPixmap().isNull()) {
0294                 const QImage photo = photoMemento->gravatarPixmap().toImage();
0295                 updateXFaceSettings(photo, settings);
0296             }
0297         } else {
0298             // if the memento is not finished yet, use other photo sources instead
0299             useOtherPhotoSources = true;
0300         }
0301     } else {
0302         useOtherPhotoSources = true;
0303     }
0304 
0305     if (settings.photoURL.isEmpty() && useOtherPhotoSources) {
0306         if (auto hrd = message->headerByType("Face")) {
0307             // no photo, look for a Face header
0308             const QString faceheader = hrd->asUnicodeString();
0309             if (!faceheader.isEmpty()) {
0310                 qCDebug(MESSAGEVIEWER_LOG) << "Found Face: header";
0311 
0312                 const QByteArray facestring = faceheader.toUtf8();
0313                 // Spec says header should be less than 998 bytes
0314                 // Face: is 5 characters
0315                 if (facestring.length() < 993) {
0316                     const QByteArray facearray = QByteArray::fromBase64(facestring);
0317 
0318                     QImage faceimage;
0319                     if (faceimage.loadFromData(facearray, "png")) {
0320                         // Spec says image must be 48x48 pixels
0321                         if ((48 == faceimage.width()) && (48 == faceimage.height())) {
0322                             settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(faceimage);
0323                             settings.photoWidth = 48;
0324                             settings.photoHeight = 48;
0325                         } else {
0326                             qCDebug(MESSAGEVIEWER_LOG) << "Face: header image is" << faceimage.width() << "by" << faceimage.height() << "not 48x48 Pixels";
0327                         }
0328                     } else {
0329                         qCDebug(MESSAGEVIEWER_LOG) << "Failed to load decoded png from Face: header";
0330                     }
0331                 } else {
0332                     qCDebug(MESSAGEVIEWER_LOG) << "Face: header too long at" << facestring.length();
0333                 }
0334             }
0335         }
0336     }
0337 
0338     if (settings.photoURL.isEmpty() && useOtherPhotoSources) {
0339         if (auto hrd = message->headerByType("X-Face")) {
0340             // no photo, look for a X-Face header
0341             const QString xfhead = hrd->asUnicodeString();
0342             if (!xfhead.isEmpty()) {
0343                 MessageViewer::KXFace xf;
0344                 settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(xf.toImage(xfhead));
0345                 settings.photoWidth = 48;
0346                 settings.photoHeight = 48;
0347             }
0348         }
0349     }
0350 
0351     return settings;
0352 }