Messagelib

grantleeheaderformatter.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 "grantleeheaderformatter.h"
8 #include "headerstyle_util.h"
9 #include "settings/messageviewersettings.h"
10 #include "utils/iconnamecache.h"
11 
12 #include <MessageCore/StringUtil>
13 #include <MimeTreeParser/NodeHelper>
14 
15 #include <KMime/DateFormatter>
16 #include <KMime/KMimeMessage>
17 
18 #include <KColorScheme>
19 #include <KIconLoader>
20 #include <KLocalizedString>
21 #include <KTextTemplate/Engine>
22 #include <KTextTemplate/MetaType>
23 
24 using namespace MessageCore;
25 
26 using namespace MessageViewer;
27 
28 Q_DECLARE_METATYPE(const KMime::Headers::Generics::AddressList *)
30 Q_DECLARE_METATYPE(const KMime::Headers::Generics::MailboxList *)
32 Q_DECLARE_METATYPE(QDateTime)
33 
34 // Read-only introspection of KMime::Headers::Generics::AddressList object.
35 namespace KTextTemplate
36 {
37 template<>
38 inline QVariant TypeAccessor<const KMime::Headers::Generics::AddressList *>::lookUp(const KMime::Headers::Generics::AddressList *const object,
39  const QString &property)
40 {
41  if (property == QLatin1StringView("nameOnly")) {
42  return StringUtil::emailAddrAsAnchor(object, StringUtil::DisplayNameOnly);
43  } else if (property == QLatin1StringView("isSet")) {
44  return !object->asUnicodeString().isEmpty();
45  } else if (property == QLatin1StringView("fullAddress")) {
46  return StringUtil::emailAddrAsAnchor(object, StringUtil::DisplayFullAddress);
47  } else if (property == QLatin1StringView("str")) {
48  return object->asUnicodeString();
49  } else if (property.startsWith(QLatin1StringView("expandable"))) {
50  const auto &name = property.mid(10);
52  MessageCore::StringUtil::DisplayFullAddress,
53  QString(),
54  MessageCore::StringUtil::ShowLink,
55  MessageCore::StringUtil::ExpandableAddresses,
56  QStringLiteral("Full") + name + QStringLiteral("AddressList"));
57  return val;
58  }
59  return {};
60 }
61 
62 template<>
63 inline QVariant TypeAccessor<QByteArray &>::lookUp(const QByteArray &object, const QString &property)
64 {
65  return object;
66 }
67 }
69 if (property == QLatin1StringView("nameOnly")) {
70  return StringUtil::emailAddrAsAnchor(object.data(), StringUtil::DisplayNameOnly);
71 } else if (property == QLatin1StringView("isSet")) {
72  return !object->asUnicodeString().isEmpty();
73 } else if (property == QLatin1StringView("fullAddress")) {
74  return StringUtil::emailAddrAsAnchor(object.data(), StringUtil::DisplayFullAddress);
75 } else if (property == QLatin1StringView("str")) {
76  return object->asUnicodeString();
77 } else if (property.startsWith(QLatin1StringView("expandable"))) {
78  const auto &name = property.mid(10);
79  const QString val = MessageCore::StringUtil::emailAddrAsAnchor(object.data(),
80  MessageCore::StringUtil::DisplayFullAddress,
81  QString(),
82  MessageCore::StringUtil::ShowLink,
83  MessageCore::StringUtil::ExpandableAddresses,
84  QStringLiteral("Full") + name + QStringLiteral("AddressList"));
85  return val;
86 }
88 
89 // Read-only introspection of KMime::Headers::Generics::MailboxList object.
90 namespace KTextTemplate
91 {
92 template<>
93 inline QVariant TypeAccessor<const KMime::Headers::Generics::MailboxList *>::lookUp(const KMime::Headers::Generics::MailboxList *const object,
94  const QString &property)
95 {
96  if (property == QLatin1StringView("nameOnly")) {
97  return StringUtil::emailAddrAsAnchor(object, StringUtil::DisplayNameOnly);
98  } else if (property == QLatin1StringView("isSet")) {
99  return !object->asUnicodeString().isEmpty();
100  } else if (property == QLatin1StringView("fullAddress")) {
101  return StringUtil::emailAddrAsAnchor(object, StringUtil::DisplayFullAddress);
102  } else if (property == QLatin1StringView("str")) {
103  return object->asUnicodeString();
104  } else if (property.startsWith(QLatin1StringView("expandable"))) {
105  const auto &name = property.mid(10);
107  MessageCore::StringUtil::DisplayFullAddress,
108  QString(),
109  MessageCore::StringUtil::ShowLink,
110  MessageCore::StringUtil::ExpandableAddresses,
111  QStringLiteral("Full") + name + QStringLiteral("AddressList"));
112  return val;
113  }
114  return {};
115 }
116 }
118 if (property == QLatin1StringView("nameOnly")) {
119  return StringUtil::emailAddrAsAnchor(object.data(), StringUtil::DisplayNameOnly);
120 } else if (property == QLatin1StringView("isSet")) {
121  return !object->asUnicodeString().isEmpty();
122 } else if (property == QLatin1StringView("fullAddress")) {
123  return StringUtil::emailAddrAsAnchor(object.data(), StringUtil::DisplayFullAddress);
124 } else if (property == QLatin1StringView("str")) {
125  return object->asUnicodeString();
126 } else if (property.startsWith(QLatin1StringView("expandable"))) {
127  const auto &name = property.mid(10);
128  const QString val = MessageCore::StringUtil::emailAddrAsAnchor(object.data(),
129  MessageCore::StringUtil::DisplayFullAddress,
130  QString(),
131  MessageCore::StringUtil::ShowLink,
132  MessageCore::StringUtil::ExpandableAddresses,
133  QStringLiteral("Full") + name + QStringLiteral("AddressList"));
134  return val;
135 }
137 namespace KTextTemplate
138 {
139 template<>
140 inline QVariant TypeAccessor<QDateTime &>::lookUp(const QDateTime &object, const QString &property)
141 {
143  if (property == QLatin1StringView("str")) {
144  return HeaderStyleUtil::dateStr(object);
145  } else if (property == QLatin1StringView("short")) {
147  } else if (property == QLatin1StringView("long")) {
149  } else if (property == QLatin1StringView("fancylong")) {
151  } else if (property == QLatin1StringView("fancyshort")) {
153  } else if (property == QLatin1StringView("localelong")) {
155  } else {
156  return {};
157  }
158 
159  return HeaderStyleUtil::strToHtml(HeaderStyleUtil::dateString(object, dateFormat));
160 }
161 }
162 
163 class Q_DECL_HIDDEN HeaderFormatter
164 {
165 public:
166  virtual ~HeaderFormatter() = default;
167 
168  virtual QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) = 0;
169  virtual QString i18nName() = 0;
170 };
171 
172 class DefaultHeaderFormatter : public HeaderFormatter
173 {
174 public:
175  DefaultHeaderFormatter(const QByteArray &h)
176  : header(h)
177  {
178  }
179 
180  QString i18nName() override
181  {
182  if (header == "list-id") {
183  return i18n("List-Id:");
184  } else {
185  return {};
186  }
187  }
188 
189  QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) override
190  {
191  Q_UNUSED(showEmoticons);
192  return nodeHelper->mailHeaderAsBase(header.constData(), message)->asUnicodeString();
193  }
194 
195 private:
196  QByteArray header;
197 };
198 
199 class SubjectFormatter : public HeaderFormatter
200 {
201 public:
202  QString i18nName() override
203  {
204  return i18n("Subject:");
205  }
206 
207  QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) override
208  {
210  if (showEmoticons) {
212  }
213  const auto subjectStr = nodeHelper->mailHeaderAsBase("subject", message)->asUnicodeString();
214 
215  return HeaderStyleUtil::strToHtml(subjectStr, flags);
216  }
217 };
218 
219 class DateFormatter : public HeaderFormatter
220 {
221 public:
222  QString i18nName() override
223  {
224  return i18n("Date:");
225  }
226 
227  QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) override
228  {
229  Q_UNUSED(showEmoticons);
230  const auto value = nodeHelper->dateHeader(message);
231  return value;
232  }
233 };
234 
235 class MessageIdFormatter : public HeaderFormatter
236 {
237 public:
238  QString i18nName() override
239  {
240  return i18n("Message-Id:");
241  }
242 
243  QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) override
244  {
245  const auto messageIdHeader = nodeHelper->mailHeaderAsBase("Message-Id", message);
246  if (messageIdHeader != nullptr) {
247  return static_cast<const KMime::Headers::MessageID *>(messageIdHeader)->identifier();
248  }
249  return {};
250  }
251 };
252 
253 class AddressHeaderFormatter : public HeaderFormatter
254 {
255 public:
256  AddressHeaderFormatter(const QByteArray &h)
257  : header(h)
258  {
259  }
260 
261  QString i18nName() override
262  {
263  if (header == "to") {
264  return i18n("To:");
265  } else if (header == "reply-To") {
266  return i18n("Reply To:");
267  } else if (header == "cc") {
268  return i18n("CC:");
269  } else if (header == "bcc") {
270  return i18n("BCC:");
271  } else if (header == "from") {
272  return i18n("From:");
273  } else if (header == "sender") {
274  return i18n("Sender:");
275  } else if (header == "resent-From") {
276  return i18n("resent from:");
277  } else if (header == "resent-To") {
278  return i18n("resent to:");
279  } else {
280  return {};
281  }
282  }
283 
284  QVariant format(KMime::Message *message, MimeTreeParser::NodeHelper *nodeHelper, bool showEmoticons) override
285  {
286  Q_UNUSED(showEmoticons);
287  const auto value = nodeHelper->mailHeaderAsAddressList(header.constData(), message);
288  return QVariant::fromValue(value);
289  }
290 
291 protected:
292  QByteArray header;
293 };
294 
295 class MessageViewer::GrantleeHeaderFormatter::GrantleeHeaderFormatterPrivate
296 {
297 public:
298  GrantleeHeaderFormatterPrivate()
299  : engine(new KTextTemplate::Engine)
300  {
301  KTextTemplate::registerMetaType<const KMime::Headers::Generics::AddressList *>();
302  KTextTemplate::registerMetaType<const KMime::Headers::Generics::MailboxList *>();
303  KTextTemplate::registerMetaType<QSharedPointer<KMime::Headers::Generics::MailboxList>>();
304  KTextTemplate::registerMetaType<QSharedPointer<KMime::Headers::Generics::AddressList>>();
305  KTextTemplate::registerMetaType<QDateTime>();
306  KTextTemplate::registerMetaType<QByteArray>();
309  engine->addTemplateLoader(templateLoader);
310 
311  QList<QByteArray> addressHeaders;
312  addressHeaders << "to"
313  << "reply-To"
314  << "cc"
315  << "bcc"
316  << "from"
317  << "sender"
318  << "resent-From"
319  << "resent-To";
320 
321  for (const auto &header : std::as_const(addressHeaders)) {
322  registerHeaderFormatter(header, QSharedPointer<HeaderFormatter>(new AddressHeaderFormatter(header)));
323  }
324 
325  registerHeaderFormatter("subject", QSharedPointer<HeaderFormatter>(new SubjectFormatter()));
326  registerHeaderFormatter("date", QSharedPointer<HeaderFormatter>(new DateFormatter()));
327  registerHeaderFormatter("Message-Id", QSharedPointer<HeaderFormatter>(new MessageIdFormatter()));
328  }
329 
330  ~GrantleeHeaderFormatterPrivate()
331  {
332  delete engine;
333  }
334 
335  void registerHeaderFormatter(const QByteArray &header, QSharedPointer<HeaderFormatter> formatter)
336  {
337  headerFormatter[header] = formatter;
338  }
340  KTextTemplate::Engine *const engine;
342  MessageViewer::HeaderStyleUtil headerStyleUtil;
343  QColor activeColor;
344 
345  int iconSize;
346 };
347 
348 GrantleeHeaderFormatter::GrantleeHeaderFormatter()
349  : d(new GrantleeHeaderFormatter::GrantleeHeaderFormatterPrivate)
350 {
351 }
352 
353 GrantleeHeaderFormatter::~GrantleeHeaderFormatter() = default;
354 
355 QString GrantleeHeaderFormatter::toHtml(const GrantleeHeaderFormatter::GrantleeHeaderFormatterSettings &settings) const
356 {
358  if (!settings.theme.isValid()) {
359  errorMessage = i18n("Grantlee theme \"%1\" is not valid.", settings.theme.name());
360  return errorMessage;
361  }
362  d->templateLoader->setTemplateDirs(QStringList() << settings.theme.absolutePath());
363  KTextTemplate::Template headerTemplate = d->engine->loadByName(settings.theme.themeFilename());
364  if (headerTemplate->error()) {
365  errorMessage = headerTemplate->errorString();
366  return errorMessage;
367  }
368  return format(settings.theme.absolutePath(),
369  headerTemplate,
370  settings.theme.displayExtraVariables(),
371  settings.isPrinting,
372  settings.style,
373  settings.message,
374  settings.showEmoticons);
375 }
376 
377 QString GrantleeHeaderFormatter::toHtml(const QStringList &displayExtraHeaders,
378  const QString &absolutPath,
379  const QString &filename,
380  const MessageViewer::HeaderStyle *style,
381  KMime::Message *message,
382  bool isPrinting) const
383 {
384  d->templateLoader->setTemplateDirs(QStringList() << absolutPath);
385  KTextTemplate::Template headerTemplate = d->engine->loadByName(filename);
386  if (headerTemplate->error()) {
387  return headerTemplate->errorString();
388  }
389  return format(absolutPath, headerTemplate, displayExtraHeaders, isPrinting, style, message);
390 }
391 
392 QString GrantleeHeaderFormatter::format(const QString &absolutePath,
393  const KTextTemplate::Template &headerTemplate,
394  const QStringList &displayExtraHeaders,
395  bool isPrinting,
396  const MessageViewer::HeaderStyle *style,
397  KMime::Message *message,
398  bool showEmoticons) const
399 {
400  QVariantHash headerObject;
401  const auto nodeHelper = style->nodeHelper();
402 
403  // However, the direction of the message subject within the header is
404  // determined according to the contents of the subject itself. Since
405  // the "Re:" and "Fwd:" prefixes would always cause the subject to be
406  // considered left-to-right, they are ignored when determining its
407  // direction.
408  const QString absoluteThemePath = QUrl::fromLocalFile(absolutePath + QLatin1Char('/')).url();
409  headerObject.insert(QStringLiteral("absoluteThemePath"), absoluteThemePath);
410  headerObject.insert(QStringLiteral("applicationDir"), QApplication::isRightToLeft() ? QStringLiteral("rtl") : QStringLiteral("ltr"));
411 
412  // TODO: use correct subject from nodeHelper->mailHeader
413  headerObject.insert(QStringLiteral("subjectDir"), d->headerStyleUtil.subjectDirectionString(message));
414 
415  QList<QByteArray> defaultHeaders;
416  defaultHeaders << "to"
417  << "reply-To"
418  << "cc"
419  << "bcc"
420  << "from"
421  << "sender"
422  << "resent-From"
423  << "resent-To"
424  << "subject"
425  << "organization"
426  << "list-id"
427  << "date"
428  << "Message-Id";
429 
430  for (const auto &header : std::as_const(defaultHeaders)) {
432  if (d->headerFormatter.contains(header)) {
433  formatter = d->headerFormatter.value(header);
434  } else {
435  formatter = QSharedPointer<HeaderFormatter>(new DefaultHeaderFormatter(header));
436  }
437  const auto i18nName = formatter->i18nName();
438  const auto objectName = QString::fromUtf8(header).remove(QLatin1Char('-'));
439  if (nodeHelper->hasMailHeader(header.constData(), message)) {
440  const auto value = formatter->format(message, nodeHelper, showEmoticons);
441  headerObject.insert(objectName, value);
442  }
443  if (!i18nName.isEmpty()) {
444  headerObject.insert(objectName + QStringLiteral("i18n"), i18nName);
445  }
446  }
447 
448  if (!nodeHelper->hasMailHeader("subject", message)) {
449  headerObject.insert(QStringLiteral("subject"), i18n("No Subject"));
450  }
451 
452  const QString spamHtml = d->headerStyleUtil.spamStatus(message);
453  if (!spamHtml.isEmpty()) {
454  headerObject.insert(QStringLiteral("spamstatusi18n"), i18n("Spam Status:"));
455  headerObject.insert(QStringLiteral("spamHTML"), spamHtml);
456  }
457 
458  if (!style->vCardName().isEmpty()) {
459  headerObject.insert(QStringLiteral("vcardname"), style->vCardName());
460  }
461 
462  if (isPrinting) {
463  // provide a bit more left padding when printing
464  // kolab/issue3254 (printed mail cut at the left side)
465  // Use it just for testing if we are in printing mode
466  headerObject.insert(QStringLiteral("isprinting"), i18n("Printing mode"));
467  headerObject.insert(QStringLiteral("printmode"), QStringLiteral("printmode"));
468  } else {
469  headerObject.insert(QStringLiteral("screenmode"), QStringLiteral("screenmode"));
470  }
471 
472  // colors depend on if it is encapsulated or not
473  QColor fontColor(Qt::white);
474  QString linkColor = QStringLiteral("white");
475 
476  if (!d->activeColor.isValid()) {
478  }
479  QColor activeColorDark = d->activeColor.darker(130);
480  // reverse colors for encapsulated
481  if (!style->isTopLevel()) {
482  activeColorDark = d->activeColor.darker(50);
483  fontColor = QColor(Qt::black);
484  linkColor = QStringLiteral("black");
485  }
486 
487  // 3D borders
488  headerObject.insert(QStringLiteral("activecolordark"), activeColorDark.name());
489  headerObject.insert(QStringLiteral("fontcolor"), fontColor.name());
490  headerObject.insert(QStringLiteral("linkcolor"), linkColor);
491 
492  MessageViewer::HeaderStyleUtil::xfaceSettings xface = d->headerStyleUtil.xface(style, message);
493  if (!xface.photoURL.isEmpty()) {
494  headerObject.insert(QStringLiteral("photowidth"), xface.photoWidth);
495  headerObject.insert(QStringLiteral("photoheight"), xface.photoHeight);
496  headerObject.insert(QStringLiteral("photourl"), xface.photoURL);
497  }
498 
499  for (QString header : std::as_const(displayExtraHeaders)) {
500  const QByteArray baHeader = header.toLocal8Bit();
501  if (auto hrd = message->headerByType(baHeader.constData())) {
502  // Grantlee doesn't support '-' in variable name => remove it.
503  header.remove(QLatin1Char('-'));
504  headerObject.insert(header, hrd->asUnicodeString());
505  }
506  }
507 
508  headerObject.insert(QStringLiteral("vcardi18n"), i18n("[vcard]"));
509  headerObject.insert(QStringLiteral("readOnlyMessage"), style->readOnlyMessage());
510 
511  const QString attachmentHtml = style->attachmentHtml();
512  const bool messageHasAttachment = !attachmentHtml.isEmpty();
513  headerObject.insert(QStringLiteral("hasAttachment"), messageHasAttachment);
514  headerObject.insert(QStringLiteral("attachmentHtml"), attachmentHtml);
515  headerObject.insert(QStringLiteral("attachmentI18n"), i18n("Attachments:"));
516 
517  if (messageHasAttachment) {
518  const QString iconPath = MessageViewer::IconNameCache::instance()->iconPath(QStringLiteral("mail-attachment"), KIconLoader::Toolbar);
519  const QString html =
520  QStringLiteral("<img height=\"%2\" width=\"%2\" src=\"%1\"></a>").arg(QUrl::fromLocalFile(iconPath).url(), QString::number(d->iconSize));
521  headerObject.insert(QStringLiteral("attachmentIcon"), html);
522  }
523 
524  const bool messageIsSigned = KMime::isSigned(message);
525  headerObject.insert(QStringLiteral("messageIsSigned"), messageIsSigned);
526  if (messageIsSigned) {
527  const QString iconPath = MessageViewer::IconNameCache::instance()->iconPath(QStringLiteral("mail-signed"), KIconLoader::Toolbar);
528  const QString html =
529  QStringLiteral("<img height=\"%2\" width=\"%2\" src=\"%1\"></a>").arg(QUrl::fromLocalFile(iconPath).url(), QString::number(d->iconSize));
530  headerObject.insert(QStringLiteral("signedIcon"), html);
531  }
532 
533  const bool messageIsEncrypted = KMime::isEncrypted(message);
534  headerObject.insert(QStringLiteral("messageIsEncrypted"), messageIsEncrypted);
535  if (messageIsEncrypted) {
536  const QString iconPath = MessageViewer::IconNameCache::instance()->iconPath(QStringLiteral("mail-encrypted"), KIconLoader::Toolbar);
537  const QString html =
538  QStringLiteral("<img height=\"%2\" width=\"%2\" src=\"%1\"></a>").arg(QUrl::fromLocalFile(iconPath).url(), QString::number(d->iconSize));
539  headerObject.insert(QStringLiteral("encryptedIcon"), html);
540  }
541 
542  const bool messageHasSecurityInfo = messageIsEncrypted || messageIsSigned;
543  headerObject.insert(QStringLiteral("messageHasSecurityInfo"), messageHasSecurityInfo);
544  headerObject.insert(QStringLiteral("messageHasSecurityInfoI18n"), i18n("Security:"));
545 
546  QVariantHash mapping;
547  mapping.insert(QStringLiteral("header"), headerObject);
548  KTextTemplate::Context context(mapping);
549 
550  return headerTemplate->render(&context);
551 }
@ FancyLongDate
Same as LongDate for dates a week or more ago.
The GrantleeHeaderFormatter class.
const QColor & color() const const
const Engine * engine() const
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
QVariant fromValue(const T &value)
@ LongDate
Locale Long date format, e.g.
QString url(QUrl::FormattingOptions options) const const
QString name() const const
QBrush background(BackgroundRole=NormalBackground) const
@ FancyShortDate
Same as ShortDate for dates a week or more ago.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QByteArray & remove(int pos, int len)
QString i18n(const char *text, const TYPE &arg...)
Template loadByName(const QString &name) const
This class encapsulates the visual appearance of message headers.
Definition: headerstyle.h:46
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
static KIconLoader * global()
QString name(StandardAction id)
#define KTEXTTEMPLATE_BEGIN_LOOKUP(Type)
#define KTEXTTEMPLATE_END_LOOKUP
QColor darker(int factor) const const
bool isRightToLeft()
int currentSize(KIconLoader::Group group) const
QString & remove(int position, int n)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString errorString() const
@ ShortDate
Locale Short date format, e.g.
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString & insert(int position, QChar ch)
virtual QString asUnicodeString() const=0
QString render(Context *c) const
MESSAGECORE_EXPORT QString emailAddrAsAnchor(const QList< KMime::Types::Mailbox > &mailboxList, Display display=DisplayNameOnly, const QString &cssStyle=QString(), Link link=ShowLink, AddressMode expandable=FullAddresses, const QString &fieldName=QString(), int collapseNumber=4)
Same as the above, only for Mailbox::List types.
QString mid(int position, int n) const const
The HeaderStyleUtil class.
QByteArray & insert(int i, char ch)
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.