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