File indexing completed on 2024-04-21 03:53:36

0001 /*
0002     This file is part of the KContacts framework.
0003     SPDX-FileCopyrightText: 2003 Tobias Koenig <tokoe@kde.org>
0004     SPDX-FileCopyrightText: 2015-2019 Laurent Montel <montel@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "fieldgroup.h"
0010 #include "gender.h"
0011 #include "kcontacts_debug.h"
0012 #include "key.h"
0013 #include "lang.h"
0014 #include "picture.h"
0015 #include "related.h"
0016 #include "secrecy.h"
0017 #include "sound.h"
0018 #include "vcardtool_p.h"
0019 
0020 #include <QString>
0021 #include <QTimeZone>
0022 
0023 using namespace KContacts;
0024 
0025 static bool needsEncoding(const QString &value)
0026 {
0027     int length = value.length();
0028     for (int i = 0; i < length; ++i) {
0029         char c = value.at(i).toLatin1();
0030         if ((c < 33 || c > 126) && c != ' ' && c != '=') {
0031             return true;
0032         }
0033     }
0034 
0035     return false;
0036 }
0037 
0038 struct AddressTypeInfo {
0039     const char *addressType;
0040     Address::TypeFlag flag;
0041 };
0042 
0043 static const AddressTypeInfo s_addressTypes[] = {
0044     {"dom", Address::Dom},
0045     {"home", Address::Home},
0046     {"intl", Address::Intl},
0047     {"parcel", Address::Parcel},
0048     {"postal", Address::Postal},
0049     {"pref", Address::Pref},
0050     {"work", Address::Work},
0051 };
0052 
0053 static Address::TypeFlag stringToAddressType(const QString &str)
0054 {
0055     auto it = std::find_if(std::begin(s_addressTypes), std::end(s_addressTypes), [&str](const AddressTypeInfo &info) {
0056         return str == QLatin1String(info.addressType);
0057     });
0058     return it != std::end(s_addressTypes) ? it->flag : Address::TypeFlag{};
0059 }
0060 
0061 struct PhoneTypeInfo {
0062     const char *phoneType;
0063     PhoneNumber::TypeFlag flag;
0064 };
0065 
0066 static const PhoneTypeInfo s_phoneTypes[] = {
0067     {"BBS", PhoneNumber::Bbs},
0068     {"CAR", PhoneNumber::Car},
0069     {"CELL", PhoneNumber::Cell},
0070     {"FAX", PhoneNumber::Fax},
0071     {"HOME", PhoneNumber::Home},
0072     {"ISDN", PhoneNumber::Isdn},
0073     {"MODEM", PhoneNumber::Modem},
0074     {"MSG", PhoneNumber::Msg},
0075     {"PAGER", PhoneNumber::Pager},
0076     {"PCS", PhoneNumber::Pcs},
0077     {"PREF", PhoneNumber::Pref},
0078     {"VIDEO", PhoneNumber::Video},
0079     {"VOICE", PhoneNumber::Voice},
0080     {"WORK", PhoneNumber::Work},
0081 };
0082 
0083 static PhoneNumber::TypeFlag stringToPhoneType(const QString &str)
0084 {
0085     auto it = std::find_if(std::begin(s_phoneTypes), std::end(s_phoneTypes), [&str](const PhoneTypeInfo &info) {
0086         return str == QLatin1String(info.phoneType);
0087     });
0088     return it != std::end(s_phoneTypes) ? it->flag : PhoneNumber::TypeFlag{};
0089 }
0090 
0091 VCardTool::VCardTool()
0092 {
0093 }
0094 
0095 VCardTool::~VCardTool()
0096 {
0097 }
0098 
0099 QByteArray VCardTool::exportVCards(const Addressee::List &list, VCard::Version version) const
0100 {
0101     return createVCards(list, version, true /*export vcard*/);
0102 }
0103 
0104 QByteArray VCardTool::createVCards(const Addressee::List &list, VCard::Version version) const
0105 {
0106     return createVCards(list, version, false /*don't export*/);
0107 }
0108 
0109 void VCardTool::addParameter(VCardLine *line, VCard::Version version, const QString &key, const QStringList &valueStringList) const
0110 {
0111     if (version == VCard::v2_1) {
0112         for (const QString &valueStr : valueStringList) {
0113             line->addParameter(valueStr, QString());
0114         }
0115     } else if (version == VCard::v3_0) {
0116         line->addParameter(key, valueStringList.join(QLatin1Char(',')));
0117     } else {
0118         if (valueStringList.count() < 2) {
0119             line->addParameter(key, valueStringList.join(QLatin1Char(',')));
0120         } else {
0121             line->addParameter(key, QLatin1Char('"') + valueStringList.join(QLatin1Char(',')) + QLatin1Char('"'));
0122         }
0123     }
0124 }
0125 
0126 void VCardTool::processAddresses(const Address::List &addresses, VCard::Version version, VCard *card) const
0127 {
0128     for (const auto &addr : addresses) {
0129         QStringList address;
0130 
0131         // clang-format off
0132         const bool isEmpty = addr.postOfficeBox().isEmpty()
0133                              && addr.extended().isEmpty()
0134                              && addr.street().isEmpty()
0135                              && addr.locality().isEmpty()
0136                              && addr.region().isEmpty()
0137                              && addr.postalCode().isEmpty()
0138                              && addr.country().isEmpty();
0139         // clang-format on
0140 
0141         address.append(addr.postOfficeBox().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0142         address.append(addr.extended().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0143         address.append(addr.street().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0144         address.append(addr.locality().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0145         address.append(addr.region().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0146         address.append(addr.postalCode().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0147         address.append(addr.country().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0148 
0149         const QString addressJoined(address.join(QLatin1Char(';')));
0150         VCardLine adrLine(QStringLiteral("ADR"), addressJoined);
0151         if (version == VCard::v2_1 && needsEncoding(addressJoined)) {
0152             adrLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0153             adrLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0154         }
0155 
0156         const bool hasLabel = !addr.label().isEmpty();
0157         QStringList addreLineType;
0158         QStringList labelLineType;
0159 
0160         for (const auto &info : s_addressTypes) {
0161             if (info.flag & addr.type()) {
0162                 const QString str = QString::fromLatin1(info.addressType);
0163                 addreLineType << str;
0164                 if (hasLabel) {
0165                     labelLineType << str;
0166                 }
0167             }
0168         }
0169 
0170         if (hasLabel) {
0171             if (version == VCard::v4_0) {
0172                 if (!addr.label().isEmpty()) {
0173                     adrLine.addParameter(QStringLiteral("LABEL"), QStringLiteral("\"%1\"").arg(addr.label()));
0174                 }
0175             } else {
0176                 VCardLine labelLine(QStringLiteral("LABEL"), addr.label());
0177                 if (version == VCard::v2_1 && needsEncoding(addr.label())) {
0178                     labelLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0179                     labelLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0180                 }
0181                 addParameter(&labelLine, version, QStringLiteral("TYPE"), labelLineType);
0182                 card->addLine(labelLine);
0183             }
0184         }
0185         if (version == VCard::v4_0) {
0186             Geo geo = addr.geo();
0187             if (geo.isValid()) {
0188                 QString str = QString::asprintf("\"geo:%.6f,%.6f\"", geo.latitude(), geo.longitude());
0189                 adrLine.addParameter(QStringLiteral("GEO"), str);
0190             }
0191         }
0192         if (!isEmpty) {
0193             addParameter(&adrLine, version, QStringLiteral("TYPE"), addreLineType);
0194             card->addLine(adrLine);
0195         }
0196     }
0197 }
0198 
0199 void VCardTool::processEmailList(const Email::List &emailList, VCard::Version version, VCard *card) const
0200 {
0201     for (const auto &email : emailList) {
0202         VCardLine line(QStringLiteral("EMAIL"), email.mail());
0203         const ParameterMap pMap = email.params();
0204         for (const auto &[param, l] : pMap) {
0205             QStringList list = l;
0206             if (version == VCard::v2_1) {
0207                 if (param.toLower() == QLatin1String("type")) {
0208                     bool hasPreferred = false;
0209                     const int removeItems = list.removeAll(QStringLiteral("PREF"));
0210                     if (removeItems > 0) {
0211                         hasPreferred = true;
0212                     }
0213                     if (!list.isEmpty()) {
0214                         addParameter(&line, version, param, list);
0215                     }
0216                     if (hasPreferred) {
0217                         line.addParameter(QStringLiteral("PREF"), QString());
0218                     }
0219                 } else {
0220                     line.addParameter(param, list.join(QLatin1Char(',')));
0221                 }
0222             } else {
0223                 line.addParameter(param, list.join(QLatin1Char(',')));
0224             }
0225         }
0226         card->addLine(line);
0227     }
0228 }
0229 
0230 void VCardTool::processOrganizations(const Addressee &addressee, VCard::Version version, VCard *card) const
0231 {
0232     const QList<Org> lstOrg = addressee.extraOrganizationList();
0233     for (const Org &org : lstOrg) {
0234         QStringList organization{org.organization().replace(QLatin1Char(';'), QLatin1String("\\;"))};
0235         if (!addressee.department().isEmpty()) {
0236             organization.append(addressee.department().replace(QLatin1Char(';'), QLatin1String("\\;")));
0237         }
0238         const QString orgStr = organization.join(QLatin1Char(';'));
0239         VCardLine orgLine(QStringLiteral("ORG"), orgStr);
0240         if (version == VCard::v2_1 && needsEncoding(orgStr)) {
0241             orgLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0242             orgLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0243         }
0244         orgLine.addParameters(org.params());
0245         card->addLine(orgLine);
0246     }
0247 }
0248 
0249 void VCardTool::processPhoneNumbers(const PhoneNumber::List &phoneNumbers, VCard::Version version, VCard *card) const
0250 {
0251     for (const auto &phone : phoneNumbers) {
0252         VCardLine line(QStringLiteral("TEL"), phone.number());
0253         const ParameterMap paramsMap = phone.params();
0254         for (const auto &[param, list] : paramsMap) {
0255             if (param.toUpper() != QLatin1String("TYPE")) {
0256                 line.addParameter(param, list.join(QLatin1Char(',')));
0257             }
0258         }
0259 
0260         const PhoneNumber::Type type = phone.type();
0261         QStringList lst;
0262         for (const auto &pType : s_phoneTypes) {
0263             if (pType.flag & type) {
0264                 const QString str = QString::fromLatin1(pType.phoneType);
0265                 if (version == VCard::v4_0) {
0266                     lst << str.toLower();
0267                 } else {
0268                     lst << str;
0269                 }
0270             }
0271         }
0272         if (!lst.isEmpty()) {
0273             addParameter(&line, version, QStringLiteral("TYPE"), lst);
0274         }
0275         card->addLine(line);
0276     }
0277 }
0278 
0279 void VCardTool::processCustoms(const QStringList &customs, VCard::Version version, VCard *card, bool exportVcard) const
0280 {
0281     for (const auto &str : customs) {
0282         QString identifier = QLatin1String("X-") + QStringView(str).left(str.indexOf(QLatin1Char(':')));
0283         const QString value = str.mid(str.indexOf(QLatin1Char(':')) + 1);
0284         if (value.isEmpty()) {
0285             continue;
0286         }
0287         // Convert to standard identifier
0288         if (exportVcard) {
0289             if (identifier == QLatin1String("X-messaging/aim-All")) {
0290                 identifier = QStringLiteral("X-AIM");
0291             } else if (identifier == QLatin1String("X-messaging/icq-All")) {
0292                 identifier = QStringLiteral("X-ICQ");
0293             } else if (identifier == QLatin1String("X-messaging/xmpp-All")) {
0294                 identifier = QStringLiteral("X-JABBER");
0295             } else if (identifier == QLatin1String("X-messaging/msn-All")) {
0296                 identifier = QStringLiteral("X-MSN");
0297             } else if (identifier == QLatin1String("X-messaging/yahoo-All")) {
0298                 identifier = QStringLiteral("X-YAHOO");
0299             } else if (identifier == QLatin1String("X-messaging/gadu-All")) {
0300                 identifier = QStringLiteral("X-GADUGADU");
0301             } else if (identifier == QLatin1String("X-messaging/skype-All")) {
0302                 identifier = QStringLiteral("X-SKYPE");
0303             } else if (identifier == QLatin1String("X-messaging/groupwise-All")) {
0304                 identifier = QStringLiteral("X-GROUPWISE");
0305             } else if (identifier == QLatin1String("X-messaging/sms-All")) {
0306                 identifier = QStringLiteral("X-SMS");
0307             } else if (identifier == QLatin1String("X-messaging/meanwhile-All")) {
0308                 identifier = QStringLiteral("X-MEANWHILE");
0309             } else if (identifier == QLatin1String("X-messaging/irc-All")) {
0310                 identifier = QStringLiteral("X-IRC"); // Not defined by rfc but need for fixing #300869
0311             } else if (identifier == QLatin1String("X-messaging/googletalk-All")) {
0312                 // Not defined by rfc but need for fixing #300869
0313                 identifier = QStringLiteral("X-GTALK");
0314             } else if (identifier == QLatin1String("X-messaging/twitter-All")) {
0315                 identifier = QStringLiteral("X-TWITTER");
0316             }
0317         }
0318 
0319         if (identifier.toLower() == QLatin1String("x-kaddressbook-x-anniversary") && version == VCard::v4_0) {
0320             // ANNIVERSARY
0321             if (!value.isEmpty()) {
0322                 const QDate date = QDate::fromString(value, Qt::ISODate);
0323                 QDateTime dt = QDateTime(date.startOfDay());
0324                 dt.setTime(QTime());
0325                 VCardLine line(QStringLiteral("ANNIVERSARY"), createDateTime(dt, version, false));
0326                 card->addLine(line);
0327             }
0328         } else if (identifier.toLower() == QLatin1String("x-kaddressbook-x-spousesname") && version == VCard::v4_0) {
0329             if (!value.isEmpty()) {
0330                 VCardLine line(QStringLiteral("RELATED"), QStringLiteral(";"));
0331                 line.addParameter(QStringLiteral("TYPE"), QStringLiteral("spouse"));
0332                 line.addParameter(QStringLiteral("VALUE"), value);
0333                 card->addLine(line);
0334             }
0335         } else {
0336             VCardLine line(identifier, value);
0337             if (version == VCard::v2_1 && needsEncoding(value)) {
0338                 line.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0339                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0340             }
0341             card->addLine(line);
0342         }
0343     }
0344 }
0345 
0346 QByteArray VCardTool::createVCards(const Addressee::List &list, VCard::Version version, bool exportVcard) const
0347 {
0348     VCard::List vCardList;
0349 
0350     for (const auto &addressee : list) {
0351         VCard card;
0352         // VERSION
0353         if (version == VCard::v2_1) {
0354             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("2.1")));
0355         } else if (version == VCard::v3_0) {
0356             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("3.0")));
0357         } else if (version == VCard::v4_0) {
0358             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("4.0")));
0359         }
0360 
0361         // ADR + LABEL
0362         const Address::List addresses = addressee.addresses();
0363         processAddresses(addresses, version, &card);
0364 
0365         // BDAY
0366         const bool withTime = addressee.birthdayHasTime();
0367         const QString birthdayString = createDateTime(addressee.birthday(), version, withTime);
0368         card.addLine(VCardLine(QStringLiteral("BDAY"), birthdayString));
0369 
0370         // CATEGORIES only > 2.1
0371         if (version != VCard::v2_1) {
0372             QStringList categories = addressee.categories();
0373             for (auto &cat : categories) {
0374                 cat.replace(QLatin1Char(','), QLatin1String("\\,"));
0375             }
0376 
0377             VCardLine catLine(QStringLiteral("CATEGORIES"), categories.join(QLatin1Char(',')));
0378             card.addLine(catLine);
0379         }
0380         // MEMBER (only in 4.0)
0381         if (version == VCard::v4_0) {
0382             // The KIND property must be set to "group" in order to use this property.
0383             if (addressee.kind().toLower() == QLatin1String("group")) {
0384                 const QStringList lst = addressee.members();
0385                 for (const QString &member : lst) {
0386                     card.addLine(VCardLine(QStringLiteral("MEMBER"), member));
0387                 }
0388             }
0389         }
0390         // SOURCE
0391         const QList<QUrl> lstUrl = addressee.sourcesUrlList();
0392         for (const QUrl &url : lstUrl) {
0393             VCardLine line = VCardLine(QStringLiteral("SOURCE"), url.url());
0394             card.addLine(line);
0395         }
0396 
0397         const Related::List relatedList = addressee.relationships();
0398         for (const auto &rel : relatedList) {
0399             VCardLine line(QStringLiteral("RELATED"), rel.related());
0400             line.addParameters(rel.params());
0401             card.addLine(line);
0402         }
0403         // CLASS only for version == 3.0
0404         if (version == VCard::v3_0) {
0405             card.addLine(createSecrecy(addressee.secrecy()));
0406         }
0407         // LANG only for version == 4.0
0408         if (version == VCard::v4_0) {
0409             const Lang::List langList = addressee.langs();
0410             for (const auto &lang : langList) {
0411                 VCardLine line(QStringLiteral("LANG"), lang.language());
0412                 line.addParameters(lang.params());
0413                 card.addLine(line);
0414             }
0415         }
0416         // CLIENTPIDMAP
0417         if (version == VCard::v4_0) {
0418             const ClientPidMap::List clientpidmapList = addressee.clientPidMapList();
0419             for (const auto &pMap : clientpidmapList) {
0420                 VCardLine line(QStringLiteral("CLIENTPIDMAP"), pMap.clientPidMap());
0421                 line.addParameters(pMap.params());
0422                 card.addLine(line);
0423             }
0424         }
0425         // EMAIL
0426         const Email::List emailList = addressee.emailList();
0427         processEmailList(emailList, version, &card);
0428 
0429         // FN required for only version > 2.1
0430         VCardLine fnLine(QStringLiteral("FN"), addressee.formattedName());
0431         if (version == VCard::v2_1 && needsEncoding(addressee.formattedName())) {
0432             fnLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0433             fnLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0434         }
0435         card.addLine(fnLine);
0436 
0437         // GEO
0438         const Geo geo = addressee.geo();
0439         if (geo.isValid()) {
0440             QString str;
0441             if (version == VCard::v4_0) {
0442                 str = QString::asprintf("geo:%.6f,%.6f", geo.latitude(), geo.longitude());
0443             } else {
0444                 str = QString::asprintf("%.6f;%.6f", geo.latitude(), geo.longitude());
0445             }
0446             card.addLine(VCardLine(QStringLiteral("GEO"), str));
0447         }
0448 
0449         // KEY
0450         const Key::List keys = addressee.keys();
0451         for (const auto &k : keys) {
0452             card.addLine(createKey(k, version));
0453         }
0454 
0455         // LOGO
0456         card.addLine(createPicture(QStringLiteral("LOGO"), addressee.logo(), version));
0457         const QList<Picture> lstLogo = addressee.extraLogoList();
0458         for (const Picture &logo : lstLogo) {
0459             card.addLine(createPicture(QStringLiteral("LOGO"), logo, version));
0460         }
0461 
0462         // MAILER only for version < 4.0
0463         if (version != VCard::v4_0) {
0464             VCardLine mailerLine(QStringLiteral("MAILER"), addressee.mailer());
0465             if (version == VCard::v2_1 && needsEncoding(addressee.mailer())) {
0466                 mailerLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0467                 mailerLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0468             }
0469             card.addLine(mailerLine);
0470         }
0471 
0472         // N required for only version < 4.0
0473         QStringList name;
0474         name.append(addressee.familyName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0475         name.append(addressee.givenName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0476         name.append(addressee.additionalName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0477         name.append(addressee.prefix().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0478         name.append(addressee.suffix().replace(QLatin1Char(';'), QStringLiteral("\\;")));
0479 
0480         VCardLine nLine(QStringLiteral("N"), name.join(QLatin1Char(';')));
0481         if (version == VCard::v2_1 && needsEncoding(name.join(QLatin1Char(';')))) {
0482             nLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0483             nLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0484         }
0485         if (version == VCard::v4_0 && !addressee.sortString().isEmpty()) {
0486             nLine.addParameter(QStringLiteral("SORT-AS"), addressee.sortString());
0487         }
0488 
0489         card.addLine(nLine);
0490 
0491         // NAME only for version < 4.0
0492         if (version != VCard::v4_0) {
0493             VCardLine nameLine(QStringLiteral("NAME"), addressee.name());
0494             if (version == VCard::v2_1 && needsEncoding(addressee.name())) {
0495                 nameLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0496                 nameLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0497             }
0498             card.addLine(nameLine);
0499         }
0500 
0501         // NICKNAME only for version > 2.1
0502         if (version != VCard::v2_1) {
0503             const QList<NickName> lstNickName = addressee.extraNickNameList();
0504             for (const NickName &nickName : lstNickName) {
0505                 VCardLine nickNameLine(QStringLiteral("NICKNAME"), nickName.nickname());
0506                 nickNameLine.addParameters(nickName.params());
0507 
0508                 card.addLine(nickNameLine);
0509             }
0510         }
0511 
0512         // NOTE
0513         VCardLine noteLine(QStringLiteral("NOTE"), addressee.note());
0514         if (version == VCard::v2_1 && needsEncoding(addressee.note())) {
0515             noteLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0516             noteLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0517         }
0518         card.addLine(noteLine);
0519 
0520         // ORG
0521         processOrganizations(addressee, version, &card);
0522 
0523         // PHOTO
0524         card.addLine(createPicture(QStringLiteral("PHOTO"), addressee.photo(), version));
0525         const QList<Picture> lstExtraPhoto = addressee.extraPhotoList();
0526         for (const Picture &photo : lstExtraPhoto) {
0527             card.addLine(createPicture(QStringLiteral("PHOTO"), photo, version));
0528         }
0529 
0530         // PROID only for version > 2.1
0531         if (version != VCard::v2_1) {
0532             card.addLine(VCardLine(QStringLiteral("PRODID"), addressee.productId()));
0533         }
0534 
0535         // REV
0536         card.addLine(VCardLine(QStringLiteral("REV"), createDateTime(addressee.revision(), version)));
0537 
0538         // ROLE
0539         const QList<Role> lstExtraRole = addressee.extraRoleList();
0540         for (const Role &role : lstExtraRole) {
0541             VCardLine roleLine(QStringLiteral("ROLE"), role.role());
0542             if (version == VCard::v2_1 && needsEncoding(role.role())) {
0543                 roleLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0544                 roleLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0545             }
0546             roleLine.addParameters(role.params());
0547             card.addLine(roleLine);
0548         }
0549 
0550         // SORT-STRING
0551         if (version == VCard::v3_0) {
0552             card.addLine(VCardLine(QStringLiteral("SORT-STRING"), addressee.sortString()));
0553         }
0554 
0555         // SOUND
0556         card.addLine(createSound(addressee.sound(), version));
0557         const QList<Sound> lstSound = addressee.extraSoundList();
0558         for (const Sound &sound : lstSound) {
0559             card.addLine(createSound(sound, version));
0560         }
0561 
0562         // TEL
0563         const PhoneNumber::List phoneNumbers = addressee.phoneNumbers();
0564         processPhoneNumbers(phoneNumbers, version, &card);
0565 
0566         // TITLE
0567         const QList<Title> lstTitle = addressee.extraTitleList();
0568         for (const Title &title : lstTitle) {
0569             VCardLine titleLine(QStringLiteral("TITLE"), title.title());
0570             if (version == VCard::v2_1 && needsEncoding(title.title())) {
0571                 titleLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
0572                 titleLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
0573             }
0574             titleLine.addParameters(title.params());
0575 
0576             card.addLine(titleLine);
0577         }
0578 
0579         // TZ
0580         // TODO Add vcard4.0 support
0581         const TimeZone timeZone = addressee.timeZone();
0582         if (timeZone.isValid()) {
0583             int neg = 1;
0584             if (timeZone.offset() < 0) {
0585                 neg = -1;
0586             }
0587 
0588             QString str =
0589                 QString::asprintf("%c%02d:%02d", (timeZone.offset() >= 0 ? '+' : '-'), (timeZone.offset() / 60) * neg, (timeZone.offset() % 60) * neg);
0590 
0591             card.addLine(VCardLine(QStringLiteral("TZ"), str));
0592         }
0593 
0594         // UID
0595         card.addLine(VCardLine(QStringLiteral("UID"), addressee.uid()));
0596 
0597         // URL
0598         const QList<ResourceLocatorUrl> lstExtraUrl = addressee.extraUrlList();
0599         for (const ResourceLocatorUrl &url : lstExtraUrl) {
0600             VCardLine line(QStringLiteral("URL"), url.url());
0601             line.addParameters(url.params());
0602             card.addLine(line);
0603         }
0604         if (version == VCard::v4_0) {
0605             // GENDER
0606             const Gender gender = addressee.gender();
0607             if (gender.isValid()) {
0608                 QString genderStr;
0609                 if (!gender.gender().isEmpty()) {
0610                     genderStr = gender.gender();
0611                 }
0612                 if (!gender.comment().isEmpty()) {
0613                     genderStr += QLatin1Char(';') + gender.comment();
0614                 }
0615                 VCardLine line(QStringLiteral("GENDER"), genderStr);
0616                 card.addLine(line);
0617             }
0618             // KIND
0619             if (!addressee.kind().isEmpty()) {
0620                 VCardLine line(QStringLiteral("KIND"), addressee.kind());
0621                 card.addLine(line);
0622             }
0623         }
0624         // From vcard4.
0625         if (version == VCard::v4_0) {
0626             const QList<CalendarUrl> lstCalendarUrl = addressee.calendarUrlList();
0627             for (const CalendarUrl &url : lstCalendarUrl) {
0628                 if (url.isValid()) {
0629                     QString type;
0630                     switch (url.type()) {
0631                     case CalendarUrl::Unknown:
0632                     case CalendarUrl::EndCalendarType:
0633                         break;
0634                     case CalendarUrl::FBUrl:
0635                         type = QStringLiteral("FBURL");
0636                         break;
0637                     case CalendarUrl::CALUri:
0638                         type = QStringLiteral("CALURI");
0639                         break;
0640                     case CalendarUrl::CALADRUri:
0641                         type = QStringLiteral("CALADRURI");
0642                         break;
0643                     }
0644                     if (!type.isEmpty()) {
0645                         VCardLine line(type, url.url().toDisplayString());
0646                         line.addParameters(url.params());
0647                         card.addLine(line);
0648                     }
0649                 }
0650             }
0651         }
0652 
0653         // FieldGroup
0654         const QList<FieldGroup> lstGroup = addressee.fieldGroupList();
0655         for (const FieldGroup &group : lstGroup) {
0656             VCardLine line(group.fieldGroupName(), group.value());
0657             line.addParameters(group.params());
0658             card.addLine(line);
0659         }
0660 
0661         // IMPP (supported in vcard 3 too)
0662         const QList<Impp> lstImpp = addressee.imppList();
0663         for (const Impp &impp : lstImpp) {
0664             VCardLine line(QStringLiteral("IMPP"), impp.address().url());
0665             const ParameterMap pMap = impp.params();
0666             for (const auto &[param, list] : pMap) {
0667                 if (param.toLower() != QLatin1String("x-service-type")) {
0668                     line.addParameter(param, list.join(QLatin1Char(',')));
0669                 }
0670             }
0671             card.addLine(line);
0672         }
0673 
0674         // X-
0675         const QStringList customs = addressee.customs();
0676         processCustoms(customs, version, &card, exportVcard);
0677 
0678         vCardList.append(card);
0679     }
0680 
0681     return VCardParser::createVCards(vCardList);
0682 }
0683 
0684 Addressee::List VCardTool::parseVCards(const QByteArray &vcard) const
0685 {
0686     static const QLatin1Char semicolonSep(';');
0687     static const QLatin1Char commaSep(',');
0688     QString identifier;
0689     QString group;
0690     Addressee::List addrList;
0691     const VCard::List vCardList = VCardParser::parseVCards(vcard);
0692 
0693     VCard::List::ConstIterator cardIt;
0694     VCard::List::ConstIterator listEnd(vCardList.end());
0695     for (cardIt = vCardList.begin(); cardIt != listEnd; ++cardIt) {
0696         Addressee addr;
0697 
0698         const QStringList idents = (*cardIt).identifiers();
0699         QStringList::ConstIterator identIt;
0700         QStringList::ConstIterator identEnd(idents.end());
0701         for (identIt = idents.begin(); identIt != identEnd; ++identIt) {
0702             const VCardLine::List lines = (*cardIt).lines((*identIt));
0703             VCardLine::List::ConstIterator lineIt;
0704 
0705             // iterate over the lines
0706             for (lineIt = lines.begin(); lineIt != lines.end(); ++lineIt) {
0707                 identifier = (*lineIt).identifier().toLower();
0708                 group = (*lineIt).group();
0709                 if (!group.isEmpty() && identifier != QLatin1String("adr")) {
0710                     KContacts::FieldGroup groupField(group + QLatin1Char('.') + (*lineIt).identifier());
0711                     groupField.setParams((*lineIt).parameterMap());
0712                     groupField.setValue((*lineIt).value().toString());
0713                     addr.insertFieldGroup(groupField);
0714                 }
0715                 // ADR
0716                 else if (identifier == QLatin1String("adr")) {
0717                     Address address;
0718                     const QStringList addrParts = splitString(semicolonSep, (*lineIt).value().toString());
0719                     const int addrPartsCount(addrParts.count());
0720                     if (addrPartsCount > 0) {
0721                         address.setPostOfficeBox(addrParts.at(0));
0722                     }
0723                     if (addrPartsCount > 1) {
0724                         address.setExtended(addrParts.at(1));
0725                     }
0726                     if (addrPartsCount > 2) {
0727                         address.setStreet(addrParts.at(2));
0728                     }
0729                     if (addrPartsCount > 3) {
0730                         address.setLocality(addrParts.at(3));
0731                     }
0732                     if (addrPartsCount > 4) {
0733                         address.setRegion(addrParts.at(4));
0734                     }
0735                     if (addrPartsCount > 5) {
0736                         address.setPostalCode(addrParts.at(5));
0737                     }
0738                     if (addrPartsCount > 6) {
0739                         address.setCountry(addrParts.at(6));
0740                     }
0741 
0742                     Address::Type type;
0743 
0744                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
0745                     QStringList::ConstIterator end(types.end());
0746                     for (QStringList::ConstIterator it = types.begin(); it != end; ++it) {
0747                         type |= stringToAddressType((*it).toLower());
0748                     }
0749 
0750                     address.setType(type);
0751                     QString label = (*lineIt).parameter(QStringLiteral("label"));
0752                     if (!label.isEmpty()) {
0753                         if (label.length() > 1) {
0754                             if (label.at(0) == QLatin1Char('"') && label.at(label.length() - 1) == QLatin1Char('"')) {
0755                                 label = label.mid(1, label.length() - 2);
0756                             }
0757                         }
0758                         address.setLabel(label);
0759                     }
0760                     QString geoStr = (*lineIt).parameter(QStringLiteral("geo"));
0761                     if (!geoStr.isEmpty()) {
0762                         geoStr.remove(QLatin1Char('\"'));
0763                         geoStr.remove(QStringLiteral("geo:"));
0764                         if (geoStr.contains(QLatin1Char(','))) {
0765                             QStringList arguments = geoStr.split(QLatin1Char(','));
0766                             KContacts::Geo geo;
0767                             geo.setLatitude(arguments.at(0).toDouble());
0768                             geo.setLongitude(arguments.at(1).toDouble());
0769                             address.setGeo(geo);
0770                         }
0771                     }
0772                     addr.insertAddress(address);
0773                 }
0774                 // ANNIVERSARY
0775                 else if (identifier == QLatin1String("anniversary")) {
0776                     const QString t = (*lineIt).value().toString();
0777                     const QDateTime dt(parseDateTime(t));
0778                     addr.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"), dt.date().toString(Qt::ISODate));
0779                 }
0780                 // BDAY
0781                 else if (identifier == QLatin1String("bday")) {
0782                     bool withTime;
0783                     const QDateTime bday = parseDateTime((*lineIt).value().toString(), &withTime);
0784                     addr.setBirthday(bday, withTime);
0785                 }
0786                 // CATEGORIES
0787                 else if (identifier == QLatin1String("categories")) {
0788                     const QStringList categories = splitString(commaSep, (*lineIt).value().toString());
0789                     addr.setCategories(categories);
0790                 }
0791                 // FBURL
0792                 else if (identifier == QLatin1String("fburl")) {
0793                     CalendarUrl calurl;
0794                     calurl.setType(CalendarUrl::FBUrl);
0795                     const QUrl url = QUrl((*lineIt).value().toString());
0796                     calurl.setUrl(url);
0797                     calurl.setParams((*lineIt).parameterMap());
0798                     addr.insertCalendarUrl(calurl);
0799                 }
0800                 // CALADRURI
0801                 else if (identifier == QLatin1String("caladruri")) {
0802                     CalendarUrl calurl;
0803                     calurl.setType(CalendarUrl::CALADRUri);
0804                     const QUrl url = QUrl((*lineIt).value().toString());
0805                     calurl.setUrl(url);
0806                     calurl.setParams((*lineIt).parameterMap());
0807                     addr.insertCalendarUrl(calurl);
0808                 }
0809                 // CALURI
0810                 else if (identifier == QLatin1String("caluri")) {
0811                     CalendarUrl calurl;
0812                     calurl.setType(CalendarUrl::CALUri);
0813                     const QUrl url = QUrl((*lineIt).value().toString());
0814                     calurl.setUrl(url);
0815                     calurl.setParams((*lineIt).parameterMap());
0816                     addr.insertCalendarUrl(calurl);
0817                 }
0818                 // IMPP
0819                 else if (identifier == QLatin1String("impp")) {
0820                     QUrl imppUrl((*lineIt).value().toString());
0821                     Impp impp;
0822                     impp.setParams((*lineIt).parameterMap());
0823                     if (!(*lineIt).parameter(QStringLiteral("x-service-type")).isEmpty() && imppUrl.scheme().isEmpty()) {
0824                         imppUrl.setScheme(normalizeImppServiceType((*lineIt).parameter(QStringLiteral("x-service-type")).toLower()));
0825                     }
0826                     impp.setAddress(imppUrl);
0827                     addr.insertImpp(impp);
0828                 }
0829                 // CLASS
0830                 else if (identifier == QLatin1String("class")) {
0831                     addr.setSecrecy(parseSecrecy(*lineIt));
0832                 }
0833                 // GENDER
0834                 else if (identifier == QLatin1String("gender")) {
0835                     QString genderStr = (*lineIt).value().toString();
0836                     if (!genderStr.isEmpty()) {
0837                         Gender gender;
0838                         if (genderStr.at(0) != QLatin1Char(';')) {
0839                             gender.setGender(genderStr.at(0));
0840                             if (genderStr.length() > 2 && (genderStr.at(1) == QLatin1Char(';'))) {
0841                                 gender.setComment(genderStr.right(genderStr.length() - 2));
0842                             }
0843                         } else {
0844                             gender.setComment(genderStr.right(genderStr.length() - 1));
0845                         }
0846                         addr.setGender(gender);
0847                     }
0848                 }
0849                 // LANG
0850                 else if (identifier == QLatin1String("lang")) {
0851                     Lang lang;
0852                     lang.setLanguage((*lineIt).value().toString());
0853                     lang.setParams((*lineIt).parameterMap());
0854                     addr.insertLang(lang);
0855                 }
0856                 // EMAIL
0857                 else if (identifier == QLatin1String("email")) {
0858                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
0859                     Email mail((*lineIt).value().toString());
0860                     mail.setParams((*lineIt).parameterMap());
0861                     addr.addEmail(mail);
0862                 }
0863                 // KIND
0864                 else if (identifier == QLatin1String("kind")) {
0865                     addr.setKind((*lineIt).value().toString());
0866                 }
0867                 // FN
0868                 else if (identifier == QLatin1String("fn")) {
0869                     addr.setFormattedName((*lineIt).value().toString());
0870                 }
0871                 // GEO
0872                 else if (identifier == QLatin1String("geo")) {
0873                     Geo geo;
0874                     QString lineStr = (*lineIt).value().toString();
0875                     if (lineStr.startsWith(QLatin1String("geo:"))) { // VCard 4.0
0876                         lineStr.remove(QStringLiteral("geo:"));
0877                         const QStringList geoParts = lineStr.split(QLatin1Char(','), Qt::KeepEmptyParts);
0878                         if (geoParts.size() >= 2) {
0879                             geo.setLatitude(geoParts.at(0).toFloat());
0880                             geo.setLongitude(geoParts.at(1).toFloat());
0881                             addr.setGeo(geo);
0882                         }
0883                     } else {
0884                         const QStringList geoParts = lineStr.split(QLatin1Char(';'), Qt::KeepEmptyParts);
0885                         if (geoParts.size() >= 2) {
0886                             geo.setLatitude(geoParts.at(0).toFloat());
0887                             geo.setLongitude(geoParts.at(1).toFloat());
0888                             addr.setGeo(geo);
0889                         }
0890                     }
0891                 }
0892                 // KEY
0893                 else if (identifier == QLatin1String("key")) {
0894                     addr.insertKey(parseKey(*lineIt));
0895                 }
0896                 // LABEL
0897                 else if (identifier == QLatin1String("label")) {
0898                     Address::Type type;
0899 
0900                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
0901                     QStringList::ConstIterator end(types.end());
0902                     for (QStringList::ConstIterator it = types.begin(); it != end; ++it) {
0903                         type |= stringToAddressType((*it).toLower());
0904                     }
0905 
0906                     bool available = false;
0907                     KContacts::Address::List addressList = addr.addresses();
0908                     for (KContacts::Address::List::Iterator it = addressList.begin(); it != addressList.end(); ++it) {
0909                         if ((*it).type() == type) {
0910                             (*it).setLabel((*lineIt).value().toString());
0911                             addr.insertAddress(*it);
0912                             available = true;
0913                             break;
0914                         }
0915                     }
0916 
0917                     if (!available) { // a standalone LABEL tag
0918                         KContacts::Address address(type);
0919                         address.setLabel((*lineIt).value().toString());
0920                         addr.insertAddress(address);
0921                     }
0922                 }
0923                 // LOGO
0924                 else if (identifier == QLatin1String("logo")) {
0925                     Picture picture = parsePicture(*lineIt);
0926                     if (addr.logo().isEmpty()) {
0927                         addr.setLogo(picture);
0928                     } else {
0929                         addr.insertExtraLogo(picture);
0930                     }
0931                 }
0932                 // MAILER
0933                 else if (identifier == QLatin1String("mailer")) {
0934                     addr.setMailer((*lineIt).value().toString());
0935                 }
0936                 // N
0937                 else if (identifier == QLatin1Char('n')) {
0938                     const QStringList nameParts = splitString(semicolonSep, (*lineIt).value().toString());
0939                     const int numberOfParts(nameParts.count());
0940                     if (numberOfParts > 0) {
0941                         addr.setFamilyName(nameParts.at(0));
0942                     }
0943                     if (numberOfParts > 1) {
0944                         addr.setGivenName(nameParts.at(1));
0945                     }
0946                     if (numberOfParts > 2) {
0947                         addr.setAdditionalName(nameParts.at(2));
0948                     }
0949                     if (numberOfParts > 3) {
0950                         addr.setPrefix(nameParts.at(3));
0951                     }
0952                     if (numberOfParts > 4) {
0953                         addr.setSuffix(nameParts.at(4));
0954                     }
0955                     if (!(*lineIt).parameter(QStringLiteral("sort-as")).isEmpty()) {
0956                         addr.setSortString((*lineIt).parameter(QStringLiteral("sort-as")));
0957                     }
0958                 }
0959                 // NAME
0960                 else if (identifier == QLatin1String("name")) {
0961                     addr.setName((*lineIt).value().toString());
0962                 }
0963                 // NICKNAME
0964                 else if (identifier == QLatin1String("nickname")) {
0965                     NickName nickName((*lineIt).value().toString());
0966                     nickName.setParams((*lineIt).parameterMap());
0967                     addr.insertExtraNickName(nickName);
0968                 }
0969                 // NOTE
0970                 else if (identifier == QLatin1String("note")) {
0971                     addr.setNote((*lineIt).value().toString());
0972                 }
0973                 // ORGANIZATION
0974                 else if (identifier == QLatin1String("org")) {
0975                     const QStringList orgParts = splitString(semicolonSep, (*lineIt).value().toString());
0976                     const int orgPartsCount(orgParts.count());
0977                     if (orgPartsCount > 0) {
0978                         Org organization(orgParts.at(0));
0979                         organization.setParams((*lineIt).parameterMap());
0980                         addr.insertExtraOrganization(organization);
0981                     }
0982                     if (orgPartsCount > 1) {
0983                         addr.setDepartment(orgParts.at(1));
0984                     }
0985                     if (!(*lineIt).parameter(QStringLiteral("sort-as")).isEmpty()) {
0986                         addr.setSortString((*lineIt).parameter(QStringLiteral("sort-as")));
0987                     }
0988                 }
0989                 // PHOTO
0990                 else if (identifier == QLatin1String("photo")) {
0991                     Picture picture = parsePicture(*lineIt);
0992                     if (addr.photo().isEmpty()) {
0993                         addr.setPhoto(picture);
0994                     } else {
0995                         addr.insertExtraPhoto(picture);
0996                     }
0997                 }
0998                 // PROID
0999                 else if (identifier == QLatin1String("prodid")) {
1000                     addr.setProductId((*lineIt).value().toString());
1001                 }
1002                 // REV
1003                 else if (identifier == QLatin1String("rev")) {
1004                     addr.setRevision(parseDateTime((*lineIt).value().toString()));
1005                 }
1006                 // ROLE
1007                 else if (identifier == QLatin1String("role")) {
1008                     Role role((*lineIt).value().toString());
1009                     role.setParams((*lineIt).parameterMap());
1010                     addr.insertExtraRole(role);
1011                 }
1012                 // SORT-STRING
1013                 else if (identifier == QLatin1String("sort-string")) {
1014                     addr.setSortString((*lineIt).value().toString());
1015                 }
1016                 // SOUND
1017                 else if (identifier == QLatin1String("sound")) {
1018                     Sound sound = parseSound(*lineIt);
1019                     if (addr.sound().isEmpty()) {
1020                         addr.setSound(sound);
1021                     } else {
1022                         addr.insertExtraSound(sound);
1023                     }
1024                 }
1025                 // TEL
1026                 else if (identifier == QLatin1String("tel")) {
1027                     PhoneNumber phone;
1028                     phone.setNumber((*lineIt).value().toString());
1029 
1030                     PhoneNumber::Type type;
1031                     bool foundType = false;
1032                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
1033                     QStringList::ConstIterator typeEnd(types.constEnd());
1034                     for (QStringList::ConstIterator it = types.constBegin(); it != typeEnd; ++it) {
1035                         type |= stringToPhoneType((*it).toUpper());
1036                         foundType = true;
1037                     }
1038                     phone.setType(foundType ? type : PhoneNumber::Undefined);
1039                     phone.setParams((*lineIt).parameterMap());
1040 
1041                     addr.insertPhoneNumber(phone);
1042                 }
1043                 // TITLE
1044                 else if (identifier == QLatin1String("title")) {
1045                     Title title((*lineIt).value().toString());
1046                     title.setParams((*lineIt).parameterMap());
1047                     addr.insertExtraTitle(title);
1048                 }
1049                 // TZ
1050                 else if (identifier == QLatin1String("tz")) {
1051                     // TODO add vcard4 support
1052                     TimeZone tz;
1053                     const QString date = (*lineIt).value().toString();
1054 
1055                     if (!date.isEmpty()) {
1056                         const QStringView dateView(date);
1057                         int hours = dateView.mid(1, 2).toInt();
1058                         int minutes = dateView.mid(4, 2).toInt();
1059                         int offset = (hours * 60) + minutes;
1060                         offset = offset * (date[0] == QLatin1Char('+') ? 1 : -1);
1061 
1062                         tz.setOffset(offset);
1063                         addr.setTimeZone(tz);
1064                     }
1065                 }
1066                 // UID
1067                 else if (identifier == QLatin1String("uid")) {
1068                     addr.setUid((*lineIt).value().toString());
1069                 }
1070                 // URL
1071                 else if (identifier == QLatin1String("url")) {
1072                     const QUrl url = QUrl((*lineIt).value().toString());
1073                     ResourceLocatorUrl resourceLocatorUrl;
1074                     resourceLocatorUrl.setUrl(url);
1075                     resourceLocatorUrl.setParams((*lineIt).parameterMap());
1076                     addr.insertExtraUrl(resourceLocatorUrl);
1077                 }
1078                 // SOURCE
1079                 else if (identifier == QLatin1String("source")) {
1080                     const QUrl url = QUrl((*lineIt).value().toString());
1081                     addr.insertSourceUrl(url);
1082                 }
1083                 // MEMBER (vcard 4.0)
1084                 else if (identifier == QLatin1String("member")) {
1085                     addr.insertMember((*lineIt).value().toString());
1086                 }
1087                 // RELATED (vcard 4.0)
1088                 else if (identifier == QLatin1String("related")) {
1089                     Related related;
1090                     related.setRelated((*lineIt).value().toString());
1091                     related.setParams((*lineIt).parameterMap());
1092                     addr.insertRelationship(related);
1093                 }
1094                 // CLIENTPIDMAP (vcard 4.0)
1095                 else if (identifier == QLatin1String("clientpidmap")) {
1096                     ClientPidMap clientpidmap;
1097                     clientpidmap.setClientPidMap((*lineIt).value().toString());
1098                     clientpidmap.setParams((*lineIt).parameterMap());
1099                     addr.insertClientPidMap(clientpidmap);
1100                 }
1101                 // X-
1102                 // TODO import X-GENDER
1103                 else if (identifier.startsWith(QLatin1String("x-"))) {
1104                     QString ident = (*lineIt).identifier();
1105                     // clang-format off
1106                     //X-Evolution
1107                     // also normalize case of our own extensions, some backends "adjust" that
1108                     if (identifier == QLatin1String("x-evolution-spouse")
1109                         || identifier == QLatin1String("x-spouse")) {
1110                         ident = QStringLiteral("X-KADDRESSBOOK-X-SpousesName");
1111                     } else if (identifier == QLatin1String("x-evolution-blog-url") || identifier.compare(QLatin1String("X-KADDRESSBOOK-BLOGFEED"), Qt::CaseInsensitive) == 0) {
1112                         ident = QStringLiteral("X-KADDRESSBOOK-BlogFeed");
1113                     } else if (identifier == QLatin1String("x-evolution-assistant")
1114                                || identifier == QLatin1String("x-assistant")
1115                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-ASSISTANTSNAME"), Qt::CaseInsensitive) == 0) {
1116                         ident = QStringLiteral("X-KADDRESSBOOK-X-AssistantsName");
1117                     } else if (identifier == QLatin1String("x-evolution-anniversary")
1118                                || identifier == QLatin1String("x-anniversary")
1119                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-ANNIVERSARY"), Qt::CaseInsensitive) == 0) {
1120                         ident = QStringLiteral("X-KADDRESSBOOK-X-Anniversary");
1121                     } else if (identifier == QLatin1String("x-evolution-manager")
1122                                || identifier == QLatin1String("x-manager")
1123                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-MANAGERSNAME"), Qt::CaseInsensitive) == 0) {
1124                         // clang-format on
1125                         ident = QStringLiteral("X-KADDRESSBOOK-X-ManagersName");
1126                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-PROFESSION"), Qt::CaseInsensitive) == 0) {
1127                         ident = QStringLiteral("X-KADDRESSBOOK-X-Profession");
1128                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-OFFICE"), Qt::CaseInsensitive) == 0) {
1129                         ident = QStringLiteral("X-KADDRESSBOOK-X-Office");
1130                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-SPOUSESNAME"), Qt::CaseInsensitive) == 0) {
1131                         ident = QStringLiteral("X-KADDRESSBOOK-X-SpousesName");
1132                     } else if (identifier == QLatin1String("x-aim")) {
1133                         ident = QStringLiteral("X-messaging/aim-All");
1134                     } else if (identifier == QLatin1String("x-icq")) {
1135                         ident = QStringLiteral("X-messaging/icq-All");
1136                     } else if (identifier == QLatin1String("x-jabber")) {
1137                         ident = QStringLiteral("X-messaging/xmpp-All");
1138                     } else if (identifier == QLatin1String("x-jabber")) {
1139                         ident = QStringLiteral("X-messaging/xmpp-All");
1140                     } else if (identifier == QLatin1String("x-msn")) {
1141                         ident = QStringLiteral("X-messaging/msn-All");
1142                     } else if (identifier == QLatin1String("x-yahoo")) {
1143                         ident = QStringLiteral("X-messaging/yahoo-All");
1144                     } else if (identifier == QLatin1String("x-gadugadu")) {
1145                         ident = QStringLiteral("X-messaging/gadu-All");
1146                     } else if (identifier == QLatin1String("x-skype")) {
1147                         ident = QStringLiteral("X-messaging/skype-All");
1148                     } else if (identifier == QLatin1String("x-groupwise")) {
1149                         ident = QStringLiteral("X-messaging/groupwise-All");
1150                     } else if (identifier == QLatin1String("x-sms")) {
1151                         ident = QStringLiteral("X-messaging/sms-All");
1152                     } else if (identifier == QLatin1String("x-meanwhile")) {
1153                         ident = QStringLiteral("X-messaging/meanwhile-All");
1154                     } else if (identifier == QLatin1String("x-irc")) {
1155                         ident = QStringLiteral("X-messaging/irc-All");
1156                     } else if (identifier == QLatin1String("x-gtalk")) {
1157                         ident = QStringLiteral("X-messaging/googletalk-All");
1158                     } else if (identifier == QLatin1String("x-twitter")) {
1159                         ident = QStringLiteral("X-messaging/twitter-All");
1160                     }
1161 
1162                     const QString key = ident.mid(2);
1163                     const int dash = key.indexOf(QLatin1Char('-'));
1164 
1165                     // convert legacy messaging fields into IMPP ones
1166                     if (key.startsWith(QLatin1String("messaging/"))) {
1167                         QUrl url;
1168                         url.setScheme(normalizeImppServiceType(key.mid(10, dash - 10)));
1169                         const auto values = (*lineIt).value().toString().split(QChar(0xE000), Qt::SkipEmptyParts);
1170                         for (const auto &value : values) {
1171                             url.setPath(value);
1172                             Impp impp;
1173                             impp.setParams((*lineIt).parameterMap());
1174                             impp.setAddress(url);
1175                             addr.insertImpp(impp);
1176                         }
1177                     } else {
1178                         addr.insertCustom(key.left(dash), key.mid(dash + 1), (*lineIt).value().toString());
1179                     }
1180                 }
1181             }
1182         }
1183 
1184         addrList.append(addr);
1185     }
1186 
1187     return addrList;
1188 }
1189 
1190 QDateTime VCardTool::parseDateTime(const QString &str, bool *timeValid)
1191 {
1192     static const QLatin1Char sep('-');
1193 
1194     const int posT = str.indexOf(QLatin1Char('T'));
1195     QString dateString = posT >= 0 ? str.left(posT) : str;
1196     const bool noYear = dateString.startsWith(QLatin1String("--"));
1197     dateString.remove(QLatin1Char('-'));
1198     QDate date;
1199 
1200     const QStringView dstr{dateString};
1201     if (noYear) {
1202         date.setDate(-1, dstr.mid(0, 2).toInt(), dstr.mid(2, 2).toInt());
1203     } else {
1204         // E.g. 20160120
1205         date.setDate(dstr.mid(0, 4).toInt(), dstr.mid(4, 2).toInt(), dstr.mid(6, 2).toInt());
1206     }
1207 
1208     QTime time;
1209     QTimeZone tz = QTimeZone::LocalTime;
1210     if (posT >= 0) {
1211         QString timeString = str.mid(posT + 1);
1212         timeString.remove(QLatin1Char(':'));
1213         const int zPos = timeString.indexOf(QLatin1Char('Z'));
1214         const int plusPos = timeString.indexOf(QLatin1Char('+'));
1215         const int minusPos = timeString.indexOf(sep);
1216         const int tzPos = qMax(qMax(zPos, plusPos), minusPos);
1217         const QStringView hhmmssString = tzPos >= 0 ? QStringView(timeString).left(tzPos) : QStringView(timeString);
1218         int hour = 0;
1219         int minutes = 0;
1220         int seconds = 0;
1221         switch (hhmmssString.size()) {
1222         case 2:
1223             hour = hhmmssString.toInt();
1224             break;
1225         case 4:
1226             hour = hhmmssString.mid(0, 2).toInt();
1227             minutes = hhmmssString.mid(2, 2).toInt();
1228             break;
1229         case 6:
1230             hour = hhmmssString.mid(0, 2).toInt();
1231             minutes = hhmmssString.mid(2, 2).toInt();
1232             seconds = hhmmssString.mid(4, 2).toInt();
1233             break;
1234         }
1235         time.setHMS(hour, minutes, seconds);
1236 
1237         if (tzPos >= 0) {
1238             if (zPos >= 0) {
1239                 tz = QTimeZone::UTC;
1240             } else {
1241                 int offsetSecs = 0;
1242                 const auto offsetString = QStringView(timeString).mid(tzPos + 1);
1243                 switch (offsetString.size()) {
1244                 case 2: // format: "hh"
1245                     offsetSecs = offsetString.left(2).toInt() * 3600;
1246                     break;
1247                 case 4: // format: "hhmm"
1248                     offsetSecs = offsetString.left(2).toInt() * 3600 + offsetString.mid(2, 2).toInt() * 60;
1249                     break;
1250                 }
1251                 if (minusPos >= 0) {
1252                     offsetSecs *= -1;
1253                 }
1254                 tz = QTimeZone::fromSecondsAheadOfUtc(offsetSecs);
1255             }
1256         }
1257     }
1258     if (timeValid) {
1259         *timeValid = time.isValid();
1260     }
1261 
1262     return QDateTime(date, time, tz);
1263 }
1264 
1265 QString VCardTool::createDateTime(const QDateTime &dateTime, VCard::Version version, bool withTime)
1266 {
1267     if (!dateTime.date().isValid()) {
1268         return QString();
1269     }
1270     QString str = createDate(dateTime.date(), version);
1271     if (!withTime) {
1272         return str;
1273     }
1274     str += createTime(dateTime.time(), version);
1275     if (dateTime.timeSpec() == Qt::UTC) {
1276         str += QLatin1Char('Z');
1277     } else if (dateTime.timeSpec() == Qt::OffsetFromUTC) {
1278         const int offsetSecs = dateTime.offsetFromUtc();
1279         if (offsetSecs >= 0) {
1280             str += QLatin1Char('+');
1281         } else {
1282             str += QLatin1Char('-');
1283         }
1284         QTime offsetTime = QTime(0, 0).addSecs(abs(offsetSecs));
1285         if (version == VCard::v4_0) {
1286             str += offsetTime.toString(QStringLiteral("HHmm"));
1287         } else {
1288             str += offsetTime.toString(QStringLiteral("HH:mm"));
1289         }
1290     }
1291     return str;
1292 }
1293 
1294 QString VCardTool::createDate(const QDate &date, VCard::Version version)
1295 {
1296     QString format;
1297     if (date.year() > 0) {
1298         format = QStringLiteral("yyyyMMdd");
1299     } else {
1300         format = QStringLiteral("--MMdd");
1301     }
1302     if (version != VCard::v4_0) {
1303         format.replace(QStringLiteral("yyyy"), QStringLiteral("yyyy-"));
1304         format.replace(QStringLiteral("MM"), QStringLiteral("MM-"));
1305     }
1306     return date.toString(format);
1307 }
1308 
1309 QString VCardTool::createTime(const QTime &time, VCard::Version version)
1310 {
1311     QString format;
1312     if (version == VCard::v4_0) {
1313         format = QStringLiteral("HHmmss");
1314     } else {
1315         format = QStringLiteral("HH:mm:ss");
1316     }
1317     return QLatin1Char('T') + time.toString(format);
1318 }
1319 
1320 Picture VCardTool::parsePicture(const VCardLine &line) const
1321 {
1322     Picture pic;
1323 
1324     const QStringList params = line.parameterList();
1325     QString type;
1326     if (params.contains(QLatin1String("type"))) {
1327         type = line.parameter(QStringLiteral("type"));
1328     }
1329     if (params.contains(QLatin1String("encoding"))) {
1330         pic.setRawData(line.value().toByteArray(), type);
1331     } else if (params.contains(QLatin1String("value"))) {
1332         if (line.parameter(QStringLiteral("value")).toLower() == QLatin1String("uri")) {
1333             pic.setUrl(line.value().toString());
1334         }
1335     }
1336 
1337     return pic;
1338 }
1339 
1340 VCardLine VCardTool::createPicture(const QString &identifier, const Picture &pic, VCard::Version version) const
1341 {
1342     VCardLine line(identifier);
1343 
1344     if (pic.isEmpty()) {
1345         return line;
1346     }
1347 
1348     if (pic.isIntern()) {
1349         line.setValue(pic.rawData());
1350         if (version == VCard::v2_1) {
1351             line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1352             line.addParameter(pic.type(), QString());
1353         } else { /*if (version == VCard::v3_0) */
1354             line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1355             line.addParameter(QStringLiteral("type"), pic.type());
1356 #if 0
1357         } else { //version 4.0
1358             line.addParameter(QStringLiteral("data") + QStringLiteral(":image/") + pic.type(), QStringLiteral("base64"));
1359 #endif
1360         }
1361     } else {
1362         line.setValue(pic.url());
1363         line.addParameter(QStringLiteral("value"), QStringLiteral("URI"));
1364     }
1365 
1366     return line;
1367 }
1368 
1369 Sound VCardTool::parseSound(const VCardLine &line) const
1370 {
1371     Sound snd;
1372 
1373     const QStringList params = line.parameterList();
1374     if (params.contains(QLatin1String("encoding"))) {
1375         snd.setData(line.value().toByteArray());
1376     } else if (params.contains(QLatin1String("value"))) {
1377         if (line.parameter(QStringLiteral("value")).toLower() == QLatin1String("uri")) {
1378             snd.setUrl(line.value().toString());
1379         }
1380     }
1381 
1382     /* TODO: support sound types
1383       if ( params.contains( "type" ) )
1384         snd.setType( line.parameter( "type" ) );
1385     */
1386 
1387     return snd;
1388 }
1389 
1390 VCardLine VCardTool::createSound(const Sound &snd, VCard::Version version) const
1391 {
1392     Q_UNUSED(version);
1393     VCardLine line(QStringLiteral("SOUND"));
1394 
1395     if (snd.isIntern()) {
1396         if (!snd.data().isEmpty()) {
1397             line.setValue(snd.data());
1398             if (version == VCard::v2_1) {
1399                 line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1400             } else {
1401                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1402             }
1403             // TODO: need to store sound type!!!
1404         }
1405     } else if (!snd.url().isEmpty()) {
1406         line.setValue(snd.url());
1407         line.addParameter(QStringLiteral("value"), QStringLiteral("URI"));
1408     }
1409 
1410     return line;
1411 }
1412 
1413 Key VCardTool::parseKey(const VCardLine &line) const
1414 {
1415     Key key;
1416 
1417     const QStringList params = line.parameterList();
1418     if (params.contains(QLatin1String("encoding"))) {
1419         key.setBinaryData(line.value().toByteArray());
1420     } else {
1421         key.setTextData(line.value().toString());
1422     }
1423 
1424     if (params.contains(QLatin1String("type"))) {
1425         if (line.parameter(QStringLiteral("type")).toLower() == QLatin1String("x509")) {
1426             key.setType(Key::X509);
1427         } else if (line.parameter(QStringLiteral("type")).toLower() == QLatin1String("pgp")) {
1428             key.setType(Key::PGP);
1429         } else {
1430             key.setType(Key::Custom);
1431             key.setCustomTypeString(line.parameter(QStringLiteral("type")));
1432         }
1433     } else if (params.contains(QLatin1String("mediatype"))) {
1434         const QString param = line.parameter(QStringLiteral("mediatype")).toLower();
1435         if (param == QLatin1String("application/x-x509-ca-cert")) {
1436             key.setType(Key::X509);
1437         } else if (param == QLatin1String("application/pgp-keys")) {
1438             key.setType(Key::PGP);
1439         } else {
1440             key.setType(Key::Custom);
1441             key.setCustomTypeString(line.parameter(QStringLiteral("type")));
1442         }
1443     }
1444 
1445     return key;
1446 }
1447 
1448 VCardLine VCardTool::createKey(const Key &key, VCard::Version version) const
1449 {
1450     VCardLine line(QStringLiteral("KEY"));
1451 
1452     if (key.isBinary()) {
1453         if (!key.binaryData().isEmpty()) {
1454             line.setValue(key.binaryData());
1455             if (version == VCard::v2_1) {
1456                 line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1457             } else {
1458                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1459             }
1460         }
1461     } else if (!key.textData().isEmpty()) {
1462         line.setValue(key.textData());
1463     }
1464 
1465     if (version == VCard::v4_0) {
1466         if (key.type() == Key::X509) {
1467             line.addParameter(QStringLiteral("MEDIATYPE"), QStringLiteral("application/x-x509-ca-cert"));
1468         } else if (key.type() == Key::PGP) {
1469             line.addParameter(QStringLiteral("MEDIATYPE"), QStringLiteral("application/pgp-keys"));
1470         } else if (key.type() == Key::Custom) {
1471             line.addParameter(QStringLiteral("MEDIATYPE"), key.customTypeString());
1472         }
1473     } else {
1474         if (key.type() == Key::X509) {
1475             line.addParameter(QStringLiteral("type"), QStringLiteral("X509"));
1476         } else if (key.type() == Key::PGP) {
1477             line.addParameter(QStringLiteral("type"), QStringLiteral("PGP"));
1478         } else if (key.type() == Key::Custom) {
1479             line.addParameter(QStringLiteral("type"), key.customTypeString());
1480         }
1481     }
1482 
1483     return line;
1484 }
1485 
1486 Secrecy VCardTool::parseSecrecy(const VCardLine &line) const
1487 {
1488     Secrecy secrecy;
1489 
1490     const QString value = line.value().toString().toLower();
1491     if (value == QLatin1String("public")) {
1492         secrecy.setType(Secrecy::Public);
1493     } else if (value == QLatin1String("private")) {
1494         secrecy.setType(Secrecy::Private);
1495     } else if (value == QLatin1String("confidential")) {
1496         secrecy.setType(Secrecy::Confidential);
1497     }
1498 
1499     return secrecy;
1500 }
1501 
1502 VCardLine VCardTool::createSecrecy(const Secrecy &secrecy) const
1503 {
1504     VCardLine line(QStringLiteral("CLASS"));
1505 
1506     int type = secrecy.type();
1507 
1508     if (type == Secrecy::Public) {
1509         line.setValue(QStringLiteral("PUBLIC"));
1510     } else if (type == Secrecy::Private) {
1511         line.setValue(QStringLiteral("PRIVATE"));
1512     } else if (type == Secrecy::Confidential) {
1513         line.setValue(QStringLiteral("CONFIDENTIAL"));
1514     }
1515 
1516     return line;
1517 }
1518 
1519 QStringList VCardTool::splitString(QChar sep, const QString &str) const
1520 {
1521     QStringList list;
1522     QString value(str);
1523 
1524     int start = 0;
1525     int pos = value.indexOf(sep, start);
1526 
1527     while (pos != -1) {
1528         if (pos == 0 || value[pos - 1] != QLatin1Char('\\')) {
1529             if (pos > start && pos <= value.length()) {
1530                 list << value.mid(start, pos - start);
1531             } else {
1532                 list << QString();
1533             }
1534 
1535             start = pos + 1;
1536             pos = value.indexOf(sep, start);
1537         } else {
1538             value.replace(pos - 1, 2, sep);
1539             pos = value.indexOf(sep, pos);
1540         }
1541     }
1542 
1543     int l = value.length() - 1;
1544     const QString mid = value.mid(start, l - start + 1);
1545     if (!mid.isEmpty()) {
1546         list << mid;
1547     } else {
1548         list << QString();
1549     }
1550 
1551     return list;
1552 }
1553 
1554 QString VCardTool::normalizeImppServiceType(const QString &serviceType) const
1555 {
1556     if (serviceType == QLatin1String("jabber")) {
1557         return QStringLiteral("xmpp");
1558     }
1559     if (serviceType == QLatin1String("yahoo")) {
1560         return QStringLiteral("ymsgr");
1561     }
1562     if (serviceType == QLatin1String("gadugadu")) {
1563         return QStringLiteral("gg");
1564     }
1565     return serviceType;
1566 }