Messagelib

headerstyle_util.cpp
1/*
2 SPDX-FileCopyrightText: 2013-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "headerstyle_util.h"
8#include "messageviewer_debug.h"
9
10#include "contactdisplaymessagememento.h"
11#include "kxface.h"
12
13#include "header/headerstyle.h"
14
15#include "settings/messageviewersettings.h"
16
17#include <MessageCore/StringUtil>
18#include <MimeTreeParser/NodeHelper>
19
20#include <MessageCore/MessageCoreSettings>
21
22#include <KEmailAddress>
23#include <KLocalizedString>
24
25#include <QBuffer>
26
27using namespace MessageCore;
28
29using namespace MessageViewer;
30//
31// Convenience functions:
32//
33HeaderStyleUtil::HeaderStyleUtil() = default;
34
35QString HeaderStyleUtil::directionOf(const QString &str) const
36{
37 return str.isRightToLeft() ? QStringLiteral("rtl") : QStringLiteral("ltr");
38}
39
40QString HeaderStyleUtil::strToHtml(const QString &str, KTextToHTML::Options flags)
41{
42 return KTextToHTML::convertToHtml(str, flags, 4096, 512);
43}
44
45// Prepare the date string
46QString HeaderStyleUtil::dateString(KMime::Message *message, HeaderStyleUtilDateFormat dateFormat)
47{
48 return dateString(message->date()->dateTime(), dateFormat);
49}
50
51QString HeaderStyleUtil::dateString(const QDateTime &dateTime, HeaderStyleUtilDateFormat dateFormat)
52{
53 if (!dateTime.isValid()) {
54 qCDebug(MESSAGEVIEWER_LOG) << "Unable to parse date";
55 return i18nc("Unknown date", "Unknown");
56 }
57
58 switch (dateFormat) {
59 case ShortDate:
61 case LongDate:
63 case FancyShortDate:
65 case FancyLongDate:
67 case CustomDate:
68 default:
69 return dateStr(dateTime);
70 }
71}
72
73QString HeaderStyleUtil::subjectString(KMime::Message *message, KTextToHTML::Options flags) const
74{
75 QString subjectStr;
76 const KMime::Headers::Subject *const subject = message->subject(false);
77 if (subject) {
78 subjectStr = subject->asUnicodeString();
79 if (subjectStr.isEmpty()) {
80 subjectStr = i18n("No Subject");
81 } else {
82 subjectStr = strToHtml(subjectStr, flags);
83 }
84 } else {
85 subjectStr = i18n("No Subject");
86 }
87 return subjectStr;
88}
89
90QString HeaderStyleUtil::subjectDirectionString(KMime::Message *message) const
91{
92 QString subjectDir;
93 if (message->subject(false)) {
94 subjectDir = directionOf(MessageCore::StringUtil::cleanSubject(message));
95 } else {
96 subjectDir = directionOf(i18n("No Subject"));
97 }
98 return subjectDir;
99}
100
101QString HeaderStyleUtil::spamStatus(KMime::Message *message) const
102{
103 QString spamHTML;
104 const SpamScores scores = SpamHeaderAnalyzer::getSpamScores(message);
105
106 for (SpamScores::const_iterator it = scores.constBegin(), end = scores.constEnd(); it != end; ++it) {
107 spamHTML +=
108 (*it).agent() + QLatin1Char(' ') + drawSpamMeter((*it).error(), (*it).score(), (*it).confidence(), (*it).spamHeader(), (*it).confidenceHeader());
109 }
110 return spamHTML;
111}
112
114HeaderStyleUtil::drawSpamMeter(SpamError spamError, double percent, double confidence, const QString &filterHeader, const QString &confidenceHeader) const
115{
116 static const int meterWidth = 20;
117 static const int meterHeight = 5;
118 QImage meterBar(meterWidth, 1, QImage::Format_Indexed8 /*QImage::Format_RGB32*/);
119 meterBar.setColorCount(24);
120
121 meterBar.setColor(meterWidth + 1, qRgb(255, 255, 255));
122 meterBar.setColor(meterWidth + 2, qRgb(170, 170, 170));
123 if (spamError != noError) { // grey is for errors
124 meterBar.fill(meterWidth + 2);
125 } else {
126 static const unsigned short gradient[meterWidth][3] = {{0, 255, 0}, {27, 254, 0}, {54, 252, 0}, {80, 250, 0}, {107, 249, 0},
127 {135, 247, 0}, {161, 246, 0}, {187, 244, 0}, {214, 242, 0}, {241, 241, 0},
128 {255, 228, 0}, {255, 202, 0}, {255, 177, 0}, {255, 151, 0}, {255, 126, 0},
129 {255, 101, 0}, {255, 76, 0}, {255, 51, 0}, {255, 25, 0}, {255, 0, 0}};
130
131 meterBar.fill(meterWidth + 1);
132 const int max = qMin(meterWidth, static_cast<int>(percent) / 5);
133 for (int i = 0; i < max; ++i) {
134 meterBar.setColor(i + 1, qRgb(gradient[i][0], gradient[i][1], gradient[i][2]));
135 meterBar.setPixel(i, 0, i + 1);
136 }
137 }
138
139 QString titleText;
140 QString confidenceString;
141 if (spamError == noError) {
142 if (confidence >= 0) {
143 confidenceString = QString::number(confidence) + QLatin1StringView("% &nbsp;");
144 titleText = i18n(
145 "%1% probability of being spam with confidence %3%.\n\n"
146 "Full report:\nProbability=%2\nConfidence=%4",
147 QString::number(percent, 'f', 2),
148 filterHeader,
149 confidence,
150 confidenceHeader);
151 } else { // do not show negative confidence
152 confidenceString = QString() + QLatin1StringView("&nbsp;");
153 titleText = i18n(
154 "%1% probability of being spam.\n\n"
155 "Full report:\nProbability=%2",
156 QString::number(percent, 'f', 2),
157 filterHeader);
158 }
159 } else {
160 QString errorMsg;
161 switch (spamError) {
162 case errorExtractingAgentString:
163 errorMsg = i18n("No Spam agent");
164 break;
165 case couldNotConverScoreToFloat:
166 errorMsg = i18n("Spam filter score not a number");
167 break;
168 case couldNotConvertThresholdToFloatOrThresholdIsNegative:
169 errorMsg = i18n("Threshold not a valid number");
170 break;
171 case couldNotFindTheScoreField:
172 errorMsg = i18n("Spam filter score could not be extracted from header");
173 break;
174 case couldNotFindTheThresholdField:
175 errorMsg = i18n("Threshold could not be extracted from header");
176 break;
177 default:
178 errorMsg = i18n("Error evaluating spam score");
179 break;
180 }
181 // report the error in the spam filter
182 titleText = i18n(
183 "%1.\n\n"
184 "Full report:\n%2",
185 errorMsg,
186 filterHeader);
187 }
188 return QStringLiteral("<img src=\"%1\" width=\"%2\" height=\"%3\" style=\"border: 1px solid black;\" title=\"%4\" />")
189 .arg(imgToDataUrl(meterBar), QString::number(meterWidth), QString::number(meterHeight), titleText)
190 + confidenceString;
191}
192
193QString HeaderStyleUtil::imgToDataUrl(const QImage &image) const
194{
195 QByteArray ba;
196 QBuffer buffer(&ba);
197 buffer.open(QIODevice::WriteOnly);
198 image.save(&buffer, "PNG");
199 return QStringLiteral("data:image/%1;base64,%2").arg(QStringLiteral("PNG"), QString::fromLatin1(ba.toBase64()));
200}
201
202QString HeaderStyleUtil::dateStr(const QDateTime &dateTime)
203{
204 return KMime::DateFormatter::formatDate(static_cast<KMime::DateFormatter::FormatType>(MessageCore::MessageCoreSettings::self()->dateFormat()),
205 dateTime.toLocalTime(),
206 MessageCore::MessageCoreSettings::self()->customDateFormat());
207}
208
209QString HeaderStyleUtil::dateShortStr(const QDateTime &dateTime)
210{
212 return formatter.dateString(dateTime);
213}
214
216{
218 const QByteArray &data = hrd->as7BitString(false);
219 mailboxList->from7BitString(data);
220 return mailboxList;
221}
222
223QSharedPointer<KMime::Headers::Generics::MailboxList> HeaderStyleUtil::resentFromList(KMime::Message *message)
224{
225 if (auto hrd = message->headerByType("Resent-From")) {
226 return mailboxesFromHeader(hrd);
227 }
228 return nullptr;
229}
230
231QSharedPointer<KMime::Headers::Generics::MailboxList> HeaderStyleUtil::resentToList(KMime::Message *message)
232{
233 if (auto hrd = message->headerByType("Resent-To")) {
234 return mailboxesFromHeader(hrd);
235 }
236 return nullptr;
237}
238
239void HeaderStyleUtil::updateXFaceSettings(QImage photo, xfaceSettings &settings) const
240{
241 if (!photo.isNull()) {
242 settings.photoWidth = photo.width();
243 settings.photoHeight = photo.height();
244 // scale below 60, otherwise it can get way too large
245 if (settings.photoHeight > 60) {
246 double ratio = (double)settings.photoHeight / (double)settings.photoWidth;
247 settings.photoHeight = 60;
248 settings.photoWidth = (int)(60 / ratio);
249 photo = photo.scaled(settings.photoWidth, settings.photoHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
250 }
251 settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(photo);
252 }
253}
254
255HeaderStyleUtil::xfaceSettings HeaderStyleUtil::xface(const MessageViewer::HeaderStyle *style, KMime::Message *message) const
256{
257 xfaceSettings settings;
258 bool useOtherPhotoSources = false;
259
260 if (style->allowAsync()) {
261 Q_ASSERT(style->nodeHelper());
262 Q_ASSERT(style->sourceObject());
263
264 ContactDisplayMessageMemento *photoMemento =
265 dynamic_cast<ContactDisplayMessageMemento *>(style->nodeHelper()->bodyPartMemento(message, "contactphoto"));
266 if (!photoMemento) {
267 const QString email = QString::fromLatin1(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
268 photoMemento = new ContactDisplayMessageMemento(email);
269 style->nodeHelper()->setBodyPartMemento(message, "contactphoto", photoMemento);
270 QObject::connect(photoMemento, SIGNAL(update(MimeTreeParser::UpdateMode)), style->sourceObject(), SLOT(update(MimeTreeParser::UpdateMode)));
271
272 // clang-format off
273 QObject::connect(photoMemento,
274 SIGNAL(changeDisplayMail(Viewer::DisplayFormatMessage,bool)),
275 style->sourceObject(),
276 SIGNAL(changeDisplayMail(Viewer::DisplayFormatMessage,bool)));
277 // clang-format on
278 }
279
280 if (photoMemento->finished()) {
281 useOtherPhotoSources = true;
282 if (photoMemento->photo().isIntern()) {
283 // get photo data and convert to data: url
284 const QImage photo = photoMemento->photo().data();
285 updateXFaceSettings(photo, settings);
286 } else if (!photoMemento->imageFromUrl().isNull()) {
287 updateXFaceSettings(photoMemento->imageFromUrl(), settings);
288 } else if (!photoMemento->photo().url().isEmpty()) {
289 settings.photoURL = photoMemento->photo().url();
290 if (settings.photoURL.startsWith(QLatin1Char('/'))) {
291 settings.photoURL.prepend(QLatin1StringView("file:"));
292 }
293 } else if (!photoMemento->gravatarPixmap().isNull()) {
294 const QImage photo = photoMemento->gravatarPixmap().toImage();
295 updateXFaceSettings(photo, settings);
296 }
297 } else {
298 // if the memento is not finished yet, use other photo sources instead
299 useOtherPhotoSources = true;
300 }
301 } else {
302 useOtherPhotoSources = true;
303 }
304
305 if (settings.photoURL.isEmpty() && useOtherPhotoSources) {
306 if (auto hrd = message->headerByType("Face")) {
307 // no photo, look for a Face header
308 const QString faceheader = hrd->asUnicodeString();
309 if (!faceheader.isEmpty()) {
310 qCDebug(MESSAGEVIEWER_LOG) << "Found Face: header";
311
312 const QByteArray facestring = faceheader.toUtf8();
313 // Spec says header should be less than 998 bytes
314 // Face: is 5 characters
315 if (facestring.length() < 993) {
316 const QByteArray facearray = QByteArray::fromBase64(facestring);
317
318 QImage faceimage;
319 if (faceimage.loadFromData(facearray, "png")) {
320 // Spec says image must be 48x48 pixels
321 if ((48 == faceimage.width()) && (48 == faceimage.height())) {
322 settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(faceimage);
323 settings.photoWidth = 48;
324 settings.photoHeight = 48;
325 } else {
326 qCDebug(MESSAGEVIEWER_LOG) << "Face: header image is" << faceimage.width() << "by" << faceimage.height() << "not 48x48 Pixels";
327 }
328 } else {
329 qCDebug(MESSAGEVIEWER_LOG) << "Failed to load decoded png from Face: header";
330 }
331 } else {
332 qCDebug(MESSAGEVIEWER_LOG) << "Face: header too long at" << facestring.length();
333 }
334 }
335 }
336 }
337
338 if (settings.photoURL.isEmpty() && useOtherPhotoSources) {
339 if (auto hrd = message->headerByType("X-Face")) {
340 // no photo, look for a X-Face header
341 const QString xfhead = hrd->asUnicodeString();
342 if (!xfhead.isEmpty()) {
344 settings.photoURL = MessageViewer::HeaderStyleUtil::imgToDataUrl(xf.toImage(xfhead));
345 settings.photoWidth = 48;
346 settings.photoHeight = 48;
347 }
348 }
349 }
350
351 return settings;
352}
QImage data() const
bool isIntern() const
QString url() const
static QString formatDate(DateFormatter::FormatType ftype, const QDateTime &t, const QString &data=QString(), bool shortFormat=true)
virtual QByteArray as7BitString(bool withHeaderType=true) const=0
virtual QString asUnicodeString() const=0
QString asUnicodeString() const override
@ LongDate
Locale Long date format, e.g.
@ FancyLongDate
Same as LongDate for dates a week or more ago.
@ ShortDate
Locale Short date format, e.g.
@ FancyShortDate
Same as ShortDate for dates a week or more ago.
This class encapsulates the visual appearance of message headers.
Definition headerstyle.h:47
The KXFace class.
Definition kxface.h:270
QImage toImage(const QString &xface)
creates a pixmap from xface
Definition kxface.cpp:157
static SpamScores getSpamScores(KMime::Message *message)
Extract scores from known anti-spam headers.
KCODECS_EXPORT QByteArray firstEmailAddress(const QByteArray &addresses)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
void update(Part *part, const QByteArray &data, qint64 dataSize)
const QList< QKeySequence > & end()
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
QString cleanSubject(KMime::Message *msg)
Return this mails subject, with all "forward" and "reply" prefixes removed.
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
qsizetype length() const const
QByteArray toBase64(Base64Options options) const const
bool isValid() const const
QDateTime toLocalTime() const const
int height() const const
bool isNull() const const
bool loadFromData(QByteArrayView data, const char *format)
bool save(QIODevice *device, const char *format, int quality) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
int width() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isNull() const const
QImage toImage() const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
bool isRightToLeft() const const
QString number(double n, char format, int precision)
QString & prepend(QChar ch)
QByteArray toUtf8() const const
IgnoreAspectRatio
SmoothTransformation
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.