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 }