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/KMimeMessage>
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, ENCODING);
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, ENCODING);
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(), ENCODING);
314 content->appendHeader(header);
315 } else {
316 content->setBody(a.data());
317 }
318 content->contentType()->setMimeType(a.mimetype().toLatin1());
319 if (!a.label().isEmpty()) {
320 header = new KMime::Headers::Generic(X_NOTES_LABEL_HEADER);
321 header->fromUnicodeString(a.label(), ENCODING);
322 content->appendHeader(header);
323 }
324 content->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
325 if (a.dataBase64Encoded()) {
326 content->contentTransferEncoding()->setDecoded(false);
327 }
328 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
329 content->contentDisposition()->setFilename(QStringLiteral("attachment"));
330 if (!a.contentID().isEmpty()) {
331 content->contentID()->setIdentifier(a.contentID().toLatin1());
332 }
333 return content;
334}
335
336void NoteMessageWrapperPrivate::parseAttachmentPart(KMime::Content *part)
337{
339 if (KMime::Headers::Base *labelHeader = part->headerByType(X_NOTES_LABEL_HEADER)) {
340 label = labelHeader->asUnicodeString();
341 }
342 if (KMime::Headers::Base *header = part->headerByType(X_NOTES_URL_HEADER)) {
343 Attachment attachment(QUrl(header->asUnicodeString()), QLatin1StringView(part->contentType()->mimeType()));
344 attachment.setLabel(label);
345 attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
346 attachments.append(attachment);
347 } else {
348 Attachment attachment(part->decodedContent(), QLatin1StringView(part->contentType()->mimeType()));
349 attachment.setLabel(label);
350 attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
351 attachments.append(attachment);
352 }
353}
354
355NoteMessageWrapper::NoteMessageWrapper()
356 : d_ptr(new NoteMessageWrapperPrivate())
357{
358}
359
360NoteMessageWrapper::NoteMessageWrapper(const KMime::MessagePtr &msg)
361 : d_ptr(new NoteMessageWrapperPrivate(msg))
362{
363}
364
365NoteMessageWrapper::~NoteMessageWrapper() = default;
366
368{
369 Q_D(const NoteMessageWrapper);
371
372 QString title = i18nc("The default name for new notes.", "New Note");
373 if (!d->title.isEmpty()) {
374 title = d->title;
375 }
376 // Need a non-empty body part so that the serializer regards this as a valid message.
377 QString text = QStringLiteral(" ");
378 if (!d->text.isEmpty()) {
379 text = d->text;
380 }
381
383 if (d->creationDate.isValid()) {
384 creationDate = d->creationDate;
385 }
386
388 if (d->lastModifiedDate.isValid()) {
389 lastModifiedDate = d->lastModifiedDate;
390 }
391
392 QString uid;
393 if (!d->uid.isEmpty()) {
394 uid = d->uid;
395 } else {
396 uid = QUuid::createUuid().toString().mid(1, 36);
397 }
398
399 msg->subject(true)->fromUnicodeString(title, ENCODING);
400 msg->date(true)->setDateTime(creationDate);
401 msg->from(true)->fromUnicodeString(d->from, ENCODING);
402 const QString formatDate = QLocale::c().toString(lastModifiedDate, QStringLiteral("ddd, ")) + lastModifiedDate.toString(Qt::RFC2822Date);
403
404 auto header = new KMime::Headers::Generic(X_NOTES_LASTMODIFIED_HEADER);
405 header->fromUnicodeString(formatDate, ENCODING);
406 msg->appendHeader(header);
407 header = new KMime::Headers::Generic(X_NOTES_UID_HEADER);
408 header->fromUnicodeString(uid, ENCODING);
409 msg->appendHeader(header);
410
411 QString classification = CLASSIFICATION_PUBLIC;
412 switch (d->classification) {
413 case Private:
414 classification = CLASSIFICATION_PRIVATE;
415 break;
416 case Confidential:
417 classification = CLASSIFICATION_CONFIDENTIAL;
418 break;
419 default:
420 // do nothing
421 break;
422 }
423 header = new KMime::Headers::Generic(X_NOTES_CLASSIFICATION_HEADER);
424 header->fromUnicodeString(classification, ENCODING);
425 msg->appendHeader(header);
426
427 for (const Attachment &a : std::as_const(d->attachments)) {
428 msg->appendContent(d->createAttachmentPart(a));
429 }
430
431 if (!d->custom.isEmpty()) {
432 msg->appendContent(d->createCustomPart());
433 }
434
435 msg->mainBodyPart()->contentType(true)->setCharset(ENCODING);
436 msg->mainBodyPart()->fromUnicodeString(text);
437 msg->mainBodyPart()->contentType(true)->setMimeType(d->textFormat == Qt::RichText ? "text/html" : "text/plain");
438
439 msg->assemble();
440 return msg;
441}
442
444{
446 d->uid = uid;
447}
448
450{
451 Q_D(const NoteMessageWrapper);
452 return d->uid;
453}
454
455void NoteMessageWrapper::setClassification(NoteMessageWrapper::Classification classification)
456{
458 d->classification = classification;
459}
460
461NoteMessageWrapper::Classification NoteMessageWrapper::classification() const
462{
463 Q_D(const NoteMessageWrapper);
464 return d->classification;
465}
466
468{
470 d->lastModifiedDate = lastModifiedDate;
471}
472
474{
475 Q_D(const NoteMessageWrapper);
476 return d->lastModifiedDate;
477}
478
480{
482 d->creationDate = creationDate;
483}
484
486{
487 Q_D(const NoteMessageWrapper);
488 return d->creationDate;
489}
490
492{
494 d->from = from;
495}
496
498{
499 Q_D(const NoteMessageWrapper);
500 return d->from;
501}
502
504{
506 d->title = title;
507}
508
510{
511 Q_D(const NoteMessageWrapper);
512 return d->title;
513}
514
516{
518 d->text = text;
519 d->textFormat = format;
520}
521
523{
524 Q_D(const NoteMessageWrapper);
525 return d->text;
526}
527
529{
530 Q_D(const NoteMessageWrapper);
531 return d->textFormat;
532}
533
535{
536 Q_D(const NoteMessageWrapper);
537 if (d->textFormat == Qt::PlainText) {
538 return d->text;
539 }
540
541 // From cleanHtml in kdepimlibs/kcalutils/incidenceformatter.cpp
542 const QRegularExpression rx(QStringLiteral("<body[^>]*>(.*)</body>"), QRegularExpression::CaseInsensitiveOption);
543 QString body = rx.match(d->text).captured(1);
544
545 return body.remove(QRegularExpression(QStringLiteral("<[^>]*>"))).trimmed().toHtmlEscaped();
546}
547
549{
551 return d->attachments;
552}
553
559
560QString noteIconName()
561{
562 return QStringLiteral("text-plain");
563}
564
565QString noteMimeType()
566{
567 return QStringLiteral("text/x-vnd.akonadi.note");
568}
569
570} // End Namespace
571} // 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.
Headers::ContentType * contentType(bool create=true)
QByteArray decodedContent()
QByteArray body() const
Headers::ContentID * contentID(bool create=true)
Headers::Base * headerByType(const char *type) const
QByteArray mimeType() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
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 Tue Mar 26 2024 11:12:32 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.