Akonadi Notes

noteutils.cpp
1/* This file is part of the KDE project
2 SPDX-FileCopyrightText: 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "noteutils.h"
8
9#include "akonadi_notes_debug.h"
10#include <KLocalizedString>
11#include <KMime/Message>
12#include <QDateTime>
13
14#include <QRegularExpression>
15#include <QString>
16#include <QUuid>
17#include <qdom.h>
18
19namespace Akonadi
20{
21namespace NoteUtils
22{
23#define X_NOTES_UID_HEADER "X-Akonotes-UID"
24#define X_NOTES_LASTMODIFIED_HEADER "X-Akonotes-LastModified"
25#define X_NOTES_CLASSIFICATION_HEADER "X-Akonotes-Classification"
26#define X_NOTES_CUSTOM_HEADER "X-Akonotes-Custom"
27
28#define CLASSIFICATION_PUBLIC QStringLiteral("Public")
29#define CLASSIFICATION_PRIVATE QStringLiteral("Private")
30#define CLASSIFICATION_CONFIDENTIAL QStringLiteral("Confidential")
31
32#define X_NOTES_URL_HEADER "X-Akonotes-Url"
33#define X_NOTES_LABEL_HEADER "X-Akonotes-Label"
34#define X_NOTES_CONTENTTYPE_HEADER "X-Akonotes-Type"
35#define CONTENT_TYPE_CUSTOM QStringLiteral("custom")
36#define CONTENT_TYPE_ATTACHMENT QStringLiteral("attachment")
37
38#define ENCODING "utf-8"
39
40class AttachmentPrivate
41{
42public:
43 AttachmentPrivate(const QUrl &url, const QString &mimetype)
44 : mUrl(url)
45 , mMimetype(mimetype)
46 {
47 }
48
49 AttachmentPrivate(const QByteArray &data, const QString &mimetype)
50 : mData(data)
51 , mMimetype(mimetype)
52 {
53 }
54
55 AttachmentPrivate(const AttachmentPrivate &other)
56 {
57 *this = other;
58 }
59
60 QUrl mUrl;
61 QByteArray mData;
62 bool mDataBase64Encoded = false;
63 QString mMimetype;
64 QString mLabel;
65 QString mContentID;
66};
67
69 : d_ptr(new AttachmentPrivate(QUrl(), QString()))
70{
71}
72
73Attachment::Attachment(const QUrl &url, const QString &mimetype)
74 : d_ptr(new AttachmentPrivate(url, mimetype))
75{
76}
77
78Attachment::Attachment(const QByteArray &data, const QString &mimetype)
79 : d_ptr(new AttachmentPrivate(data, mimetype))
80{
81}
82
84 : d_ptr(new AttachmentPrivate(*other.d_func()))
85{
86}
87
88Attachment::~Attachment() = default;
89
90bool Attachment::operator==(const Attachment &a) const
91{
92 Q_D(const Attachment);
93 if (d->mUrl.isEmpty()) {
94 return d->mUrl == a.d_func()->mUrl && d->mDataBase64Encoded == a.d_func()->mDataBase64Encoded && d->mMimetype == a.d_func()->mMimetype
95 && d->mContentID == a.d_func()->mContentID && d->mLabel == a.d_func()->mLabel;
96 }
97 return d->mData == a.d_func()->mData && d->mDataBase64Encoded == a.d_func()->mDataBase64Encoded && d->mMimetype == a.d_func()->mMimetype
98 && d->mContentID == a.d_func()->mContentID && d->mLabel == a.d_func()->mLabel;
99}
100
101void Attachment::operator=(const Attachment &a)
102{
103 *d_ptr = *a.d_ptr;
104}
105
107{
108 Q_D(const Attachment);
109 return d->mUrl;
110}
111
113{
114 Q_D(const Attachment);
115 return d->mData;
116}
117
119{
121 d->mDataBase64Encoded = true;
122}
123
125{
126 Q_D(const Attachment);
127 return d->mDataBase64Encoded;
128}
129
130void Attachment::setContentID(const QString &contentID)
131{
133 d->mContentID = contentID;
134}
135
137{
138 Q_D(const Attachment);
139 return d->mContentID;
140}
141
143{
144 Q_D(const Attachment);
145 return d->mMimetype;
146}
147
149{
151 d->mLabel = label;
152}
153
155{
156 Q_D(const Attachment);
157 return d->mLabel;
158}
159
160class NoteMessageWrapperPrivate
161{
162public:
163 NoteMessageWrapperPrivate() = default;
164
165 NoteMessageWrapperPrivate(const KMime::MessagePtr &msg)
166 {
167 readMimeMessage(msg);
168 }
169
170 void readMimeMessage(const KMime::MessagePtr &msg);
171
172 KMime::Content *createCustomPart() const;
173 void parseCustomPart(KMime::Content *);
174
175 KMime::Content *createAttachmentPart(const Attachment &) const;
176 void parseAttachmentPart(KMime::Content *);
177
178 QString uid;
179 QString title;
180 QString text;
181 QString from;
182 QDateTime creationDate;
183 QDateTime lastModifiedDate;
185 QList<Attachment> attachments;
186 NoteMessageWrapper::Classification classification = NoteMessageWrapper::Public;
187 Qt::TextFormat textFormat = Qt::PlainText;
188};
189
190void NoteMessageWrapperPrivate::readMimeMessage(const KMime::MessagePtr &msg)
191{
192 if (!msg.data()) {
193 qCWarning(AKONADINOTES_LOG) << "Empty message";
194 return;
195 }
196 title = msg->subject(true)->asUnicodeString();
197 text = msg->mainBodyPart()->decodedText(true); // remove trailing whitespace, so we get rid of " " in empty notes
198 if (msg->from(false)) {
199 from = msg->from(false)->asUnicodeString();
200 }
201 creationDate = msg->date(true)->dateTime();
202 if (msg->mainBodyPart()->contentType(false) && msg->mainBodyPart()->contentType()->mimeType() == "text/html") {
203 textFormat = Qt::RichText;
204 }
205
206 if (KMime::Headers::Base *lastmod = msg->headerByType(X_NOTES_LASTMODIFIED_HEADER)) {
207 lastModifiedDate = QDateTime::fromString(lastmod->asUnicodeString(), Qt::RFC2822Date);
208 if (!lastModifiedDate.isValid()) {
209 qCWarning(AKONADINOTES_LOG) << "failed to parse lastModifiedDate";
210 }
211 }
212
213 if (KMime::Headers::Base *uidHeader = msg->headerByType(X_NOTES_UID_HEADER)) {
214 uid = uidHeader->asUnicodeString();
215 }
216
217 if (KMime::Headers::Base *classificationHeader = msg->headerByType(X_NOTES_CLASSIFICATION_HEADER)) {
218 const QString &c = classificationHeader->asUnicodeString();
219 if (c == CLASSIFICATION_PRIVATE) {
220 classification = NoteMessageWrapper::Private;
221 } else if (c == CLASSIFICATION_CONFIDENTIAL) {
222 classification = NoteMessageWrapper::Confidential;
223 }
224 }
225
226 const auto list = msg->contents();
227 for (KMime::Content *c : list) {
228 if (KMime::Headers::Base *typeHeader = c->headerByType(X_NOTES_CONTENTTYPE_HEADER)) {
229 const QString &type = typeHeader->asUnicodeString();
230 if (type == CONTENT_TYPE_CUSTOM) {
231 parseCustomPart(c);
232 } else if (type == CONTENT_TYPE_ATTACHMENT) {
233 parseAttachmentPart(c);
234 } else {
235 qCWarning(AKONADINOTES_LOG) << "unknown type " << type;
236 }
237 }
238 }
239}
240
241QDomDocument createXMLDocument()
242{
243 QDomDocument document;
244 const QString p = QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"");
245 document.appendChild(document.createProcessingInstruction(QStringLiteral("xml"), p));
246 return document;
247}
248
249QDomDocument loadDocument(KMime::Content *part)
250{
251 QDomDocument document;
252 const QDomDocument::ParseResult parseResult = document.setContent(part->body());
253 if (!parseResult) {
254 qCWarning(AKONADINOTES_LOG) << part->body();
255 qWarning("Error loading document: %s, line %lld, column %lld", qPrintable(parseResult.errorMessage), parseResult.errorLine, parseResult.errorColumn);
256 return {};
257 }
258 return document;
259}
260
261KMime::Content *NoteMessageWrapperPrivate::createCustomPart() const
262{
263 auto content = new KMime::Content();
264 auto header = new KMime::Headers::Generic(X_NOTES_CONTENTTYPE_HEADER);
265 header->fromUnicodeString(CONTENT_TYPE_CUSTOM);
266 content->appendHeader(header);
267 QDomDocument document = createXMLDocument();
268 QDomElement element = document.createElement(QStringLiteral("custom"));
269 element.setAttribute(QStringLiteral("version"), QStringLiteral("1.0"));
271 for (QMap<QString, QString>::const_iterator it = custom.begin(); it != end; ++it) {
272 QDomElement e = element.ownerDocument().createElement(it.key());
273 QDomText t = element.ownerDocument().createTextNode(it.value());
274 e.appendChild(t);
275 element.appendChild(e);
276 document.appendChild(element);
277 }
278 content->setBody(document.toString().toLatin1());
279 return content;
280}
281
282void NoteMessageWrapperPrivate::parseCustomPart(KMime::Content *part)
283{
284 QDomDocument document = loadDocument(part);
285 if (document.isNull()) {
286 return;
287 }
288 QDomElement top = document.documentElement();
289 if (top.tagName() != QLatin1StringView("custom")) {
290 qWarning("XML error: Top tag was %s instead of the expected custom", top.tagName().toLatin1().data());
291 return;
292 }
293
294 for (QDomNode n = top.firstChild(); !n.isNull(); n = n.nextSibling()) {
295 if (n.isElement()) {
296 QDomElement e = n.toElement();
297 custom.insert(e.tagName(), e.text());
298 } else {
299 qCDebug(AKONADINOTES_LOG) << "Node is not an element";
300 Q_ASSERT(false);
301 }
302 }
303}
304
305KMime::Content *NoteMessageWrapperPrivate::createAttachmentPart(const Attachment &a) const
306{
307 auto content = new KMime::Content();
308 auto header = new KMime::Headers::Generic(X_NOTES_CONTENTTYPE_HEADER);
309 header->fromUnicodeString(CONTENT_TYPE_ATTACHMENT);
310 content->appendHeader(header);
311 if (a.url().isValid()) {
312 header = new KMime::Headers::Generic(X_NOTES_URL_HEADER);
313 header->fromUnicodeString(a.url().toString());
314 content->appendHeader(header);
315 } else {
316 if (a.dataBase64Encoded()) {
317 content->setEncodedBody(a.data());
318 } else {
319 content->setBody(a.data());
320 }
321 }
322 content->contentType()->setMimeType(a.mimetype().toLatin1());
323 if (!a.label().isEmpty()) {
324 header = new KMime::Headers::Generic(X_NOTES_LABEL_HEADER);
325 header->fromUnicodeString(a.label());
326 content->appendHeader(header);
327 }
328 content->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
329 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
330 content->contentDisposition()->setFilename(QStringLiteral("attachment"));
331 if (!a.contentID().isEmpty()) {
332 content->contentID()->setIdentifier(a.contentID().toLatin1());
333 }
334 return content;
335}
336
337void NoteMessageWrapperPrivate::parseAttachmentPart(KMime::Content *part)
338{
340 if (KMime::Headers::Base *labelHeader = part->headerByType(X_NOTES_LABEL_HEADER)) {
341 label = labelHeader->asUnicodeString();
342 }
343 if (KMime::Headers::Base *header = part->headerByType(X_NOTES_URL_HEADER)) {
344 Attachment attachment(QUrl(header->asUnicodeString()), QLatin1StringView(part->contentType()->mimeType()));
345 attachment.setLabel(label);
346 attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
347 attachments.append(attachment);
348 } else {
349 Attachment attachment(part->decodedContent(), QLatin1StringView(part->contentType()->mimeType()));
350 attachment.setLabel(label);
351 attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
352 attachments.append(attachment);
353 }
354}
355
356NoteMessageWrapper::NoteMessageWrapper()
357 : d_ptr(new NoteMessageWrapperPrivate())
358{
359}
360
361NoteMessageWrapper::NoteMessageWrapper(const KMime::MessagePtr &msg)
362 : d_ptr(new NoteMessageWrapperPrivate(msg))
363{
364}
365
366NoteMessageWrapper::~NoteMessageWrapper() = default;
367
369{
370 Q_D(const NoteMessageWrapper);
372
373 QString title = i18nc("The default name for new notes.", "New Note");
374 if (!d->title.isEmpty()) {
375 title = d->title;
376 }
377 // Need a non-empty body part so that the serializer regards this as a valid message.
378 QString text = QStringLiteral(" ");
379 if (!d->text.isEmpty()) {
380 text = d->text;
381 }
382
384 if (d->creationDate.isValid()) {
385 creationDate = d->creationDate;
386 }
387
389 if (d->lastModifiedDate.isValid()) {
390 lastModifiedDate = d->lastModifiedDate;
391 }
392
393 QString uid;
394 if (!d->uid.isEmpty()) {
395 uid = d->uid;
396 } else {
397 uid = QUuid::createUuid().toString().mid(1, 36);
398 }
399
400 msg->subject(true)->fromUnicodeString(title);
401 msg->date(true)->setDateTime(creationDate);
402 msg->from(true)->fromUnicodeString(d->from);
403 const QString formatDate = QLocale::c().toString(lastModifiedDate, QStringLiteral("ddd, ")) + lastModifiedDate.toString(Qt::RFC2822Date);
404
405 auto header = new KMime::Headers::Generic(X_NOTES_LASTMODIFIED_HEADER);
406 header->fromUnicodeString(formatDate);
407 msg->appendHeader(header);
408 header = new KMime::Headers::Generic(X_NOTES_UID_HEADER);
409 header->fromUnicodeString(uid);
410 msg->appendHeader(header);
411
412 QString classification = CLASSIFICATION_PUBLIC;
413 switch (d->classification) {
414 case Private:
415 classification = CLASSIFICATION_PRIVATE;
416 break;
417 case Confidential:
418 classification = CLASSIFICATION_CONFIDENTIAL;
419 break;
420 default:
421 // do nothing
422 break;
423 }
424 header = new KMime::Headers::Generic(X_NOTES_CLASSIFICATION_HEADER);
425 header->fromUnicodeString(classification);
426 msg->appendHeader(header);
427
428 for (const Attachment &a : std::as_const(d->attachments)) {
429 msg->appendContent(d->createAttachmentPart(a));
430 }
431
432 if (!d->custom.isEmpty()) {
433 msg->appendContent(d->createCustomPart());
434 }
435
436 msg->mainBodyPart()->contentType(true)->setCharset(ENCODING);
437 msg->mainBodyPart()->fromUnicodeString(text);
438 msg->mainBodyPart()->contentType(true)->setMimeType(d->textFormat == Qt::RichText ? "text/html" : "text/plain");
439
440 msg->assemble();
441 return msg;
442}
443
445{
447 d->uid = uid;
448}
449
451{
452 Q_D(const NoteMessageWrapper);
453 return d->uid;
454}
455
456void NoteMessageWrapper::setClassification(NoteMessageWrapper::Classification classification)
457{
459 d->classification = classification;
460}
461
462NoteMessageWrapper::Classification NoteMessageWrapper::classification() const
463{
464 Q_D(const NoteMessageWrapper);
465 return d->classification;
466}
467
469{
471 d->lastModifiedDate = lastModifiedDate;
472}
473
475{
476 Q_D(const NoteMessageWrapper);
477 return d->lastModifiedDate;
478}
479
481{
483 d->creationDate = creationDate;
484}
485
487{
488 Q_D(const NoteMessageWrapper);
489 return d->creationDate;
490}
491
493{
495 d->from = from;
496}
497
499{
500 Q_D(const NoteMessageWrapper);
501 return d->from;
502}
503
505{
507 d->title = title;
508}
509
511{
512 Q_D(const NoteMessageWrapper);
513 return d->title;
514}
515
517{
519 d->text = text;
520 d->textFormat = format;
521}
522
524{
525 Q_D(const NoteMessageWrapper);
526 return d->text;
527}
528
530{
531 Q_D(const NoteMessageWrapper);
532 return d->textFormat;
533}
534
536{
537 Q_D(const NoteMessageWrapper);
538 if (d->textFormat == Qt::PlainText) {
539 return d->text;
540 }
541
542 // From cleanHtml in kdepimlibs/kcalutils/incidenceformatter.cpp
543 const QRegularExpression rx(QStringLiteral("<body[^>]*>(.*)</body>"), QRegularExpression::CaseInsensitiveOption);
544 QString body = rx.match(d->text).captured(1);
545
546 return body.remove(QRegularExpression(QStringLiteral("<[^>]*>"))).trimmed().toHtmlEscaped();
547}
548
550{
552 return d->attachments;
553}
554
560
561QString noteIconName()
562{
563 return QStringLiteral("text-plain");
564}
565
566QString noteMimeType()
567{
568 return QStringLiteral("text/x-vnd.akonadi.note");
569}
570
571} // End Namespace
572} // End Namespace
An attachment for a note.
Definition noteutils.h:49
void setContentID(const QString &contentID)
Sets the unique identifier of the attachment.
void setDataBase64Encoded(bool encoded)
Set this to true if inline data provided via ctor is already base64 encoded.
QString label() const
Returns the label of the attachment.
Attachment()
Create an attachment referencing a url only.
Definition noteutils.cpp:68
QString contentID() const
Returns the unique identifier for inline attachment.
QByteArray data() const
Returns the date for inline attachments.
QString mimetype() const
Returns the mimetype.
void setLabel(const QString &label)
Sets the label to be presented to the user.
QUrl url() const
Returns the url for url-only attachments.
bool dataBase64Encoded() const
Returns true if data is already base64-encoded.
A convenience wrapper around KMime::MessagePtr for notes.
Definition noteutils.h:170
QMap< QString, QString > & custom()
Returns a reference to the custom-value map.
void setText(const QString &text, Qt::TextFormat format=Qt::PlainText)
Set the text of the note.
QString from() const
Returns the origin (creator) of the note.
QList< Attachment > & attachments()
Returns a reference to the list of attachments of the note.
void setFrom(const QString &from)
Set the origin (creator) of the note (stored in the mime header) This is usually the application crea...
QDateTime lastModifiedDate() const
Returns the lastModified-date of the note.
QString text() const
Returns the text of the note.
QDateTime creationDate() const
Returns the creation date of the note.
void setUid(const QString &uid)
Set the uid of the note.
void setClassification(Classification)
Set the classification of the note.
void setCreationDate(const QDateTime &creationDate)
Set the creation date of the note (stored in the mime header)
void setLastModifiedDate(const QDateTime &lastModifiedDate)
Set the lastModified-date of the note.
QString title() const
Returns the title of the note.
Classification classification() const
Returns the classification of the note.
void setTitle(const QString &title)
Set the title of the note.
KMime::MessagePtr message() const
Assemble a KMime message with the given values.
QString uid() const
Returns the uid of the note.
const Headers::ContentType * contentType() const
Headers::Base * headerByType(QByteArrayView type) const
QByteArray decodedContent() const
QByteArray body() const
const Headers::ContentID * contentID() const
QByteArray mimeType() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
char * data()
QDateTime currentDateTime()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
QString toString(QStringView format, QCalendar cal) const const
QDomElement createElement(const QString &tagName)
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomText createTextNode(const QString &value)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QString toString(int indent) const const
void setAttribute(const QString &name, const QString &value)
QString tagName() const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
QDomNode firstChild() const const
bool isNull() const const
QDomDocument ownerDocument() const const
QDomElement toElement() const const
QLocale c()
QString toString(QDate date, FormatType format) const const
iterator begin()
iterator end()
iterator insert(const Key &key, const T &value)
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
QString captured(QStringView name) const const
T * data() const const
QString fromLatin1(QByteArrayView str)
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString toHtmlEscaped() const const
QByteArray toLatin1() const const
QString trimmed() const const
RFC2822Date
TextFormat
QUuid createUuid()
QString toString(StringFormat mode) const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:19:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.