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 
27 using namespace MessageCore;
28 
29 using namespace MessageViewer;
30 //
31 // Convenience functions:
32 //
33 HeaderStyleUtil::HeaderStyleUtil() = default;
34 
35 QString HeaderStyleUtil::directionOf(const QString &str) const
36 {
37  return str.isRightToLeft() ? QStringLiteral("rtl") : QStringLiteral("ltr");
38 }
39 
40 QString HeaderStyleUtil::strToHtml(const QString &str, KTextToHTML::Options flags)
41 {
42  return KTextToHTML::convertToHtml(str, flags, 4096, 512);
43 }
44 
45 // Prepare the date string
46 QString HeaderStyleUtil::dateString(KMime::Message *message, HeaderStyleUtilDateFormat dateFormat)
47 {
48  return dateString(message->date()->dateTime(), dateFormat);
49 }
50 
51 QString 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 
73 QString 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 
90 QString 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 
101 QString 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 
113 QString
114 HeaderStyleUtil::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 
193 QString 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 
202 QString 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 
209 QString 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 
223 QSharedPointer<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 
231 QSharedPointer<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 
239 void 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 
255 HeaderStyleUtil::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 }
QString cleanSubject(KMime::Message *msg)
Return this mails subject, with all "forward" and "reply" prefixes removed.
Definition: stringutil.cpp:722
QString number(int n, int base)
int height() const const
QString asUnicodeString() const override
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QList::const_iterator constBegin() const const
QByteArray toBase64(QByteArray::Base64Options options) const const
bool loadFromData(const uchar *data, int len, const char *format)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
KCODECS_EXPORT QByteArray firstEmailAddress(const QByteArray &addresses)
IgnoreAspectRatio
QString i18n(const char *text, const TYPE &arg...)
This class encapsulates the visual appearance of message headers.
Definition: headerstyle.h:46
bool isEmpty() const const
QByteArray toUtf8() const const
bool isNull() const const
QByteArray fromBase64(const QByteArray &base64, QByteArray::Base64Options options)
QImage toImage(const QString &xface)
creates a pixmap from xface
Definition: kxface.cpp:157
QDateTime toLocalTime() const const
QList::const_iterator constEnd() const const
QString fromLatin1(const char *str, int size)
virtual QByteArray as7BitString(bool withHeaderType=true) const=0
void update(Part *part, const QByteArray &data, qint64 dataSize)
bool isValid() const const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
bool isRightToLeft() const const
virtual QString asUnicodeString() const=0
int length() const const
The KXFace class.
Definition: kxface.h:269
SmoothTransformation
bool save(const QString &fileName, const char *format, int quality) const const
static QString formatDate(DateFormatter::FormatType ftype, const QDateTime &t, const QString &data=QString(), bool shortFormat=true)
int width() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:55:20 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.