Messagelib

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

KDE's Doxygen guidelines are available online.