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
24using namespace MessageCore;
25
26using namespace MessageViewer;
27
28Q_DECLARE_METATYPE(const KMime::Headers::Generics::AddressList *)
30Q_DECLARE_METATYPE(const KMime::Headers::Generics::MailboxList *)
32Q_DECLARE_METATYPE(QDateTime)
33
34// Read-only introspection of KMime::Headers::Generics::AddressList object.
35namespace KTextTemplate
36{
37template<>
38inline 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);
51 const QString val = MessageCore::StringUtil::emailAddrAsAnchor(object,
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
62template<>
63inline QVariant TypeAccessor<QByteArray &>::lookUp(const QByteArray &object, const QString &property)
64{
65 return object;
66}
67}
69if (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.
90namespace KTextTemplate
91{
92template<>
93inline 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);
106 const QString val = MessageCore::StringUtil::emailAddrAsAnchor(object,
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}
118if (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}
137namespace KTextTemplate
138{
139template<>
140inline 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
163class Q_DECL_HIDDEN HeaderFormatter
164{
165public:
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
172class DefaultHeaderFormatter : public HeaderFormatter
173{
174public:
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
195private:
196 QByteArray header;
197};
198
199class SubjectFormatter : public HeaderFormatter
200{
201public:
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
219class DateFormatter : public HeaderFormatter
220{
221public:
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
235class MessageIdFormatter : public HeaderFormatter
236{
237public:
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
253class AddressHeaderFormatter : public HeaderFormatter
254{
255public:
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
291protected:
292 QByteArray header;
293};
294
295class MessageViewer::GrantleeHeaderFormatter::GrantleeHeaderFormatterPrivate
296{
297public:
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
348GrantleeHeaderFormatter::GrantleeHeaderFormatter()
349 : d(new GrantleeHeaderFormatter::GrantleeHeaderFormatterPrivate)
350{
351}
352
353GrantleeHeaderFormatter::~GrantleeHeaderFormatter() = default;
354
355QString 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
377QString 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
392QString 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}
QBrush background(BackgroundRole=NormalBackground) const
int currentSize(KIconLoader::Group group) const
static KIconLoader * global()
virtual QString asUnicodeString() const=0
void addTemplateLoader(QSharedPointer< AbstractTemplateLoader > loader)
QString errorString() const
QString render(Context *c) const
The GrantleeHeaderFormatter class.
The HeaderStyleUtil class.
@ 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
QString i18n(const char *text, const TYPE &arg...)
#define KTEXTTEMPLATE_END_LOOKUP
#define KTEXTTEMPLATE_BEGIN_LOOKUP(Type)
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QString name(StandardShortcut id)
const QColor & color() const const
const char * constData() const const
QByteArray & insert(qsizetype i, QByteArrayView data)
QByteArray & remove(qsizetype pos, qsizetype len)
QString name(NameFormat format) const const
bool isRightToLeft()
QString arg(Args &&... args) const const
QString fromUtf8(QByteArrayView str)
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QUrl fromLocalFile(const QString &localFile)
QString url(FormattingOptions options) const const
QVariant fromValue(T &&value)
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.