File indexing completed on 2025-02-16 04:50:27

0001 /*
0002  * SPDX-FileCopyrightText: 2012 Christian Mollekopf <mollekopf@kolabsys.com>
0003  *
0004  * SPDX-License-Identifier: LGPL-3.0-or-later
0005  */
0006 
0007 #include "kolabobject.h"
0008 #include "libkolab-version.h"
0009 #include "pimkolab_debug.h"
0010 #include "v2helpers.h"
0011 
0012 #include <Akonadi/NoteUtils>
0013 
0014 #include "conversion/commonconversion.h"
0015 #include "conversion/kabcconversion.h"
0016 #include "conversion/kcalconversion.h"
0017 #include "conversion/kolabconversion.h"
0018 #include "kolabformat/mimeobject.h"
0019 #include "kolabformatV2/contact.h"
0020 #include "kolabformatV2/distributionlist.h"
0021 #include "kolabformatV2/event.h"
0022 #include "kolabformatV2/journal.h"
0023 #include "kolabformatV2/note.h"
0024 #include "kolabformatV2/task.h"
0025 #include "mime/mimeutils.h"
0026 #include <kolabformat.h>
0027 
0028 #include <QUrlQuery>
0029 
0030 namespace Kolab
0031 {
0032 static inline QString eventKolabType()
0033 {
0034     return QStringLiteral(KOLAB_TYPE_EVENT);
0035 }
0036 
0037 static inline QString todoKolabType()
0038 {
0039     return QStringLiteral(KOLAB_TYPE_TASK);
0040 }
0041 
0042 static inline QString journalKolabType()
0043 {
0044     return QStringLiteral(KOLAB_TYPE_JOURNAL);
0045 }
0046 
0047 static inline QString contactKolabType()
0048 {
0049     return QStringLiteral(KOLAB_TYPE_CONTACT);
0050 }
0051 
0052 static inline QString distlistKolabType()
0053 {
0054     return QStringLiteral(KOLAB_TYPE_DISTLIST);
0055 }
0056 
0057 static inline QString distlistKolabTypeCompat()
0058 {
0059     return QStringLiteral(KOLAB_TYPE_DISTLIST_V2);
0060 }
0061 
0062 static inline QString noteKolabType()
0063 {
0064     return QStringLiteral(KOLAB_TYPE_NOTE);
0065 }
0066 
0067 static inline QString configurationKolabType()
0068 {
0069     return QStringLiteral(KOLAB_TYPE_CONFIGURATION);
0070 }
0071 
0072 static inline QString dictKolabType()
0073 {
0074     return QStringLiteral(KOLAB_TYPE_DICT);
0075 }
0076 
0077 static inline QString freebusyKolabType()
0078 {
0079     return QStringLiteral(KOLAB_TYPE_FREEBUSY);
0080 }
0081 
0082 static inline QString relationKolabType()
0083 {
0084     return QStringLiteral(KOLAB_TYPE_RELATION);
0085 }
0086 
0087 static inline QString xCalMimeType()
0088 {
0089     return QStringLiteral(MIME_TYPE_XCAL);
0090 }
0091 
0092 static inline QString xCardMimeType()
0093 {
0094     return QStringLiteral(MIME_TYPE_XCARD);
0095 }
0096 
0097 static inline QString kolabMimeType()
0098 {
0099     return QStringLiteral(MIME_TYPE_KOLAB);
0100 }
0101 
0102 KCalendarCore::Event::Ptr readV2EventXML(const QByteArray &xmlData, QStringList &attachments)
0103 {
0104     return fromXML<KCalendarCore::Event::Ptr, KolabV2::Event>(xmlData, attachments);
0105 }
0106 
0107 QString ownUrlDecode(QByteArray encodedParam)
0108 {
0109     encodedParam.replace('+', ' ');
0110     return QUrl::fromPercentEncoding(encodedParam);
0111 }
0112 
0113 RelationMember parseMemberUrl(const QString &string)
0114 {
0115     if (string.startsWith(QLatin1StringView("urn:uuid:"))) {
0116         RelationMember member;
0117         member.gid = string.mid(9);
0118         return member;
0119     }
0120     QUrl url(QUrl::fromEncoded(string.toLatin1()));
0121     QList<QByteArray> path;
0122     const QList<QByteArray> fragments = url.path(QUrl::FullyEncoded).toLatin1().split('/');
0123     path.reserve(fragments.count());
0124     for (const QByteArray &fragment : fragments) {
0125         path.append(ownUrlDecode(fragment).toUtf8());
0126     }
0127     // qCDebug(PIMKOLAB_LOG) << path;
0128     bool isShared = false;
0129     int start = path.indexOf("user");
0130     if (start < 0) {
0131         start = path.indexOf("shared");
0132         isShared = true;
0133     }
0134     if (start < 0) {
0135         qCWarning(PIMKOLAB_LOG) << R"(Couldn't find "user" or "shared" in path: )" << path;
0136         return {};
0137     }
0138     path = path.mid(start + 1);
0139     if (path.size() < 2) {
0140         qCWarning(PIMKOLAB_LOG) << "Incomplete path: " << path;
0141         return {};
0142     }
0143     RelationMember member;
0144     if (!isShared) {
0145         member.user = QString::fromUtf8(path.takeFirst());
0146     }
0147     member.uid = path.takeLast().toLong();
0148     member.mailbox = path;
0149     QUrlQuery query(url);
0150     member.messageId = ownUrlDecode(query.queryItemValue(QStringLiteral("message-id"), QUrl::FullyEncoded).toUtf8());
0151     member.subject = ownUrlDecode(query.queryItemValue(QStringLiteral("subject"), QUrl::FullyEncoded).toUtf8());
0152     member.date = ownUrlDecode(query.queryItemValue(QStringLiteral("date"), QUrl::FullyEncoded).toUtf8());
0153     // qCDebug(PIMKOLAB_LOG) << member.uid << member.mailbox;
0154     return member;
0155 }
0156 
0157 static QByteArray join(const QList<QByteArray> &list, const QByteArray &c)
0158 {
0159     QByteArray result;
0160     for (const QByteArray &a : list) {
0161         result += a + c;
0162     }
0163     result.chop(c.size());
0164     return result;
0165 }
0166 
0167 KOLAB_EXPORT QString generateMemberUrl(const RelationMember &member)
0168 {
0169     if (!member.gid.isEmpty()) {
0170         return QStringLiteral("urn:uuid:%1").arg(member.gid);
0171     }
0172     QUrl url;
0173     url.setScheme(QStringLiteral("imap"));
0174     QList<QByteArray> path;
0175     path << "/";
0176     if (!member.user.isEmpty()) {
0177         path << "user";
0178         path << QUrl::toPercentEncoding(member.user);
0179     } else {
0180         path << "shared";
0181     }
0182     const auto memberMailbox{member.mailbox};
0183     for (const QByteArray &mb : memberMailbox) {
0184         path << QUrl::toPercentEncoding(QString::fromUtf8(mb));
0185     }
0186     path << QByteArray::number(member.uid);
0187     url.setPath(QString::fromUtf8('/' + join(path, "/")), QUrl::TolerantMode);
0188 
0189     QUrlQuery query;
0190     query.addQueryItem(QStringLiteral("message-id"), member.messageId);
0191     query.addQueryItem(QStringLiteral("subject"), member.subject);
0192     query.addQueryItem(QStringLiteral("date"), member.date);
0193     url.setQuery(query);
0194 
0195     return QString::fromLatin1(url.toEncoded());
0196 }
0197 
0198 //@cond PRIVATE
0199 class KolabObjectReaderPrivate
0200 {
0201 public:
0202     KolabObjectReaderPrivate()
0203         : mAddressee(KContacts::Addressee())
0204         , mObjectType(InvalidObject)
0205         , mVersion(KolabV3)
0206         , mOverrideObjectType(InvalidObject)
0207     {
0208     }
0209 
0210     KCalendarCore::Incidence::Ptr mIncidence;
0211     KContacts::Addressee mAddressee;
0212     KContacts::ContactGroup mContactGroup;
0213     KMime::Message::Ptr mNote;
0214     QStringList mDictionary;
0215     QString mDictionaryLanguage;
0216     ObjectType mObjectType;
0217     Version mVersion;
0218     Kolab::Freebusy mFreebusy;
0219     ObjectType mOverrideObjectType;
0220     Version mOverrideVersion;
0221     bool mDoOverrideVersion = false;
0222     Akonadi::Relation mRelation;
0223     Akonadi::Tag mTag;
0224     QStringList mTagMembers;
0225 };
0226 //@endcond
0227 
0228 KolabObjectReader::KolabObjectReader()
0229     : d(new KolabObjectReaderPrivate)
0230 {
0231 }
0232 
0233 KolabObjectReader::KolabObjectReader(const KMime::Message::Ptr &msg)
0234     : d(new KolabObjectReaderPrivate)
0235 {
0236     parseMimeMessage(msg);
0237 }
0238 
0239 KolabObjectReader::~KolabObjectReader() = default;
0240 
0241 void KolabObjectReader::setObjectType(ObjectType type)
0242 {
0243     d->mOverrideObjectType = type;
0244 }
0245 
0246 void KolabObjectReader::setVersion(Version version)
0247 {
0248     d->mOverrideVersion = version;
0249     d->mDoOverrideVersion = true;
0250 }
0251 
0252 void printMessageDebugInfo(const KMime::Message::Ptr &msg)
0253 {
0254     // TODO replace by Debug stream for Mimemessage
0255     qCDebug(PIMKOLAB_LOG) << "MessageId: " << msg->messageID()->asUnicodeString();
0256     qCDebug(PIMKOLAB_LOG) << "Subject: " << msg->subject()->asUnicodeString();
0257     //     Debug() << msg->encodedContent();
0258 }
0259 
0260 ObjectType KolabObjectReader::parseMimeMessage(const KMime::Message::Ptr &msg)
0261 {
0262     ErrorHandler::clearErrors();
0263     d->mObjectType = InvalidObject;
0264     if (msg->contents().isEmpty()) {
0265         qCCritical(PIMKOLAB_LOG) << "message has no contents (we likely failed to parse it correctly)";
0266         printMessageDebugInfo(msg);
0267         return InvalidObject;
0268     }
0269     const std::string message = msg->encodedContent().toStdString();
0270     Kolab::MIMEObject mimeObject;
0271     mimeObject.setObjectType(d->mOverrideObjectType);
0272     if (d->mDoOverrideVersion) {
0273         mimeObject.setVersion(d->mOverrideVersion);
0274     }
0275     d->mObjectType = mimeObject.parseMessage(message);
0276     d->mVersion = mimeObject.getVersion();
0277     switch (mimeObject.getType()) {
0278     case EventObject: {
0279         const Kolab::Event &event = mimeObject.getEvent();
0280         d->mIncidence = Kolab::Conversion::toKCalendarCore(event);
0281         break;
0282     }
0283     case TodoObject: {
0284         const Kolab::Todo &event = mimeObject.getTodo();
0285         d->mIncidence = Kolab::Conversion::toKCalendarCore(event);
0286         break;
0287     }
0288     case JournalObject: {
0289         const Kolab::Journal &event = mimeObject.getJournal();
0290         d->mIncidence = Kolab::Conversion::toKCalendarCore(event);
0291         break;
0292     }
0293     case ContactObject: {
0294         const Kolab::Contact &contact = mimeObject.getContact();
0295         d->mAddressee = Kolab::Conversion::toKABC(contact); // TODO extract attachments
0296         break;
0297     }
0298     case DistlistObject: {
0299         const Kolab::DistList &distlist = mimeObject.getDistlist();
0300         d->mContactGroup = Kolab::Conversion::toKABC(distlist);
0301         break;
0302     }
0303     case NoteObject: {
0304         const Kolab::Note &note = mimeObject.getNote();
0305         d->mNote = Kolab::Conversion::toNote(note);
0306         break;
0307     }
0308     case DictionaryConfigurationObject: {
0309         const Kolab::Configuration &configuration = mimeObject.getConfiguration();
0310         const Kolab::Dictionary &dictionary = configuration.dictionary();
0311         d->mDictionary.clear();
0312         const std::vector<std::string> entries = dictionary.entries();
0313         d->mDictionary.reserve(entries.size());
0314         for (const std::string &entry : entries) {
0315             d->mDictionary.append(Conversion::fromStdString(entry));
0316         }
0317         d->mDictionaryLanguage = Conversion::fromStdString(dictionary.language());
0318         break;
0319     }
0320     case FreebusyObject: {
0321         const Kolab::Freebusy &fb = mimeObject.getFreebusy();
0322         d->mFreebusy = fb;
0323         break;
0324     }
0325     case RelationConfigurationObject: {
0326         const Kolab::Configuration &configuration = mimeObject.getConfiguration();
0327         const Kolab::Relation &relation = configuration.relation();
0328 
0329         if (relation.type() == "tag") {
0330             d->mTag = Akonadi::Tag();
0331             d->mTag.setName(Conversion::fromStdString(relation.name()));
0332             d->mTag.setGid(Conversion::fromStdString(configuration.uid()).toLatin1());
0333             d->mTag.setType(Akonadi::Tag::GENERIC);
0334 
0335             d->mTagMembers.reserve(relation.members().size());
0336             const auto members{relation.members()};
0337             for (const std::string &member : members) {
0338                 d->mTagMembers << Conversion::fromStdString(member);
0339             }
0340         } else if (relation.type() == "generic") {
0341             if (relation.members().size() == 2) {
0342                 d->mRelation = Akonadi::Relation();
0343                 d->mRelation.setRemoteId(Conversion::fromStdString(configuration.uid()).toLatin1());
0344                 d->mRelation.setType(Akonadi::Relation::GENERIC);
0345 
0346                 d->mTagMembers.reserve(relation.members().size());
0347                 const auto members{relation.members()};
0348                 for (const std::string &member : members) {
0349                     d->mTagMembers << Conversion::fromStdString(member);
0350                 }
0351             } else {
0352                 qCCritical(PIMKOLAB_LOG) << "generic relation had wrong number of members:" << relation.members().size();
0353                 printMessageDebugInfo(msg);
0354             }
0355         } else {
0356             qCCritical(PIMKOLAB_LOG) << "unknown configuration object type" << relation.type();
0357             printMessageDebugInfo(msg);
0358         }
0359         break;
0360     }
0361     default:
0362         qCCritical(PIMKOLAB_LOG) << "no kolab object found ";
0363         printMessageDebugInfo(msg);
0364         break;
0365     }
0366     return d->mObjectType;
0367 }
0368 
0369 Version KolabObjectReader::getVersion() const
0370 {
0371     return d->mVersion;
0372 }
0373 
0374 ObjectType KolabObjectReader::getType() const
0375 {
0376     return d->mObjectType;
0377 }
0378 
0379 KCalendarCore::Event::Ptr KolabObjectReader::getEvent() const
0380 {
0381     return d->mIncidence.dynamicCast<KCalendarCore::Event>();
0382 }
0383 
0384 KCalendarCore::Todo::Ptr KolabObjectReader::getTodo() const
0385 {
0386     return d->mIncidence.dynamicCast<KCalendarCore::Todo>();
0387 }
0388 
0389 KCalendarCore::Journal::Ptr KolabObjectReader::getJournal() const
0390 {
0391     return d->mIncidence.dynamicCast<KCalendarCore::Journal>();
0392 }
0393 
0394 KCalendarCore::Incidence::Ptr KolabObjectReader::getIncidence() const
0395 {
0396     return d->mIncidence;
0397 }
0398 
0399 KContacts::Addressee KolabObjectReader::getContact() const
0400 {
0401     return d->mAddressee;
0402 }
0403 
0404 KContacts::ContactGroup KolabObjectReader::getDistlist() const
0405 {
0406     return d->mContactGroup;
0407 }
0408 
0409 KMime::Message::Ptr KolabObjectReader::getNote() const
0410 {
0411     return d->mNote;
0412 }
0413 
0414 QStringList KolabObjectReader::getDictionary(QString &lang) const
0415 {
0416     lang = d->mDictionaryLanguage;
0417     return d->mDictionary;
0418 }
0419 
0420 Freebusy KolabObjectReader::getFreebusy() const
0421 {
0422     return d->mFreebusy;
0423 }
0424 
0425 bool KolabObjectReader::isTag() const
0426 {
0427     return !d->mTag.gid().isEmpty();
0428 }
0429 
0430 Akonadi::Tag KolabObjectReader::getTag() const
0431 {
0432     return d->mTag;
0433 }
0434 
0435 QStringList KolabObjectReader::getTagMembers() const
0436 {
0437     return d->mTagMembers;
0438 }
0439 
0440 bool KolabObjectReader::isRelation() const
0441 {
0442     return d->mRelation.isValid();
0443 }
0444 
0445 Akonadi::Relation KolabObjectReader::getRelation() const
0446 {
0447     return d->mRelation;
0448 }
0449 
0450 static KMime::Message::Ptr createMimeMessage(const std::string &mimeMessage)
0451 {
0452     KMime::Message::Ptr msg(new KMime::Message);
0453     msg->setContent(QByteArray(mimeMessage.c_str()));
0454     msg->parse();
0455     return msg;
0456 }
0457 
0458 KMime::Message::Ptr KolabObjectWriter::writeEvent(const KCalendarCore::Event::Ptr &i, Version v, const QString &productId, const QString &)
0459 {
0460     ErrorHandler::clearErrors();
0461     if (!i) {
0462         qCCritical(PIMKOLAB_LOG) << "passed a null pointer";
0463         return {};
0464     }
0465     const Kolab::Event &event = Kolab::Conversion::fromKCalendarCore(*i);
0466     Kolab::MIMEObject mimeObject;
0467     const std::string mimeMessage = mimeObject.writeEvent(event, v, productId.toStdString());
0468     return createMimeMessage(mimeMessage);
0469 }
0470 
0471 KMime::Message::Ptr KolabObjectWriter::writeTodo(const KCalendarCore::Todo::Ptr &i, Version v, const QString &productId, const QString &)
0472 {
0473     ErrorHandler::clearErrors();
0474     if (!i) {
0475         qCCritical(PIMKOLAB_LOG) << "passed a null pointer";
0476         return {};
0477     }
0478     const Kolab::Todo &todo = Kolab::Conversion::fromKCalendarCore(*i);
0479     Kolab::MIMEObject mimeObject;
0480     const std::string mimeMessage = mimeObject.writeTodo(todo, v, productId.toStdString());
0481     return createMimeMessage(mimeMessage);
0482 }
0483 
0484 KMime::Message::Ptr KolabObjectWriter::writeJournal(const KCalendarCore::Journal::Ptr &i, Version v, const QString &productId, const QString &)
0485 {
0486     ErrorHandler::clearErrors();
0487     if (!i) {
0488         qCCritical(PIMKOLAB_LOG) << "passed a null pointer";
0489         return {};
0490     }
0491     const Kolab::Journal &journal = Kolab::Conversion::fromKCalendarCore(*i);
0492     Kolab::MIMEObject mimeObject;
0493     const std::string mimeMessage = mimeObject.writeJournal(journal, v, productId.toStdString());
0494     return createMimeMessage(mimeMessage);
0495 }
0496 
0497 KMime::Message::Ptr KolabObjectWriter::writeIncidence(const KCalendarCore::Incidence::Ptr &i, Version v, const QString &productId, const QString &tz)
0498 {
0499     if (!i) {
0500         qCCritical(PIMKOLAB_LOG) << "passed a null pointer";
0501         return {};
0502     }
0503     switch (i->type()) {
0504     case KCalendarCore::IncidenceBase::TypeEvent:
0505         return writeEvent(i.dynamicCast<KCalendarCore::Event>(), v, productId, tz);
0506     case KCalendarCore::IncidenceBase::TypeTodo:
0507         return writeTodo(i.dynamicCast<KCalendarCore::Todo>(), v, productId, tz);
0508     case KCalendarCore::IncidenceBase::TypeJournal:
0509         return writeJournal(i.dynamicCast<KCalendarCore::Journal>(), v, productId, tz);
0510     default:
0511         qCCritical(PIMKOLAB_LOG) << "unknown incidence type";
0512     }
0513     return {};
0514 }
0515 
0516 KMime::Message::Ptr KolabObjectWriter::writeContact(const KContacts::Addressee &addressee, Version v, const QString &productId)
0517 {
0518     ErrorHandler::clearErrors();
0519     const Kolab::Contact &contact = Kolab::Conversion::fromKABC(addressee);
0520     Kolab::MIMEObject mimeObject;
0521     const std::string mimeMessage = mimeObject.writeContact(contact, v, productId.toStdString());
0522     return createMimeMessage(mimeMessage);
0523 }
0524 
0525 KMime::Message::Ptr KolabObjectWriter::writeDistlist(const KContacts::ContactGroup &kDistList, Version v, const QString &productId)
0526 {
0527     ErrorHandler::clearErrors();
0528     const Kolab::DistList &distlist = Kolab::Conversion::fromKABC(kDistList);
0529     Kolab::MIMEObject mimeObject;
0530     const std::string mimeMessage = mimeObject.writeDistlist(distlist, v, productId.toStdString());
0531     return createMimeMessage(mimeMessage);
0532 }
0533 
0534 KMime::Message::Ptr KolabObjectWriter::writeNote(const KMime::Message::Ptr &n, Version v, const QString &productId)
0535 {
0536     ErrorHandler::clearErrors();
0537     if (!n) {
0538         qCCritical(PIMKOLAB_LOG) << "passed a null pointer";
0539         return {};
0540     }
0541     const Kolab::Note &note = Kolab::Conversion::fromNote(n);
0542     Kolab::MIMEObject mimeObject;
0543     const std::string mimeMessage = mimeObject.writeNote(note, v, productId.toStdString());
0544     return createMimeMessage(mimeMessage);
0545 }
0546 
0547 KMime::Message::Ptr KolabObjectWriter::writeDictionary(const QStringList &entries, const QString &lang, Version v, const QString &productId)
0548 {
0549     ErrorHandler::clearErrors();
0550 
0551     Kolab::Dictionary dictionary(Conversion::toStdString(lang));
0552     std::vector<std::string> ent;
0553     ent.reserve(entries.count());
0554     for (const QString &e : entries) {
0555         ent.push_back(Conversion::toStdString(e));
0556     }
0557     dictionary.setEntries(ent);
0558     Kolab::Configuration configuration(dictionary); // TODO preserve creation/lastModified date
0559     Kolab::MIMEObject mimeObject;
0560     const std::string mimeMessage = mimeObject.writeConfiguration(configuration, v, productId.toStdString());
0561     return createMimeMessage(mimeMessage);
0562 }
0563 
0564 KMime::Message::Ptr KolabObjectWriter::writeFreebusy(const Freebusy &freebusy, Version v, const QString &productId)
0565 {
0566     ErrorHandler::clearErrors();
0567     Kolab::MIMEObject mimeObject;
0568     const std::string mimeMessage = mimeObject.writeFreebusy(freebusy, v, productId.toStdString());
0569     return createMimeMessage(mimeMessage);
0570 }
0571 
0572 KMime::Message::Ptr writeRelationHelper(const Kolab::Relation &relation, const QByteArray &uid, const QString &productId)
0573 {
0574     ErrorHandler::clearErrors();
0575     Kolab::MIMEObject mimeObject;
0576 
0577     Kolab::Configuration configuration(relation); // TODO preserve creation/lastModified date
0578     configuration.setUid(uid.constData());
0579     const std::string mimeMessage = mimeObject.writeConfiguration(configuration, Kolab::KolabV3, Conversion::toStdString(productId));
0580     return createMimeMessage(mimeMessage);
0581 }
0582 
0583 KMime::Message::Ptr KolabObjectWriter::writeTag(const Akonadi::Tag &tag, const QStringList &members, Version v, const QString &productId)
0584 {
0585     ErrorHandler::clearErrors();
0586     if (v != KolabV3) {
0587         qCCritical(PIMKOLAB_LOG) << "only v3 implementation available";
0588     }
0589 
0590     Kolab::Relation relation(Conversion::toStdString(tag.name()), "tag");
0591     std::vector<std::string> m;
0592     m.reserve(members.count());
0593     for (const QString &member : members) {
0594         m.push_back(Conversion::toStdString(member));
0595     }
0596     relation.setMembers(m);
0597 
0598     return writeRelationHelper(relation, tag.gid(), productId);
0599 }
0600 
0601 KMime::Message::Ptr KolabObjectWriter::writeRelation(const Akonadi::Relation &relation, const QStringList &items, Version v, const QString &productId)
0602 {
0603     ErrorHandler::clearErrors();
0604     if (v != KolabV3) {
0605         qCCritical(PIMKOLAB_LOG) << "only v3 implementation available";
0606     }
0607 
0608     if (items.size() != 2) {
0609         qCCritical(PIMKOLAB_LOG) << "Wrong number of members for generic relation.";
0610         return {};
0611     }
0612 
0613     Kolab::Relation kolabRelation(std::string(), "generic");
0614     std::vector<std::string> m;
0615     m.reserve(2);
0616     m.push_back(Conversion::toStdString(items.at(0)));
0617     m.push_back(Conversion::toStdString(items.at(1)));
0618     kolabRelation.setMembers(m);
0619 
0620     return writeRelationHelper(kolabRelation, relation.remoteId(), productId);
0621 }
0622 } // Namespace