File indexing completed on 2024-04-28 15:20:05

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