File indexing completed on 2024-05-12 05:11:13

0001 /*  This file is part of the KDE project
0002     SPDX-FileCopyrightText: 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "noteutils.h"
0008 
0009 #include "akonadi_notes_debug.h"
0010 #include <KLocalizedString>
0011 #include <KMime/KMimeMessage>
0012 #include <QDateTime>
0013 
0014 #include <QRegularExpression>
0015 #include <QString>
0016 #include <QUuid>
0017 #include <qdom.h>
0018 
0019 namespace Akonadi
0020 {
0021 namespace NoteUtils
0022 {
0023 #define X_NOTES_UID_HEADER "X-Akonotes-UID"
0024 #define X_NOTES_LASTMODIFIED_HEADER "X-Akonotes-LastModified"
0025 #define X_NOTES_CLASSIFICATION_HEADER "X-Akonotes-Classification"
0026 #define X_NOTES_CUSTOM_HEADER "X-Akonotes-Custom"
0027 
0028 #define CLASSIFICATION_PUBLIC QStringLiteral("Public")
0029 #define CLASSIFICATION_PRIVATE QStringLiteral("Private")
0030 #define CLASSIFICATION_CONFIDENTIAL QStringLiteral("Confidential")
0031 
0032 #define X_NOTES_URL_HEADER "X-Akonotes-Url"
0033 #define X_NOTES_LABEL_HEADER "X-Akonotes-Label"
0034 #define X_NOTES_CONTENTTYPE_HEADER "X-Akonotes-Type"
0035 #define CONTENT_TYPE_CUSTOM QStringLiteral("custom")
0036 #define CONTENT_TYPE_ATTACHMENT QStringLiteral("attachment")
0037 
0038 #define ENCODING "utf-8"
0039 
0040 class AttachmentPrivate
0041 {
0042 public:
0043     AttachmentPrivate(const QUrl &url, const QString &mimetype)
0044         : mUrl(url)
0045         , mMimetype(mimetype)
0046     {
0047     }
0048 
0049     AttachmentPrivate(const QByteArray &data, const QString &mimetype)
0050         : mData(data)
0051         , mMimetype(mimetype)
0052     {
0053     }
0054 
0055     AttachmentPrivate(const AttachmentPrivate &other)
0056     {
0057         *this = other;
0058     }
0059 
0060     QUrl mUrl;
0061     QByteArray mData;
0062     bool mDataBase64Encoded = false;
0063     QString mMimetype;
0064     QString mLabel;
0065     QString mContentID;
0066 };
0067 
0068 Attachment::Attachment()
0069     : d_ptr(new AttachmentPrivate(QUrl(), QString()))
0070 {
0071 }
0072 
0073 Attachment::Attachment(const QUrl &url, const QString &mimetype)
0074     : d_ptr(new AttachmentPrivate(url, mimetype))
0075 {
0076 }
0077 
0078 Attachment::Attachment(const QByteArray &data, const QString &mimetype)
0079     : d_ptr(new AttachmentPrivate(data, mimetype))
0080 {
0081 }
0082 
0083 Attachment::Attachment(const Attachment &other)
0084     : d_ptr(new AttachmentPrivate(*other.d_func()))
0085 {
0086 }
0087 
0088 Attachment::~Attachment() = default;
0089 
0090 bool Attachment::operator==(const Attachment &a) const
0091 {
0092     Q_D(const Attachment);
0093     if (d->mUrl.isEmpty()) {
0094         return d->mUrl == a.d_func()->mUrl && d->mDataBase64Encoded == a.d_func()->mDataBase64Encoded && d->mMimetype == a.d_func()->mMimetype
0095             && d->mContentID == a.d_func()->mContentID && d->mLabel == a.d_func()->mLabel;
0096     }
0097     return d->mData == a.d_func()->mData && d->mDataBase64Encoded == a.d_func()->mDataBase64Encoded && d->mMimetype == a.d_func()->mMimetype
0098         && d->mContentID == a.d_func()->mContentID && d->mLabel == a.d_func()->mLabel;
0099 }
0100 
0101 void Attachment::operator=(const Attachment &a)
0102 {
0103     *d_ptr = *a.d_ptr;
0104 }
0105 
0106 QUrl Attachment::url() const
0107 {
0108     Q_D(const Attachment);
0109     return d->mUrl;
0110 }
0111 
0112 QByteArray Attachment::data() const
0113 {
0114     Q_D(const Attachment);
0115     return d->mData;
0116 }
0117 
0118 void Attachment::setDataBase64Encoded(bool encoded)
0119 {
0120     Q_D(Attachment);
0121     d->mDataBase64Encoded = true;
0122 }
0123 
0124 bool Attachment::dataBase64Encoded() const
0125 {
0126     Q_D(const Attachment);
0127     return d->mDataBase64Encoded;
0128 }
0129 
0130 void Attachment::setContentID(const QString &contentID)
0131 {
0132     Q_D(Attachment);
0133     d->mContentID = contentID;
0134 }
0135 
0136 QString Attachment::contentID() const
0137 {
0138     Q_D(const Attachment);
0139     return d->mContentID;
0140 }
0141 
0142 QString Attachment::mimetype() const
0143 {
0144     Q_D(const Attachment);
0145     return d->mMimetype;
0146 }
0147 
0148 void Attachment::setLabel(const QString &label)
0149 {
0150     Q_D(Attachment);
0151     d->mLabel = label;
0152 }
0153 
0154 QString Attachment::label() const
0155 {
0156     Q_D(const Attachment);
0157     return d->mLabel;
0158 }
0159 
0160 class NoteMessageWrapperPrivate
0161 {
0162 public:
0163     NoteMessageWrapperPrivate() = default;
0164 
0165     NoteMessageWrapperPrivate(const KMime::MessagePtr &msg)
0166     {
0167         readMimeMessage(msg);
0168     }
0169 
0170     void readMimeMessage(const KMime::MessagePtr &msg);
0171 
0172     KMime::Content *createCustomPart() const;
0173     void parseCustomPart(KMime::Content *);
0174 
0175     KMime::Content *createAttachmentPart(const Attachment &) const;
0176     void parseAttachmentPart(KMime::Content *);
0177 
0178     QString uid;
0179     QString title;
0180     QString text;
0181     QString from;
0182     QDateTime creationDate;
0183     QDateTime lastModifiedDate;
0184     QMap<QString, QString> custom;
0185     QList<Attachment> attachments;
0186     NoteMessageWrapper::Classification classification = NoteMessageWrapper::Public;
0187     Qt::TextFormat textFormat = Qt::PlainText;
0188 };
0189 
0190 void NoteMessageWrapperPrivate::readMimeMessage(const KMime::MessagePtr &msg)
0191 {
0192     if (!msg.data()) {
0193         qCWarning(AKONADINOTES_LOG) << "Empty message";
0194         return;
0195     }
0196     title = msg->subject(true)->asUnicodeString();
0197     text = msg->mainBodyPart()->decodedText(true); // remove trailing whitespace, so we get rid of "  " in empty notes
0198     if (msg->from(false)) {
0199         from = msg->from(false)->asUnicodeString();
0200     }
0201     creationDate = msg->date(true)->dateTime();
0202     if (msg->mainBodyPart()->contentType(false) && msg->mainBodyPart()->contentType()->mimeType() == "text/html") {
0203         textFormat = Qt::RichText;
0204     }
0205 
0206     if (KMime::Headers::Base *lastmod = msg->headerByType(X_NOTES_LASTMODIFIED_HEADER)) {
0207         lastModifiedDate = QDateTime::fromString(lastmod->asUnicodeString(), Qt::RFC2822Date);
0208         if (!lastModifiedDate.isValid()) {
0209             qCWarning(AKONADINOTES_LOG) << "failed to parse lastModifiedDate";
0210         }
0211     }
0212 
0213     if (KMime::Headers::Base *uidHeader = msg->headerByType(X_NOTES_UID_HEADER)) {
0214         uid = uidHeader->asUnicodeString();
0215     }
0216 
0217     if (KMime::Headers::Base *classificationHeader = msg->headerByType(X_NOTES_CLASSIFICATION_HEADER)) {
0218         const QString &c = classificationHeader->asUnicodeString();
0219         if (c == CLASSIFICATION_PRIVATE) {
0220             classification = NoteMessageWrapper::Private;
0221         } else if (c == CLASSIFICATION_CONFIDENTIAL) {
0222             classification = NoteMessageWrapper::Confidential;
0223         }
0224     }
0225 
0226     const auto list = msg->contents();
0227     for (KMime::Content *c : list) {
0228         if (KMime::Headers::Base *typeHeader = c->headerByType(X_NOTES_CONTENTTYPE_HEADER)) {
0229             const QString &type = typeHeader->asUnicodeString();
0230             if (type == CONTENT_TYPE_CUSTOM) {
0231                 parseCustomPart(c);
0232             } else if (type == CONTENT_TYPE_ATTACHMENT) {
0233                 parseAttachmentPart(c);
0234             } else {
0235                 qCWarning(AKONADINOTES_LOG) << "unknown type " << type;
0236             }
0237         }
0238     }
0239 }
0240 
0241 QDomDocument createXMLDocument()
0242 {
0243     QDomDocument document;
0244     const QString p = QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"");
0245     document.appendChild(document.createProcessingInstruction(QStringLiteral("xml"), p));
0246     return document;
0247 }
0248 
0249 QDomDocument loadDocument(KMime::Content *part)
0250 {
0251     QDomDocument document;
0252     const QDomDocument::ParseResult parseResult = document.setContent(part->body());
0253     if (!parseResult) {
0254         qCWarning(AKONADINOTES_LOG) << part->body();
0255         qWarning("Error loading document: %s, line %lld, column %lld", qPrintable(parseResult.errorMessage), parseResult.errorLine, parseResult.errorColumn);
0256         return {};
0257     }
0258     return document;
0259 }
0260 
0261 KMime::Content *NoteMessageWrapperPrivate::createCustomPart() const
0262 {
0263     auto content = new KMime::Content();
0264     auto header = new KMime::Headers::Generic(X_NOTES_CONTENTTYPE_HEADER);
0265     header->fromUnicodeString(CONTENT_TYPE_CUSTOM, ENCODING);
0266     content->appendHeader(header);
0267     QDomDocument document = createXMLDocument();
0268     QDomElement element = document.createElement(QStringLiteral("custom"));
0269     element.setAttribute(QStringLiteral("version"), QStringLiteral("1.0"));
0270     QMap<QString, QString>::const_iterator end = custom.end();
0271     for (QMap<QString, QString>::const_iterator it = custom.begin(); it != end; ++it) {
0272         QDomElement e = element.ownerDocument().createElement(it.key());
0273         QDomText t = element.ownerDocument().createTextNode(it.value());
0274         e.appendChild(t);
0275         element.appendChild(e);
0276         document.appendChild(element);
0277     }
0278     content->setBody(document.toString().toLatin1());
0279     return content;
0280 }
0281 
0282 void NoteMessageWrapperPrivate::parseCustomPart(KMime::Content *part)
0283 {
0284     QDomDocument document = loadDocument(part);
0285     if (document.isNull()) {
0286         return;
0287     }
0288     QDomElement top = document.documentElement();
0289     if (top.tagName() != QLatin1StringView("custom")) {
0290         qWarning("XML error: Top tag was %s instead of the expected custom", top.tagName().toLatin1().data());
0291         return;
0292     }
0293 
0294     for (QDomNode n = top.firstChild(); !n.isNull(); n = n.nextSibling()) {
0295         if (n.isElement()) {
0296             QDomElement e = n.toElement();
0297             custom.insert(e.tagName(), e.text());
0298         } else {
0299             qCDebug(AKONADINOTES_LOG) << "Node is not an element";
0300             Q_ASSERT(false);
0301         }
0302     }
0303 }
0304 
0305 KMime::Content *NoteMessageWrapperPrivate::createAttachmentPart(const Attachment &a) const
0306 {
0307     auto content = new KMime::Content();
0308     auto header = new KMime::Headers::Generic(X_NOTES_CONTENTTYPE_HEADER);
0309     header->fromUnicodeString(CONTENT_TYPE_ATTACHMENT, ENCODING);
0310     content->appendHeader(header);
0311     if (a.url().isValid()) {
0312         header = new KMime::Headers::Generic(X_NOTES_URL_HEADER);
0313         header->fromUnicodeString(a.url().toString(), ENCODING);
0314         content->appendHeader(header);
0315     } else {
0316         content->setBody(a.data());
0317     }
0318     content->contentType()->setMimeType(a.mimetype().toLatin1());
0319     if (!a.label().isEmpty()) {
0320         header = new KMime::Headers::Generic(X_NOTES_LABEL_HEADER);
0321         header->fromUnicodeString(a.label(), ENCODING);
0322         content->appendHeader(header);
0323     }
0324     content->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
0325     if (a.dataBase64Encoded()) {
0326         content->contentTransferEncoding()->setDecoded(false);
0327     }
0328     content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
0329     content->contentDisposition()->setFilename(QStringLiteral("attachment"));
0330     if (!a.contentID().isEmpty()) {
0331         content->contentID()->setIdentifier(a.contentID().toLatin1());
0332     }
0333     return content;
0334 }
0335 
0336 void NoteMessageWrapperPrivate::parseAttachmentPart(KMime::Content *part)
0337 {
0338     QString label;
0339     if (KMime::Headers::Base *labelHeader = part->headerByType(X_NOTES_LABEL_HEADER)) {
0340         label = labelHeader->asUnicodeString();
0341     }
0342     if (KMime::Headers::Base *header = part->headerByType(X_NOTES_URL_HEADER)) {
0343         Attachment attachment(QUrl(header->asUnicodeString()), QLatin1StringView(part->contentType()->mimeType()));
0344         attachment.setLabel(label);
0345         attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
0346         attachments.append(attachment);
0347     } else {
0348         Attachment attachment(part->decodedContent(), QLatin1StringView(part->contentType()->mimeType()));
0349         attachment.setLabel(label);
0350         attachment.setContentID(QString::fromLatin1(part->contentID()->identifier()));
0351         attachments.append(attachment);
0352     }
0353 }
0354 
0355 NoteMessageWrapper::NoteMessageWrapper()
0356     : d_ptr(new NoteMessageWrapperPrivate())
0357 {
0358 }
0359 
0360 NoteMessageWrapper::NoteMessageWrapper(const KMime::MessagePtr &msg)
0361     : d_ptr(new NoteMessageWrapperPrivate(msg))
0362 {
0363 }
0364 
0365 NoteMessageWrapper::~NoteMessageWrapper() = default;
0366 
0367 KMime::MessagePtr NoteMessageWrapper::message() const
0368 {
0369     Q_D(const NoteMessageWrapper);
0370     KMime::MessagePtr msg = KMime::MessagePtr(new KMime::Message());
0371 
0372     QString title = i18nc("The default name for new notes.", "New Note");
0373     if (!d->title.isEmpty()) {
0374         title = d->title;
0375     }
0376     // Need a non-empty body part so that the serializer regards this as a valid message.
0377     QString text = QStringLiteral("  ");
0378     if (!d->text.isEmpty()) {
0379         text = d->text;
0380     }
0381 
0382     QDateTime creationDate = QDateTime::currentDateTime();
0383     if (d->creationDate.isValid()) {
0384         creationDate = d->creationDate;
0385     }
0386 
0387     QDateTime lastModifiedDate = QDateTime::currentDateTime();
0388     if (d->lastModifiedDate.isValid()) {
0389         lastModifiedDate = d->lastModifiedDate;
0390     }
0391 
0392     QString uid;
0393     if (!d->uid.isEmpty()) {
0394         uid = d->uid;
0395     } else {
0396         uid = QUuid::createUuid().toString().mid(1, 36);
0397     }
0398 
0399     msg->subject(true)->fromUnicodeString(title, ENCODING);
0400     msg->date(true)->setDateTime(creationDate);
0401     msg->from(true)->fromUnicodeString(d->from, ENCODING);
0402     const QString formatDate = QLocale::c().toString(lastModifiedDate, QStringLiteral("ddd, ")) + lastModifiedDate.toString(Qt::RFC2822Date);
0403 
0404     auto header = new KMime::Headers::Generic(X_NOTES_LASTMODIFIED_HEADER);
0405     header->fromUnicodeString(formatDate, ENCODING);
0406     msg->appendHeader(header);
0407     header = new KMime::Headers::Generic(X_NOTES_UID_HEADER);
0408     header->fromUnicodeString(uid, ENCODING);
0409     msg->appendHeader(header);
0410 
0411     QString classification = CLASSIFICATION_PUBLIC;
0412     switch (d->classification) {
0413     case Private:
0414         classification = CLASSIFICATION_PRIVATE;
0415         break;
0416     case Confidential:
0417         classification = CLASSIFICATION_CONFIDENTIAL;
0418         break;
0419     default:
0420         // do nothing
0421         break;
0422     }
0423     header = new KMime::Headers::Generic(X_NOTES_CLASSIFICATION_HEADER);
0424     header->fromUnicodeString(classification, ENCODING);
0425     msg->appendHeader(header);
0426 
0427     for (const Attachment &a : std::as_const(d->attachments)) {
0428         msg->appendContent(d->createAttachmentPart(a));
0429     }
0430 
0431     if (!d->custom.isEmpty()) {
0432         msg->appendContent(d->createCustomPart());
0433     }
0434 
0435     msg->mainBodyPart()->contentType(true)->setCharset(ENCODING);
0436     msg->mainBodyPart()->fromUnicodeString(text);
0437     msg->mainBodyPart()->contentType(true)->setMimeType(d->textFormat == Qt::RichText ? "text/html" : "text/plain");
0438 
0439     msg->assemble();
0440     return msg;
0441 }
0442 
0443 void NoteMessageWrapper::setUid(const QString &uid)
0444 {
0445     Q_D(NoteMessageWrapper);
0446     d->uid = uid;
0447 }
0448 
0449 QString NoteMessageWrapper::uid() const
0450 {
0451     Q_D(const NoteMessageWrapper);
0452     return d->uid;
0453 }
0454 
0455 void NoteMessageWrapper::setClassification(NoteMessageWrapper::Classification classification)
0456 {
0457     Q_D(NoteMessageWrapper);
0458     d->classification = classification;
0459 }
0460 
0461 NoteMessageWrapper::Classification NoteMessageWrapper::classification() const
0462 {
0463     Q_D(const NoteMessageWrapper);
0464     return d->classification;
0465 }
0466 
0467 void NoteMessageWrapper::setLastModifiedDate(const QDateTime &lastModifiedDate)
0468 {
0469     Q_D(NoteMessageWrapper);
0470     d->lastModifiedDate = lastModifiedDate;
0471 }
0472 
0473 QDateTime NoteMessageWrapper::lastModifiedDate() const
0474 {
0475     Q_D(const NoteMessageWrapper);
0476     return d->lastModifiedDate;
0477 }
0478 
0479 void NoteMessageWrapper::setCreationDate(const QDateTime &creationDate)
0480 {
0481     Q_D(NoteMessageWrapper);
0482     d->creationDate = creationDate;
0483 }
0484 
0485 QDateTime NoteMessageWrapper::creationDate() const
0486 {
0487     Q_D(const NoteMessageWrapper);
0488     return d->creationDate;
0489 }
0490 
0491 void NoteMessageWrapper::setFrom(const QString &from)
0492 {
0493     Q_D(NoteMessageWrapper);
0494     d->from = from;
0495 }
0496 
0497 QString NoteMessageWrapper::from() const
0498 {
0499     Q_D(const NoteMessageWrapper);
0500     return d->from;
0501 }
0502 
0503 void NoteMessageWrapper::setTitle(const QString &title)
0504 {
0505     Q_D(NoteMessageWrapper);
0506     d->title = title;
0507 }
0508 
0509 QString NoteMessageWrapper::title() const
0510 {
0511     Q_D(const NoteMessageWrapper);
0512     return d->title;
0513 }
0514 
0515 void NoteMessageWrapper::setText(const QString &text, Qt::TextFormat format)
0516 {
0517     Q_D(NoteMessageWrapper);
0518     d->text = text;
0519     d->textFormat = format;
0520 }
0521 
0522 QString NoteMessageWrapper::text() const
0523 {
0524     Q_D(const NoteMessageWrapper);
0525     return d->text;
0526 }
0527 
0528 Qt::TextFormat NoteMessageWrapper::textFormat() const
0529 {
0530     Q_D(const NoteMessageWrapper);
0531     return d->textFormat;
0532 }
0533 
0534 QString NoteMessageWrapper::toPlainText() const
0535 {
0536     Q_D(const NoteMessageWrapper);
0537     if (d->textFormat == Qt::PlainText) {
0538         return d->text;
0539     }
0540 
0541     // From cleanHtml in kdepimlibs/kcalutils/incidenceformatter.cpp
0542     const QRegularExpression rx(QStringLiteral("<body[^>]*>(.*)</body>"), QRegularExpression::CaseInsensitiveOption);
0543     QString body = rx.match(d->text).captured(1);
0544 
0545     return body.remove(QRegularExpression(QStringLiteral("<[^>]*>"))).trimmed().toHtmlEscaped();
0546 }
0547 
0548 QList<Attachment> &NoteMessageWrapper::attachments()
0549 {
0550     Q_D(NoteMessageWrapper);
0551     return d->attachments;
0552 }
0553 
0554 QMap<QString, QString> &NoteMessageWrapper::custom()
0555 {
0556     Q_D(NoteMessageWrapper);
0557     return d->custom;
0558 }
0559 
0560 QString noteIconName()
0561 {
0562     return QStringLiteral("text-plain");
0563 }
0564 
0565 QString noteMimeType()
0566 {
0567     return QStringLiteral("text/x-vnd.akonadi.note");
0568 }
0569 
0570 } // End Namespace
0571 } // End Namespace