Messagelib

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

KDE's Doxygen guidelines are available online.