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

KDE's Doxygen guidelines are available online.