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("% "); 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(" "); 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 }