File indexing completed on 2024-11-24 04:44:17

0001 /*
0002     SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kolabhelpers.h"
0008 #include "kolabresource_debug.h"
0009 #include "kolabresource_trace.h"
0010 #include "pimkolab/kolabformat/errorhandler.h"
0011 #include "pimkolab/kolabformat/kolabobject.h"
0012 
0013 #include <Akonadi/NoteUtils>
0014 
0015 #include <Akonadi/Collection>
0016 #include <Akonadi/ItemFetchJob>
0017 #include <Akonadi/ItemFetchScope>
0018 
0019 #include <KCalendarCore/Incidence>
0020 #include <KLocalizedString>
0021 #include <QColor>
0022 
0023 bool KolabHelpers::checkForErrors(const Akonadi::Item &item)
0024 {
0025     if (!Kolab::ErrorHandler::instance().errorOccured()) {
0026         Kolab::ErrorHandler::instance().clear();
0027         return false;
0028     }
0029 
0030     QString errorMsg;
0031     const auto errors{Kolab::ErrorHandler::instance().getErrors()};
0032     for (const Kolab::ErrorHandler::Err &error : errors) {
0033         errorMsg.append(error.message);
0034         errorMsg.append(QLatin1Char('\n'));
0035     }
0036 
0037     qCWarning(KOLABRESOURCE_LOG) << "Error on item with id: " << item.id() << " remote id: " << item.remoteId() << ":\n" << errorMsg;
0038     Kolab::ErrorHandler::instance().clear();
0039     return true;
0040 }
0041 
0042 Akonadi::Item getErrorItem(Kolab::FolderType folderType, const QString &remoteId)
0043 {
0044     // TODO set title, text and icon
0045     Akonadi::Item item;
0046     item.setRemoteId(remoteId);
0047     switch (folderType) {
0048     case Kolab::EventType: {
0049         KCalendarCore::Event::Ptr event(new KCalendarCore::Event);
0050         // FIXME Use message creation date time
0051         event->setDtStart(QDateTime::currentDateTimeUtc());
0052         event->setSummary(i18n("Corrupt Event"));
0053         event->setDescription(
0054             i18n("Event could not be read. Delete this event to remove it from the server. Technical information: remote identifier %1", remoteId));
0055         item.setMimeType(KCalendarCore::Event::eventMimeType());
0056         item.setPayload(event);
0057         break;
0058     }
0059     case Kolab::TaskType: {
0060         KCalendarCore::Todo::Ptr task(new KCalendarCore::Todo);
0061         // FIXME Use message creation date time
0062         task->setDtStart(QDateTime::currentDateTimeUtc());
0063         task->setSummary(i18n("Corrupt Task"));
0064         task->setDescription(i18n("Task could not be read. Delete this task to remove it from the server."));
0065         item.setMimeType(KCalendarCore::Todo::todoMimeType());
0066         item.setPayload(task);
0067         break;
0068     }
0069     case Kolab::JournalType: {
0070         KCalendarCore::Journal::Ptr journal(new KCalendarCore::Journal);
0071         // FIXME Use message creation date time
0072         journal->setDtStart(QDateTime::currentDateTimeUtc());
0073         journal->setSummary(i18n("Corrupt journal"));
0074         journal->setDescription(i18n("Journal could not be read. Delete this journal to remove it from the server."));
0075         item.setMimeType(KCalendarCore::Journal::journalMimeType());
0076         item.setPayload(journal);
0077         break;
0078     }
0079     case Kolab::ContactType: {
0080         KContacts::Addressee addressee;
0081         addressee.setName(i18n("Corrupt Contact"));
0082         addressee.setNote(i18n("Contact could not be read. Delete this contact to remove it from the server."));
0083         item.setMimeType(KContacts::Addressee::mimeType());
0084         item.setPayload(addressee);
0085         break;
0086     }
0087     case Kolab::NoteType: {
0088         Akonadi::NoteUtils::NoteMessageWrapper note;
0089         note.setTitle(i18n("Corrupt Note"));
0090         note.setText(i18n("Note could not be read. Delete this note to remove it from the server."));
0091         item.setPayload(Akonadi::NoteUtils::noteMimeType());
0092         item.setPayload(note.message());
0093         break;
0094     }
0095     case Kolab::MailType:
0096     // We don't convert mails, so that should never fail.
0097     default:
0098         qCWarning(KOLABRESOURCE_LOG) << "unhandled folder type: " << folderType;
0099     }
0100     return item;
0101 }
0102 
0103 Akonadi::Item KolabHelpers::translateFromImap(Kolab::FolderType folderType, const Akonadi::Item &imapItem, bool &ok)
0104 {
0105     // Avoid trying to convert imap messages
0106     if (folderType == Kolab::MailType) {
0107         return imapItem;
0108     }
0109 
0110     // No payload, probably a flag change or alike, we just pass it through
0111     if (!imapItem.hasPayload()) {
0112         return imapItem;
0113     }
0114     if (!imapItem.hasPayload<KMime::Message::Ptr>()) {
0115         qCWarning(KOLABRESOURCE_LOG) << "Payload is not a MessagePtr!";
0116         Q_ASSERT(false);
0117         ok = false;
0118         return imapItem;
0119     }
0120 
0121     const auto payload = imapItem.payload<KMime::Message::Ptr>();
0122     const Kolab::KolabObjectReader reader(payload);
0123     if (checkForErrors(imapItem)) {
0124         ok = true;
0125         // We return an error object so the sync keeps working, and we can clean up the mess by simply deleting the object in the application.
0126         return getErrorItem(folderType, imapItem.remoteId());
0127     }
0128     switch (reader.getType()) {
0129     case Kolab::EventObject:
0130     case Kolab::TodoObject:
0131     case Kolab::JournalObject: {
0132         const KCalendarCore::Incidence::Ptr incidencePtr = reader.getIncidence();
0133         if (!incidencePtr) {
0134             qCWarning(KOLABRESOURCE_LOG) << "Failed to read incidence.";
0135             ok = false;
0136             return {};
0137         }
0138         Akonadi::Item newItem(incidencePtr->mimeType());
0139         newItem.setPayload(incidencePtr);
0140         newItem.setRemoteId(imapItem.remoteId());
0141         newItem.setGid(incidencePtr->instanceIdentifier());
0142         return newItem;
0143     }
0144     case Kolab::NoteObject: {
0145         const KMime::Message::Ptr note = reader.getNote();
0146         if (!note) {
0147             qCWarning(KOLABRESOURCE_LOG) << "Failed to read note.";
0148             ok = false;
0149             return {};
0150         }
0151         Akonadi::Item newItem(QStringLiteral("text/x-vnd.akonadi.note"));
0152         newItem.setPayload(note);
0153         newItem.setRemoteId(imapItem.remoteId());
0154         const Akonadi::NoteUtils::NoteMessageWrapper wrapper(note);
0155         newItem.setGid(wrapper.uid());
0156         return newItem;
0157     }
0158     case Kolab::ContactObject: {
0159         Akonadi::Item newItem(KContacts::Addressee::mimeType());
0160         newItem.setPayload(reader.getContact());
0161         newItem.setRemoteId(imapItem.remoteId());
0162         newItem.setGid(reader.getContact().uid());
0163         return newItem;
0164     }
0165     case Kolab::DistlistObject: {
0166         KContacts::ContactGroup contactGroup = reader.getDistlist();
0167 
0168         QList<KContacts::ContactGroup::ContactReference> toAdd;
0169         for (int index = 0; index < contactGroup.contactReferenceCount(); ++index) {
0170             const KContacts::ContactGroup::ContactReference &reference = contactGroup.contactReference(index);
0171             KContacts::ContactGroup::ContactReference ref;
0172             ref.setGid(reference.uid()); // libkolab set a gid with setUid()
0173             toAdd << ref;
0174         }
0175         contactGroup.removeAllContactReferences();
0176         for (const KContacts::ContactGroup::ContactReference &ref : std::as_const(toAdd)) {
0177             contactGroup.append(ref);
0178         }
0179 
0180         Akonadi::Item newItem(KContacts::ContactGroup::mimeType());
0181         newItem.setPayload(contactGroup);
0182         newItem.setRemoteId(imapItem.remoteId());
0183         newItem.setGid(contactGroup.id());
0184         return newItem;
0185     }
0186     case Kolab::RelationConfigurationObject:
0187         // Do nothing about tags and relations, this is handled separately in KolabRetrieveTagTask::onMessagesAvailable
0188         ok = false;
0189         break;
0190     default:
0191         qCWarning(KOLABRESOURCE_LOG) << "Object type not handled";
0192         ok = false;
0193         break;
0194     }
0195     return {};
0196 }
0197 
0198 Akonadi::Item::List KolabHelpers::translateToImap(const Akonadi::Item::List &items, bool &ok)
0199 {
0200     Akonadi::Item::List imapItems;
0201     imapItems.reserve(items.count());
0202     for (const Akonadi::Item &item : items) {
0203         bool translationOk = true;
0204         imapItems << translateToImap(item, translationOk);
0205         if (!translationOk) {
0206             ok = false;
0207         }
0208     }
0209     return imapItems;
0210 }
0211 
0212 static KContacts::ContactGroup convertToGidOnly(const KContacts::ContactGroup &contactGroup)
0213 {
0214     QList<KContacts::ContactGroup::ContactReference> toAdd;
0215     for (int index = 0; index < contactGroup.contactReferenceCount(); ++index) {
0216         const KContacts::ContactGroup::ContactReference &reference = contactGroup.contactReference(index);
0217         QString gid;
0218         if (!reference.gid().isEmpty()) {
0219             gid = reference.gid();
0220         } else {
0221             // WARNING: this is an ugly hack for backwards compatibility. Normally this codepath shouldn't be hit.
0222             // Replace all references with real data-sets
0223             // Hopefully all resources are available during saving, so we can look up
0224             // in the addressbook to get name+email from the UID.
0225 
0226             const Akonadi::Item item(reference.uid().toLongLong());
0227             auto job = new Akonadi::ItemFetchJob(item);
0228             job->fetchScope().fetchFullPayload();
0229             if (!job->exec()) {
0230                 continue;
0231             }
0232 
0233             const Akonadi::Item::List items = job->items();
0234             if (items.count() != 1) {
0235                 continue;
0236             }
0237             const auto addressee = job->items().at(0).payload<KContacts::Addressee>();
0238             gid = addressee.uid();
0239         }
0240         KContacts::ContactGroup::ContactReference ref;
0241         ref.setUid(gid); // libkolab expects a gid for uid()
0242         toAdd << ref;
0243     }
0244     KContacts::ContactGroup gidOnlyContactGroup = contactGroup;
0245     gidOnlyContactGroup.removeAllContactReferences();
0246     for (const KContacts::ContactGroup::ContactReference &ref : std::as_const(toAdd)) {
0247         gidOnlyContactGroup.append(ref);
0248     }
0249     return gidOnlyContactGroup;
0250 }
0251 
0252 Akonadi::Item KolabHelpers::translateToImap(const Akonadi::Item &item, bool &ok)
0253 {
0254     ok = true;
0255     // imap messages don't need to be translated
0256     if (item.mimeType() == KMime::Message::mimeType()) {
0257         Q_ASSERT(item.hasPayload<KMime::Message::Ptr>());
0258         return item;
0259     }
0260     const QLatin1StringView productId("Akonadi-Kolab-Resource");
0261     // Everything stays the same, except mime type and payload
0262     Akonadi::Item imapItem = item;
0263     imapItem.setMimeType(QStringLiteral("message/rfc822"));
0264     try {
0265         switch (getKolabTypeFromMimeType(item.mimeType())) {
0266         case Kolab::EventObject:
0267         case Kolab::TodoObject:
0268         case Kolab::JournalObject: {
0269             qCDebug(KOLABRESOURCE_LOG) << "converted event";
0270             const KMime::Message::Ptr message =
0271                 Kolab::KolabObjectWriter::writeIncidence(item.payload<KCalendarCore::Incidence::Ptr>(), Kolab::KolabV3, productId, QStringLiteral("UTC"));
0272             imapItem.setPayload(message);
0273             break;
0274         }
0275         case Kolab::NoteObject: {
0276             qCDebug(KOLABRESOURCE_LOG) << "converted note";
0277             const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeNote(item.payload<KMime::Message::Ptr>(), Kolab::KolabV3, productId);
0278             imapItem.setPayload(message);
0279             break;
0280         }
0281         case Kolab::ContactObject: {
0282             qCDebug(KOLABRESOURCE_LOG) << "converted contact";
0283             const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeContact(item.payload<KContacts::Addressee>(), Kolab::KolabV3, productId);
0284             imapItem.setPayload(message);
0285             break;
0286         }
0287         case Kolab::DistlistObject: {
0288             const KContacts::ContactGroup contactGroup = convertToGidOnly(item.payload<KContacts::ContactGroup>());
0289             qCDebug(KOLABRESOURCE_LOG) << "converted distlist";
0290             const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeDistlist(contactGroup, Kolab::KolabV3, productId);
0291             imapItem.setPayload(message);
0292             break;
0293         }
0294         default:
0295             qCWarning(KOLABRESOURCE_LOG) << "object type not handled: " << item.id() << item.mimeType();
0296             ok = false;
0297             return {};
0298         }
0299     } catch (const Akonadi::PayloadException &e) {
0300         qCWarning(KOLABRESOURCE_LOG) << "The item contains the wrong or no payload: " << item.id() << item.mimeType();
0301         qCWarning(KOLABRESOURCE_LOG) << e.what();
0302         return {};
0303     }
0304 
0305     if (checkForErrors(item)) {
0306         qCWarning(KOLABRESOURCE_LOG) << "an error occurred while trying to translate the item to the kolab format: " << item.id();
0307         ok = false;
0308         return {};
0309     }
0310     return imapItem;
0311 }
0312 
0313 QByteArray KolabHelpers::kolabTypeForMimeType(const QStringList &contentMimeTypes)
0314 {
0315     if (contentMimeTypes.contains(KContacts::Addressee::mimeType())) {
0316         return QByteArrayLiteral("contact");
0317     } else if (contentMimeTypes.contains(KCalendarCore::Event::eventMimeType())) {
0318         return QByteArrayLiteral("event");
0319     } else if (contentMimeTypes.contains(KCalendarCore::Todo::todoMimeType())) {
0320         return QByteArrayLiteral("task");
0321     } else if (contentMimeTypes.contains(KCalendarCore::Journal::journalMimeType())) {
0322         return QByteArrayLiteral("journal");
0323     } else if (contentMimeTypes.contains(QLatin1StringView("application/x-vnd.akonadi.note"))
0324                || contentMimeTypes.contains(QLatin1StringView("text/x-vnd.akonadi.note"))) {
0325         return QByteArrayLiteral("note");
0326     }
0327     return {};
0328 }
0329 
0330 Kolab::ObjectType KolabHelpers::getKolabTypeFromMimeType(const QString &type)
0331 {
0332     if (type == KCalendarCore::Event::eventMimeType()) {
0333         return Kolab::EventObject;
0334     } else if (type == KCalendarCore::Todo::todoMimeType()) {
0335         return Kolab::TodoObject;
0336     } else if (type == KCalendarCore::Journal::journalMimeType()) {
0337         return Kolab::JournalObject;
0338     } else if (type == KContacts::Addressee::mimeType()) {
0339         return Kolab::ContactObject;
0340     } else if (type == KContacts::ContactGroup::mimeType()) {
0341         return Kolab::DistlistObject;
0342     } else if (type == QLatin1StringView("text/x-vnd.akonadi.note") || type == QLatin1StringView("application/x-vnd.akonadi.note")) {
0343         return Kolab::NoteObject;
0344     }
0345     return Kolab::InvalidObject;
0346 }
0347 
0348 QString KolabHelpers::getMimeType(Kolab::FolderType type)
0349 {
0350     switch (type) {
0351     case Kolab::MailType:
0352         return KMime::Message::mimeType();
0353     case Kolab::ConfigurationType:
0354         return QStringLiteral(KOLAB_TYPE_RELATION);
0355     default:
0356         qCDebug(KOLABRESOURCE_LOG) << "unhandled folder type: " << type;
0357     }
0358     return {};
0359 }
0360 
0361 QStringList KolabHelpers::getContentMimeTypes(Kolab::FolderType type)
0362 {
0363     QStringList contentTypes;
0364     contentTypes << Akonadi::Collection::mimeType();
0365     switch (type) {
0366     case Kolab::EventType:
0367         contentTypes << KCalendarCore::Event().mimeType();
0368         break;
0369     case Kolab::TaskType:
0370         contentTypes << KCalendarCore::Todo().mimeType();
0371         break;
0372     case Kolab::JournalType:
0373         contentTypes << KCalendarCore::Journal().mimeType();
0374         break;
0375     case Kolab::ContactType:
0376         contentTypes << KContacts::Addressee::mimeType() << KContacts::ContactGroup::mimeType();
0377         break;
0378     case Kolab::NoteType:
0379         contentTypes << QStringLiteral("text/x-vnd.akonadi.note") << QStringLiteral("application/x-vnd.akonadi.note");
0380         break;
0381     case Kolab::MailType:
0382         contentTypes << KMime::Message::mimeType();
0383         break;
0384     case Kolab::ConfigurationType:
0385         contentTypes << QStringLiteral(KOLAB_TYPE_RELATION);
0386         break;
0387     default:
0388         break;
0389     }
0390     return contentTypes;
0391 }
0392 
0393 Kolab::FolderType KolabHelpers::folderTypeFromString(const QByteArray &folderTypeName)
0394 {
0395     const QByteArray stripped = folderTypeName.split('.').first();
0396     return Kolab::folderTypeFromString(std::string(stripped.data(), stripped.size()));
0397 }
0398 
0399 QByteArray KolabHelpers::getFolderTypeAnnotation(const QMap<QByteArray, QByteArray> &annotations)
0400 {
0401     if (annotations.contains("/shared" KOLAB_FOLDER_TYPE_ANNOTATION) && !annotations.value("/shared" KOLAB_FOLDER_TYPE_ANNOTATION).isEmpty()) {
0402         return annotations.value("/shared" KOLAB_FOLDER_TYPE_ANNOTATION);
0403     } else if (annotations.contains("/private" KOLAB_FOLDER_TYPE_ANNOTATION) && !annotations.value("/private" KOLAB_FOLDER_TYPE_ANNOTATION).isEmpty()) {
0404         return annotations.value("/private" KOLAB_FOLDER_TYPE_ANNOTATION);
0405     }
0406     return annotations.value(KOLAB_FOLDER_TYPE_ANNOTATION);
0407 }
0408 
0409 void KolabHelpers::setFolderTypeAnnotation(QMap<QByteArray, QByteArray> &annotations, const QByteArray &value)
0410 {
0411     annotations["/shared" KOLAB_FOLDER_TYPE_ANNOTATION] = value;
0412 }
0413 
0414 QColor KolabHelpers::getFolderColor(const QMap<QByteArray, QByteArray> &annotations)
0415 {
0416     // kolab saves the color without a "#", so we need to add it to the rgb string to have a proper QColor
0417     if (annotations.contains("/shared" KOLAB_COLOR_ANNOTATION) && !annotations.value("/shared" KOLAB_COLOR_ANNOTATION).isEmpty()) {
0418         return QColor(QStringLiteral("#").append(QString::fromUtf8(annotations.value("/shared" KOLAB_COLOR_ANNOTATION))));
0419     } else if (annotations.contains("/private" KOLAB_COLOR_ANNOTATION) && !annotations.value("/private" KOLAB_COLOR_ANNOTATION).isEmpty()) {
0420         return QColor(QStringLiteral("#").append(QString::fromUtf8(annotations.value("/private" KOLAB_COLOR_ANNOTATION))));
0421     }
0422     return {};
0423 }
0424 
0425 void KolabHelpers::setFolderColor(QMap<QByteArray, QByteArray> &annotations, const QColor &color)
0426 {
0427     // kolab saves the color without a "#", so we need to delete the prefix "#" if we save it to the annotations
0428     annotations["/shared" KOLAB_COLOR_ANNOTATION] = color.name().toLatin1().remove(0, 1);
0429 }
0430 
0431 QString KolabHelpers::getIcon(Kolab::FolderType type)
0432 {
0433     switch (type) {
0434     case Kolab::EventType:
0435     case Kolab::TaskType:
0436     case Kolab::JournalType:
0437         return QStringLiteral("view-calendar");
0438     case Kolab::ContactType:
0439         return QStringLiteral("view-pim-contacts");
0440     case Kolab::NoteType:
0441         return QStringLiteral("view-pim-notes");
0442     case Kolab::MailType:
0443     case Kolab::ConfigurationType:
0444     case Kolab::FreebusyType:
0445     case Kolab::FileType:
0446     case Kolab::LastType:
0447         return {};
0448     }
0449     return {};
0450 }
0451 
0452 bool KolabHelpers::isHandledType(Kolab::FolderType type)
0453 {
0454     switch (type) {
0455     case Kolab::EventType:
0456     case Kolab::TaskType:
0457     case Kolab::JournalType:
0458     case Kolab::ContactType:
0459     case Kolab::NoteType:
0460     case Kolab::MailType:
0461         return true;
0462     case Kolab::ConfigurationType:
0463     case Kolab::FreebusyType:
0464     case Kolab::FileType:
0465     case Kolab::LastType:
0466         return false;
0467     }
0468     return false;
0469 }
0470 
0471 QList<QByteArray> KolabHelpers::ancestorChain(const Akonadi::Collection &col)
0472 {
0473     Q_ASSERT(col.isValid());
0474     if (col.parentCollection() == Akonadi::Collection::root() || col == Akonadi::Collection::root() || !col.isValid()) {
0475         return {};
0476     }
0477     QList<QByteArray> ancestors = ancestorChain(col.parentCollection());
0478     Q_ASSERT(!col.remoteId().isEmpty());
0479     ancestors << col.remoteId().toLatin1().mid(1); // We strip the first character which is always the separator
0480     return ancestors;
0481 }
0482 
0483 QString KolabHelpers::createMemberUrl(const Akonadi::Item &item, const QString &user)
0484 {
0485     qCDebug(KOLABRESOURCE_TRACE) << item.id() << item.mimeType() << item.gid() << item.hasPayload();
0486     Kolab::RelationMember member;
0487     if (item.mimeType() == KMime::Message::mimeType()) {
0488         if (!item.hasPayload<KMime::Message::Ptr>()) {
0489             qCWarning(KOLABRESOURCE_LOG) << "Email without payload, failed to add to tag: " << item.id() << item.remoteId();
0490             return {};
0491         }
0492         auto msg = item.payload<KMime::Message::Ptr>();
0493         member.uid = item.remoteId().toLong();
0494         member.user = user;
0495         member.subject = msg->subject()->asUnicodeString();
0496         member.messageId = msg->messageID()->asUnicodeString();
0497         member.date = msg->date()->asUnicodeString();
0498         member.mailbox = ancestorChain(item.parentCollection());
0499     } else {
0500         if (item.gid().isEmpty()) {
0501             qCWarning(KOLABRESOURCE_LOG) << "Groupware object without GID, failed to add to tag: " << item.id() << item.remoteId();
0502             return {};
0503         }
0504         member.gid = item.gid();
0505     }
0506     return Kolab::generateMemberUrl(member);
0507 }