1 /*
2     This file is part of the KContacts framework.
3     SPDX-FileCopyrightText: 2003 Tobias Koenig <tokoe@kde.org>
4     SPDX-FileCopyrightText: 2015-2019 Laurent Montel <montel@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "fieldgroup.h"
10 #include "gender.h"
11 #include "kcontacts_debug.h"
12 #include "key.h"
13 #include "lang.h"
14 #include "picture.h"
15 #include "related.h"
16 #include "secrecy.h"
17 #include "sound.h"
18 #include "vcardtool_p.h"
19 
20 #include <QString>
21 
22 using namespace KContacts;
23 
needsEncoding(const QString & value)24 static bool needsEncoding(const QString &value)
25 {
26     int length = value.length();
27     for (int i = 0; i < length; ++i) {
28         char c = value.at(i).toLatin1();
29         if ((c < 33 || c > 126) && c != ' ' && c != '=') {
30             return true;
31         }
32     }
33 
34     return false;
35 }
36 
37 struct AddressTypeInfo {
38     const char *addressType;
39     Address::TypeFlag flag;
40 };
41 
42 static const AddressTypeInfo s_addressTypes[] = {
43     {"dom", Address::Dom},
44     {"home", Address::Home},
45     {"intl", Address::Intl},
46     {"parcel", Address::Parcel},
47     {"postal", Address::Postal},
48     {"pref", Address::Pref},
49     {"work", Address::Work},
50 };
51 
stringToAddressType(const QString & str)52 static Address::TypeFlag stringToAddressType(const QString &str)
53 {
54     auto it = std::find_if(std::begin(s_addressTypes), std::end(s_addressTypes), [&str](const AddressTypeInfo &info) {
55         return str == QLatin1String(info.addressType);
56     });
57     return it != std::end(s_addressTypes) ? it->flag : Address::TypeFlag{};
58 }
59 
60 struct PhoneTypeInfo {
61     const char *phoneType;
62     PhoneNumber::TypeFlag flag;
63 };
64 
65 static const PhoneTypeInfo s_phoneTypes[] = {
66     {"BBS", PhoneNumber::Bbs},
67     {"CAR", PhoneNumber::Car},
68     {"CELL", PhoneNumber::Cell},
69     {"FAX", PhoneNumber::Fax},
70     {"HOME", PhoneNumber::Home},
71     {"ISDN", PhoneNumber::Isdn},
72     {"MODEM", PhoneNumber::Modem},
73     {"MSG", PhoneNumber::Msg},
74     {"PAGER", PhoneNumber::Pager},
75     {"PCS", PhoneNumber::Pcs},
76     {"PREF", PhoneNumber::Pref},
77     {"VIDEO", PhoneNumber::Video},
78     {"VOICE", PhoneNumber::Voice},
79     {"WORK", PhoneNumber::Work},
80 };
81 
stringToPhoneType(const QString & str)82 static PhoneNumber::TypeFlag stringToPhoneType(const QString &str)
83 {
84     auto it = std::find_if(std::begin(s_phoneTypes), std::end(s_phoneTypes), [&str](const PhoneTypeInfo &info) {
85         return str == QLatin1String(info.phoneType);
86     });
87     return it != std::end(s_phoneTypes) ? it->flag : PhoneNumber::TypeFlag{};
88 }
89 
VCardTool()90 VCardTool::VCardTool()
91 {
92 }
93 
~VCardTool()94 VCardTool::~VCardTool()
95 {
96 }
97 
exportVCards(const Addressee::List & list,VCard::Version version) const98 QByteArray VCardTool::exportVCards(const Addressee::List &list, VCard::Version version) const
99 {
100     return createVCards(list, version, true /*export vcard*/);
101 }
102 
createVCards(const Addressee::List & list,VCard::Version version) const103 QByteArray VCardTool::createVCards(const Addressee::List &list, VCard::Version version) const
104 {
105     return createVCards(list, version, false /*don't export*/);
106 }
107 
addParameter(VCardLine * line,VCard::Version version,const QString & key,const QStringList & valueStringList) const108 void VCardTool::addParameter(VCardLine *line, VCard::Version version, const QString &key, const QStringList &valueStringList) const
109 {
110     if (version == VCard::v2_1) {
111         for (const QString &valueStr : valueStringList) {
112             line->addParameter(valueStr, QString());
113         }
114     } else if (version == VCard::v3_0) {
115         line->addParameter(key, valueStringList.join(QLatin1Char(',')));
116     } else {
117         if (valueStringList.count() < 2) {
118             line->addParameter(key, valueStringList.join(QLatin1Char(',')));
119         } else {
120             line->addParameter(key, QLatin1Char('"') + valueStringList.join(QLatin1Char(',')) + QLatin1Char('"'));
121         }
122     }
123 }
124 
processAddresses(const Address::List & addresses,VCard::Version version,VCard * card) const125 void VCardTool::processAddresses(const Address::List &addresses, VCard::Version version, VCard *card) const
126 {
127     for (const auto &addr : addresses) {
128         QStringList address;
129 
130         // clang-format off
131         const bool isEmpty = addr.postOfficeBox().isEmpty()
132                              && addr.extended().isEmpty()
133                              && addr.street().isEmpty()
134                              && addr.locality().isEmpty()
135                              && addr.region().isEmpty()
136                              && addr.postalCode().isEmpty()
137                              && addr.country().isEmpty();
138         // clang-format on
139 
140         address.append(addr.postOfficeBox().replace(QLatin1Char(';'), QStringLiteral("\\;")));
141         address.append(addr.extended().replace(QLatin1Char(';'), QStringLiteral("\\;")));
142         address.append(addr.street().replace(QLatin1Char(';'), QStringLiteral("\\;")));
143         address.append(addr.locality().replace(QLatin1Char(';'), QStringLiteral("\\;")));
144         address.append(addr.region().replace(QLatin1Char(';'), QStringLiteral("\\;")));
145         address.append(addr.postalCode().replace(QLatin1Char(';'), QStringLiteral("\\;")));
146         address.append(addr.country().replace(QLatin1Char(';'), QStringLiteral("\\;")));
147 
148         const QString addressJoined(address.join(QLatin1Char(';')));
149         VCardLine adrLine(QStringLiteral("ADR"), addressJoined);
150         if (version == VCard::v2_1 && needsEncoding(addressJoined)) {
151             adrLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
152             adrLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
153         }
154 
155         const bool hasLabel = !addr.label().isEmpty();
156         QStringList addreLineType;
157         QStringList labelLineType;
158 
159         for (const auto &info : s_addressTypes) {
160             if (info.flag & addr.type()) {
161                 const QString str = QString::fromLatin1(info.addressType);
162                 addreLineType << str;
163                 if (hasLabel) {
164                     labelLineType << str;
165                 }
166             }
167         }
168 
169         if (hasLabel) {
170             if (version == VCard::v4_0) {
171                 if (!addr.label().isEmpty()) {
172                     adrLine.addParameter(QStringLiteral("LABEL"), QStringLiteral("\"%1\"").arg(addr.label()));
173                 }
174             } else {
175                 VCardLine labelLine(QStringLiteral("LABEL"), addr.label());
176                 if (version == VCard::v2_1 && needsEncoding(addr.label())) {
177                     labelLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
178                     labelLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
179                 }
180                 addParameter(&labelLine, version, QStringLiteral("TYPE"), labelLineType);
181                 card->addLine(labelLine);
182             }
183         }
184         if (version == VCard::v4_0) {
185             Geo geo = addr.geo();
186             if (geo.isValid()) {
187                 QString str = QString::asprintf("\"geo:%.6f,%.6f\"", geo.latitude(), geo.longitude());
188                 adrLine.addParameter(QStringLiteral("GEO"), str);
189             }
190         }
191         if (!isEmpty) {
192             addParameter(&adrLine, version, QStringLiteral("TYPE"), addreLineType);
193             card->addLine(adrLine);
194         }
195     }
196 }
197 
processEmailList(const Email::List & emailList,VCard::Version version,VCard * card) const198 void VCardTool::processEmailList(const Email::List &emailList, VCard::Version version, VCard *card) const
199 {
200     for (const auto &email : emailList) {
201         VCardLine line(QStringLiteral("EMAIL"), email.mail());
202         const ParameterMap pMap = email.params();
203         for (const auto &[param, l] : pMap) {
204             QStringList list = l;
205             if (version == VCard::v2_1) {
206                 if (param.toLower() == QLatin1String("type")) {
207                     bool hasPreferred = false;
208                     const int removeItems = list.removeAll(QStringLiteral("PREF"));
209                     if (removeItems > 0) {
210                         hasPreferred = true;
211                     }
212                     if (!list.isEmpty()) {
213                         addParameter(&line, version, param, list);
214                     }
215                     if (hasPreferred) {
216                         line.addParameter(QStringLiteral("PREF"), QString());
217                     }
218                 } else {
219                     line.addParameter(param, list.join(QLatin1Char(',')));
220                 }
221             } else {
222                 line.addParameter(param, list.join(QLatin1Char(',')));
223             }
224         }
225         card->addLine(line);
226     }
227 }
228 
processOrganizations(const Addressee & addressee,VCard::Version version,VCard * card) const229 void VCardTool::processOrganizations(const Addressee &addressee, VCard::Version version, VCard *card) const
230 {
231     const QVector<Org> lstOrg = addressee.extraOrganizationList();
232     for (const Org &org : lstOrg) {
233         QStringList organization{org.organization().replace(QLatin1Char(';'), QLatin1String("\\;"))};
234         if (!addressee.department().isEmpty()) {
235             organization.append(addressee.department().replace(QLatin1Char(';'), QLatin1String("\\;")));
236         }
237         const QString orgStr = organization.join(QLatin1Char(';'));
238         VCardLine orgLine(QStringLiteral("ORG"), orgStr);
239         if (version == VCard::v2_1 && needsEncoding(orgStr)) {
240             orgLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
241             orgLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
242         }
243         orgLine.addParameters(org.params());
244         card->addLine(orgLine);
245     }
246 }
247 
processPhoneNumbers(const PhoneNumber::List & phoneNumbers,VCard::Version version,VCard * card) const248 void VCardTool::processPhoneNumbers(const PhoneNumber::List &phoneNumbers, VCard::Version version, VCard *card) const
249 {
250     for (const auto &phone : phoneNumbers) {
251         VCardLine line(QStringLiteral("TEL"), phone.number());
252         const ParameterMap paramsMap = phone.params();
253         for (const auto &[param, list] : paramsMap) {
254             if (param.toUpper() != QLatin1String("TYPE")) {
255                 line.addParameter(param, list.join(QLatin1Char(',')));
256             }
257         }
258 
259         const PhoneNumber::Type type = phone.type();
260         QStringList lst;
261         for (const auto &pType : s_phoneTypes) {
262             if (pType.flag & type) {
263                 const QString str = QString::fromLatin1(pType.phoneType);
264                 if (version == VCard::v4_0) {
265                     lst << str.toLower();
266                 } else {
267                     lst << str;
268                 }
269             }
270         }
271         if (!lst.isEmpty()) {
272             addParameter(&line, version, QStringLiteral("TYPE"), lst);
273         }
274         card->addLine(line);
275     }
276 }
277 
processCustoms(const QStringList & customs,VCard::Version version,VCard * card,bool exportVcard) const278 void VCardTool::processCustoms(const QStringList &customs, VCard::Version version, VCard *card, bool exportVcard) const
279 {
280     for (const auto &str : customs) {
281         QString identifier = QLatin1String("X-") + QStringView(str).left(str.indexOf(QLatin1Char(':')));
282         const QString value = str.mid(str.indexOf(QLatin1Char(':')) + 1);
283         if (value.isEmpty()) {
284             continue;
285         }
286         // Convert to standard identifier
287         if (exportVcard) {
288             if (identifier == QLatin1String("X-messaging/aim-All")) {
289                 identifier = QStringLiteral("X-AIM");
290             } else if (identifier == QLatin1String("X-messaging/icq-All")) {
291                 identifier = QStringLiteral("X-ICQ");
292             } else if (identifier == QLatin1String("X-messaging/xmpp-All")) {
293                 identifier = QStringLiteral("X-JABBER");
294             } else if (identifier == QLatin1String("X-messaging/msn-All")) {
295                 identifier = QStringLiteral("X-MSN");
296             } else if (identifier == QLatin1String("X-messaging/yahoo-All")) {
297                 identifier = QStringLiteral("X-YAHOO");
298             } else if (identifier == QLatin1String("X-messaging/gadu-All")) {
299                 identifier = QStringLiteral("X-GADUGADU");
300             } else if (identifier == QLatin1String("X-messaging/skype-All")) {
301                 identifier = QStringLiteral("X-SKYPE");
302             } else if (identifier == QLatin1String("X-messaging/groupwise-All")) {
303                 identifier = QStringLiteral("X-GROUPWISE");
304             } else if (identifier == QLatin1String("X-messaging/sms-All")) {
305                 identifier = QStringLiteral("X-SMS");
306             } else if (identifier == QLatin1String("X-messaging/meanwhile-All")) {
307                 identifier = QStringLiteral("X-MEANWHILE");
308             } else if (identifier == QLatin1String("X-messaging/irc-All")) {
309                 identifier = QStringLiteral("X-IRC"); // Not defined by rfc but need for fixing #300869
310             } else if (identifier == QLatin1String("X-messaging/googletalk-All")) {
311                 // Not defined by rfc but need for fixing #300869
312                 identifier = QStringLiteral("X-GTALK");
313             } else if (identifier == QLatin1String("X-messaging/twitter-All")) {
314                 identifier = QStringLiteral("X-TWITTER");
315             }
316         }
317 
318         if (identifier.toLower() == QLatin1String("x-kaddressbook-x-anniversary") && version == VCard::v4_0) {
319             // ANNIVERSARY
320             if (!value.isEmpty()) {
321                 const QDate date = QDate::fromString(value, Qt::ISODate);
322                 QDateTime dt = QDateTime(date.startOfDay());
323                 dt.setTime(QTime());
324                 VCardLine line(QStringLiteral("ANNIVERSARY"), createDateTime(dt, version, false));
325                 card->addLine(line);
326             }
327         } else if (identifier.toLower() == QLatin1String("x-kaddressbook-x-spousesname") && version == VCard::v4_0) {
328             if (!value.isEmpty()) {
329                 VCardLine line(QStringLiteral("RELATED"), QStringLiteral(";"));
330                 line.addParameter(QStringLiteral("TYPE"), QStringLiteral("spouse"));
331                 line.addParameter(QStringLiteral("VALUE"), value);
332                 card->addLine(line);
333             }
334         } else {
335             VCardLine line(identifier, value);
336             if (version == VCard::v2_1 && needsEncoding(value)) {
337                 line.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
338                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
339             }
340             card->addLine(line);
341         }
342     }
343 }
344 
createVCards(const Addressee::List & list,VCard::Version version,bool exportVcard) const345 QByteArray VCardTool::createVCards(const Addressee::List &list, VCard::Version version, bool exportVcard) const
346 {
347     VCard::List vCardList;
348 
349     for (const auto &addressee : list) {
350         VCard card;
351         // VERSION
352         if (version == VCard::v2_1) {
353             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("2.1")));
354         } else if (version == VCard::v3_0) {
355             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("3.0")));
356         } else if (version == VCard::v4_0) {
357             card.addLine(VCardLine(QStringLiteral("VERSION"), QStringLiteral("4.0")));
358         }
359 
360         // ADR + LABEL
361         const Address::List addresses = addressee.addresses();
362         processAddresses(addresses, version, &card);
363 
364         // BDAY
365         const bool withTime = addressee.birthdayHasTime();
366         const QString birthdayString = createDateTime(addressee.birthday(), version, withTime);
367         card.addLine(VCardLine(QStringLiteral("BDAY"), birthdayString));
368 
369         // Laurent: 31 Jan 2015. Not necessary to export it. When Categories were changes as AkonadiTag nobody thought that it would break categories support...
370         //=> not necessary to export just tag...
371         // CATEGORIES only > 2.1
372         if (!exportVcard) {
373             if (version != VCard::v2_1) {
374                 QStringList categories = addressee.categories();
375                 for (auto &cat : categories) {
376                     cat.replace(QLatin1Char(','), QLatin1String("\\,"));
377                 }
378 
379                 VCardLine catLine(QStringLiteral("CATEGORIES"), categories.join(QLatin1Char(',')));
380                 card.addLine(catLine);
381             }
382         }
383         // MEMBER (only in 4.0)
384         if (version == VCard::v4_0) {
385             // The KIND property must be set to "group" in order to use this property.
386             if (addressee.kind().toLower() == QLatin1String("group")) {
387                 const QStringList lst = addressee.members();
388                 for (const QString &member : lst) {
389                     card.addLine(VCardLine(QStringLiteral("MEMBER"), member));
390                 }
391             }
392         }
393         // SOURCE
394         const QVector<QUrl> lstUrl = addressee.sourcesUrlList();
395         for (const QUrl &url : lstUrl) {
396             VCardLine line = VCardLine(QStringLiteral("SOURCE"), url.url());
397             card.addLine(line);
398         }
399 
400         const Related::List relatedList = addressee.relationships();
401         for (const auto &rel : relatedList) {
402             VCardLine line(QStringLiteral("RELATED"), rel.related());
403             line.addParameters(rel.params());
404             card.addLine(line);
405         }
406         // CLASS only for version == 3.0
407         if (version == VCard::v3_0) {
408             card.addLine(createSecrecy(addressee.secrecy()));
409         }
410         // LANG only for version == 4.0
411         if (version == VCard::v4_0) {
412             const Lang::List langList = addressee.langs();
413             for (const auto &lang : langList) {
414                 VCardLine line(QStringLiteral("LANG"), lang.language());
415                 line.addParameters(lang.params());
416                 card.addLine(line);
417             }
418         }
419         // CLIENTPIDMAP
420         if (version == VCard::v4_0) {
421             const ClientPidMap::List clientpidmapList = addressee.clientPidMapList();
422             for (const auto &pMap : clientpidmapList) {
423                 VCardLine line(QStringLiteral("CLIENTPIDMAP"), pMap.clientPidMap());
424                 line.addParameters(pMap.params());
425                 card.addLine(line);
426             }
427         }
428         // EMAIL
429         const Email::List emailList = addressee.emailList();
430         processEmailList(emailList, version, &card);
431 
432         // FN required for only version > 2.1
433         VCardLine fnLine(QStringLiteral("FN"), addressee.formattedName());
434         if (version == VCard::v2_1 && needsEncoding(addressee.formattedName())) {
435             fnLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
436             fnLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
437         }
438         card.addLine(fnLine);
439 
440         // GEO
441         const Geo geo = addressee.geo();
442         if (geo.isValid()) {
443             QString str;
444             if (version == VCard::v4_0) {
445                 str = QString::asprintf("geo:%.6f,%.6f", geo.latitude(), geo.longitude());
446             } else {
447                 str = QString::asprintf("%.6f;%.6f", geo.latitude(), geo.longitude());
448             }
449             card.addLine(VCardLine(QStringLiteral("GEO"), str));
450         }
451 
452         // KEY
453         const Key::List keys = addressee.keys();
454         for (const auto &k : keys) {
455             card.addLine(createKey(k, version));
456         }
457 
458         // LOGO
459         card.addLine(createPicture(QStringLiteral("LOGO"), addressee.logo(), version));
460         const QVector<Picture> lstLogo = addressee.extraLogoList();
461         for (const Picture &logo : lstLogo) {
462             card.addLine(createPicture(QStringLiteral("LOGO"), logo, version));
463         }
464 
465         // MAILER only for version < 4.0
466         if (version != VCard::v4_0) {
467             VCardLine mailerLine(QStringLiteral("MAILER"), addressee.mailer());
468             if (version == VCard::v2_1 && needsEncoding(addressee.mailer())) {
469                 mailerLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
470                 mailerLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
471             }
472             card.addLine(mailerLine);
473         }
474 
475         // N required for only version < 4.0
476         QStringList name;
477         name.append(addressee.familyName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
478         name.append(addressee.givenName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
479         name.append(addressee.additionalName().replace(QLatin1Char(';'), QStringLiteral("\\;")));
480         name.append(addressee.prefix().replace(QLatin1Char(';'), QStringLiteral("\\;")));
481         name.append(addressee.suffix().replace(QLatin1Char(';'), QStringLiteral("\\;")));
482 
483         VCardLine nLine(QStringLiteral("N"), name.join(QLatin1Char(';')));
484         if (version == VCard::v2_1 && needsEncoding(name.join(QLatin1Char(';')))) {
485             nLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
486             nLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
487         }
488         if (version == VCard::v4_0 && !addressee.sortString().isEmpty()) {
489             nLine.addParameter(QStringLiteral("SORT-AS"), addressee.sortString());
490         }
491 
492         card.addLine(nLine);
493 
494         // NAME only for version < 4.0
495         if (version != VCard::v4_0) {
496             VCardLine nameLine(QStringLiteral("NAME"), addressee.name());
497             if (version == VCard::v2_1 && needsEncoding(addressee.name())) {
498                 nameLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
499                 nameLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
500             }
501             card.addLine(nameLine);
502         }
503 
504         // NICKNAME only for version > 2.1
505         if (version != VCard::v2_1) {
506             const QVector<NickName> lstNickName = addressee.extraNickNameList();
507             for (const NickName &nickName : lstNickName) {
508                 VCardLine nickNameLine(QStringLiteral("NICKNAME"), nickName.nickname());
509                 nickNameLine.addParameters(nickName.params());
510 
511                 card.addLine(nickNameLine);
512             }
513         }
514 
515         // NOTE
516         VCardLine noteLine(QStringLiteral("NOTE"), addressee.note());
517         if (version == VCard::v2_1 && needsEncoding(addressee.note())) {
518             noteLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
519             noteLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
520         }
521         card.addLine(noteLine);
522 
523         // ORG
524         processOrganizations(addressee, version, &card);
525 
526         // PHOTO
527         card.addLine(createPicture(QStringLiteral("PHOTO"), addressee.photo(), version));
528         const QVector<Picture> lstExtraPhoto = addressee.extraPhotoList();
529         for (const Picture &photo : lstExtraPhoto) {
530             card.addLine(createPicture(QStringLiteral("PHOTO"), photo, version));
531         }
532 
533         // PROID only for version > 2.1
534         if (version != VCard::v2_1) {
535             card.addLine(VCardLine(QStringLiteral("PRODID"), addressee.productId()));
536         }
537 
538         // REV
539         card.addLine(VCardLine(QStringLiteral("REV"), createDateTime(addressee.revision(), version)));
540 
541         // ROLE
542         const QVector<Role> lstExtraRole = addressee.extraRoleList();
543         for (const Role &role : lstExtraRole) {
544             VCardLine roleLine(QStringLiteral("ROLE"), role.role());
545             if (version == VCard::v2_1 && needsEncoding(role.role())) {
546                 roleLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
547                 roleLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
548             }
549             roleLine.addParameters(role.params());
550             card.addLine(roleLine);
551         }
552 
553         // SORT-STRING
554         if (version == VCard::v3_0) {
555             card.addLine(VCardLine(QStringLiteral("SORT-STRING"), addressee.sortString()));
556         }
557 
558         // SOUND
559         card.addLine(createSound(addressee.sound(), version));
560         const QVector<Sound> lstSound = addressee.extraSoundList();
561         for (const Sound &sound : lstSound) {
562             card.addLine(createSound(sound, version));
563         }
564 
565         // TEL
566         const PhoneNumber::List phoneNumbers = addressee.phoneNumbers();
567         processPhoneNumbers(phoneNumbers, version, &card);
568 
569         // TITLE
570         const QVector<Title> lstTitle = addressee.extraTitleList();
571         for (const Title &title : lstTitle) {
572             VCardLine titleLine(QStringLiteral("TITLE"), title.title());
573             if (version == VCard::v2_1 && needsEncoding(title.title())) {
574                 titleLine.addParameter(QStringLiteral("charset"), QStringLiteral("UTF-8"));
575                 titleLine.addParameter(QStringLiteral("encoding"), QStringLiteral("QUOTED-PRINTABLE"));
576             }
577             titleLine.addParameters(title.params());
578 
579             card.addLine(titleLine);
580         }
581 
582         // TZ
583         // TODO Add vcard4.0 support
584         const TimeZone timeZone = addressee.timeZone();
585         if (timeZone.isValid()) {
586             int neg = 1;
587             if (timeZone.offset() < 0) {
588                 neg = -1;
589             }
590 
591             QString str =
592                 QString::asprintf("%c%02d:%02d", (timeZone.offset() >= 0 ? '+' : '-'), (timeZone.offset() / 60) * neg, (timeZone.offset() % 60) * neg);
593 
594             card.addLine(VCardLine(QStringLiteral("TZ"), str));
595         }
596 
597         // UID
598         card.addLine(VCardLine(QStringLiteral("UID"), addressee.uid()));
599 
600         // URL
601         const QVector<ResourceLocatorUrl> lstExtraUrl = addressee.extraUrlList();
602         for (const ResourceLocatorUrl &url : lstExtraUrl) {
603             VCardLine line(QStringLiteral("URL"), url.url());
604             line.addParameters(url.params());
605             card.addLine(line);
606         }
607         if (version == VCard::v4_0) {
608             // GENDER
609             const Gender gender = addressee.gender();
610             if (gender.isValid()) {
611                 QString genderStr;
612                 if (!gender.gender().isEmpty()) {
613                     genderStr = gender.gender();
614                 }
615                 if (!gender.comment().isEmpty()) {
616                     genderStr += QLatin1Char(';') + gender.comment();
617                 }
618                 VCardLine line(QStringLiteral("GENDER"), genderStr);
619                 card.addLine(line);
620             }
621             // KIND
622             if (!addressee.kind().isEmpty()) {
623                 VCardLine line(QStringLiteral("KIND"), addressee.kind());
624                 card.addLine(line);
625             }
626         }
627         // From vcard4.
628         if (version == VCard::v4_0) {
629             const QVector<CalendarUrl> lstCalendarUrl = addressee.calendarUrlList();
630             for (const CalendarUrl &url : lstCalendarUrl) {
631                 if (url.isValid()) {
632                     QString type;
633                     switch (url.type()) {
634                     case CalendarUrl::Unknown:
635                     case CalendarUrl::EndCalendarType:
636                         break;
637                     case CalendarUrl::FBUrl:
638                         type = QStringLiteral("FBURL");
639                         break;
640                     case CalendarUrl::CALUri:
641                         type = QStringLiteral("CALURI");
642                         break;
643                     case CalendarUrl::CALADRUri:
644                         type = QStringLiteral("CALADRURI");
645                         break;
646                     }
647                     if (!type.isEmpty()) {
648                         VCardLine line(type, url.url().toDisplayString());
649                         line.addParameters(url.params());
650                         card.addLine(line);
651                     }
652                 }
653             }
654         }
655 
656         // FieldGroup
657         const QVector<FieldGroup> lstGroup = addressee.fieldGroupList();
658         for (const FieldGroup &group : lstGroup) {
659             VCardLine line(group.fieldGroupName(), group.value());
660             line.addParameters(group.params());
661             card.addLine(line);
662         }
663 
664         // IMPP (supported in vcard 3 too)
665         const QVector<Impp> lstImpp = addressee.imppList();
666         for (const Impp &impp : lstImpp) {
667             VCardLine line(QStringLiteral("IMPP"), impp.address().url());
668             const ParameterMap pMap = impp.params();
669             for (const auto &[param, list] : pMap) {
670                 if (param.toLower() != QLatin1String("x-service-type")) {
671                     line.addParameter(param, list.join(QLatin1Char(',')));
672                 }
673             }
674             card.addLine(line);
675         }
676 
677         // X-
678         const QStringList customs = addressee.customs();
679         processCustoms(customs, version, &card, exportVcard);
680 
681         vCardList.append(card);
682     }
683 
684     return VCardParser::createVCards(vCardList);
685 }
686 
parseVCards(const QByteArray & vcard) const687 Addressee::List VCardTool::parseVCards(const QByteArray &vcard) const
688 {
689     static const QLatin1Char semicolonSep(';');
690     static const QLatin1Char commaSep(',');
691     QString identifier;
692     QString group;
693     Addressee::List addrList;
694     const VCard::List vCardList = VCardParser::parseVCards(vcard);
695 
696     VCard::List::ConstIterator cardIt;
697     VCard::List::ConstIterator listEnd(vCardList.end());
698     for (cardIt = vCardList.begin(); cardIt != listEnd; ++cardIt) {
699         Addressee addr;
700 
701         const QStringList idents = (*cardIt).identifiers();
702         QStringList::ConstIterator identIt;
703         QStringList::ConstIterator identEnd(idents.end());
704         for (identIt = idents.begin(); identIt != identEnd; ++identIt) {
705             const VCardLine::List lines = (*cardIt).lines((*identIt));
706             VCardLine::List::ConstIterator lineIt;
707 
708             // iterate over the lines
709             for (lineIt = lines.begin(); lineIt != lines.end(); ++lineIt) {
710                 identifier = (*lineIt).identifier().toLower();
711                 group = (*lineIt).group();
712                 if (!group.isEmpty() && identifier != QLatin1String("adr")) {
713                     KContacts::FieldGroup groupField(group + QLatin1Char('.') + (*lineIt).identifier());
714                     groupField.setParams((*lineIt).parameterMap());
715                     groupField.setValue((*lineIt).value().toString());
716                     addr.insertFieldGroup(groupField);
717                 }
718                 // ADR
719                 else if (identifier == QLatin1String("adr")) {
720                     Address address;
721                     const QStringList addrParts = splitString(semicolonSep, (*lineIt).value().toString());
722                     const int addrPartsCount(addrParts.count());
723                     if (addrPartsCount > 0) {
724                         address.setPostOfficeBox(addrParts.at(0));
725                     }
726                     if (addrPartsCount > 1) {
727                         address.setExtended(addrParts.at(1));
728                     }
729                     if (addrPartsCount > 2) {
730                         address.setStreet(addrParts.at(2));
731                     }
732                     if (addrPartsCount > 3) {
733                         address.setLocality(addrParts.at(3));
734                     }
735                     if (addrPartsCount > 4) {
736                         address.setRegion(addrParts.at(4));
737                     }
738                     if (addrPartsCount > 5) {
739                         address.setPostalCode(addrParts.at(5));
740                     }
741                     if (addrPartsCount > 6) {
742                         address.setCountry(addrParts.at(6));
743                     }
744 
745                     Address::Type type;
746 
747                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
748                     QStringList::ConstIterator end(types.end());
749                     for (QStringList::ConstIterator it = types.begin(); it != end; ++it) {
750                         type |= stringToAddressType((*it).toLower());
751                     }
752 
753                     address.setType(type);
754                     QString label = (*lineIt).parameter(QStringLiteral("label"));
755                     if (!label.isEmpty()) {
756                         if (label.length() > 1) {
757                             if (label.at(0) == QLatin1Char('"') && label.at(label.length() - 1) == QLatin1Char('"')) {
758                                 label = label.mid(1, label.length() - 2);
759                             }
760                         }
761                         address.setLabel(label);
762                     }
763                     QString geoStr = (*lineIt).parameter(QStringLiteral("geo"));
764                     if (!geoStr.isEmpty()) {
765                         geoStr.remove(QLatin1Char('\"'));
766                         geoStr.remove(QStringLiteral("geo:"));
767                         if (geoStr.contains(QLatin1Char(','))) {
768                             QStringList arguments = geoStr.split(QLatin1Char(','));
769                             KContacts::Geo geo;
770                             geo.setLatitude(arguments.at(0).toDouble());
771                             geo.setLongitude(arguments.at(1).toDouble());
772                             address.setGeo(geo);
773                         }
774                     }
775                     addr.insertAddress(address);
776                 }
777                 // ANNIVERSARY
778                 else if (identifier == QLatin1String("anniversary")) {
779                     const QString t = (*lineIt).value().toString();
780                     const QDateTime dt(parseDateTime(t));
781                     addr.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"), dt.date().toString(Qt::ISODate));
782                 }
783                 // BDAY
784                 else if (identifier == QLatin1String("bday")) {
785                     bool withTime;
786                     const QDateTime bday = parseDateTime((*lineIt).value().toString(), &withTime);
787                     addr.setBirthday(bday, withTime);
788                 }
789                 // CATEGORIES
790                 else if (identifier == QLatin1String("categories")) {
791                     const QStringList categories = splitString(commaSep, (*lineIt).value().toString());
792                     addr.setCategories(categories);
793                 }
794                 // FBURL
795                 else if (identifier == QLatin1String("fburl")) {
796                     CalendarUrl calurl;
797                     calurl.setType(CalendarUrl::FBUrl);
798                     const QUrl url = QUrl((*lineIt).value().toString());
799                     calurl.setUrl(url);
800                     calurl.setParams((*lineIt).parameterMap());
801                     addr.insertCalendarUrl(calurl);
802                 }
803                 // CALADRURI
804                 else if (identifier == QLatin1String("caladruri")) {
805                     CalendarUrl calurl;
806                     calurl.setType(CalendarUrl::CALADRUri);
807                     const QUrl url = QUrl((*lineIt).value().toString());
808                     calurl.setUrl(url);
809                     calurl.setParams((*lineIt).parameterMap());
810                     addr.insertCalendarUrl(calurl);
811                 }
812                 // CALURI
813                 else if (identifier == QLatin1String("caluri")) {
814                     CalendarUrl calurl;
815                     calurl.setType(CalendarUrl::CALUri);
816                     const QUrl url = QUrl((*lineIt).value().toString());
817                     calurl.setUrl(url);
818                     calurl.setParams((*lineIt).parameterMap());
819                     addr.insertCalendarUrl(calurl);
820                 }
821                 // IMPP
822                 else if (identifier == QLatin1String("impp")) {
823                     QUrl imppUrl((*lineIt).value().toString());
824                     Impp impp;
825                     impp.setParams((*lineIt).parameterMap());
826                     if (!(*lineIt).parameter(QStringLiteral("x-service-type")).isEmpty() && imppUrl.scheme().isEmpty()) {
827                         imppUrl.setScheme(normalizeImppServiceType((*lineIt).parameter(QStringLiteral("x-service-type")).toLower()));
828                     }
829                     impp.setAddress(imppUrl);
830                     addr.insertImpp(impp);
831                 }
832                 // CLASS
833                 else if (identifier == QLatin1String("class")) {
834                     addr.setSecrecy(parseSecrecy(*lineIt));
835                 }
836                 // GENDER
837                 else if (identifier == QLatin1String("gender")) {
838                     QString genderStr = (*lineIt).value().toString();
839                     if (!genderStr.isEmpty()) {
840                         Gender gender;
841                         if (genderStr.at(0) != QLatin1Char(';')) {
842                             gender.setGender(genderStr.at(0));
843                             if (genderStr.length() > 2 && (genderStr.at(1) == QLatin1Char(';'))) {
844                                 gender.setComment(genderStr.right(genderStr.length() - 2));
845                             }
846                         } else {
847                             gender.setComment(genderStr.right(genderStr.length() - 1));
848                         }
849                         addr.setGender(gender);
850                     }
851                 }
852                 // LANG
853                 else if (identifier == QLatin1String("lang")) {
854                     Lang lang;
855                     lang.setLanguage((*lineIt).value().toString());
856                     lang.setParams((*lineIt).parameterMap());
857                     addr.insertLang(lang);
858                 }
859                 // EMAIL
860                 else if (identifier == QLatin1String("email")) {
861                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
862                     Email mail((*lineIt).value().toString());
863                     mail.setParams((*lineIt).parameterMap());
864                     addr.addEmail(mail);
865                 }
866                 // KIND
867                 else if (identifier == QLatin1String("kind")) {
868                     addr.setKind((*lineIt).value().toString());
869                 }
870                 // FN
871                 else if (identifier == QLatin1String("fn")) {
872                     addr.setFormattedName((*lineIt).value().toString());
873                 }
874                 // GEO
875                 else if (identifier == QLatin1String("geo")) {
876                     Geo geo;
877                     QString lineStr = (*lineIt).value().toString();
878                     if (lineStr.startsWith(QLatin1String("geo:"))) { // VCard 4.0
879                         lineStr.remove(QStringLiteral("geo:"));
880                         const QStringList geoParts = lineStr.split(QLatin1Char(','), Qt::KeepEmptyParts);
881                         if (geoParts.size() >= 2) {
882                             geo.setLatitude(geoParts.at(0).toFloat());
883                             geo.setLongitude(geoParts.at(1).toFloat());
884                             addr.setGeo(geo);
885                         }
886                     } else {
887                         const QStringList geoParts = lineStr.split(QLatin1Char(';'), Qt::KeepEmptyParts);
888                         if (geoParts.size() >= 2) {
889                             geo.setLatitude(geoParts.at(0).toFloat());
890                             geo.setLongitude(geoParts.at(1).toFloat());
891                             addr.setGeo(geo);
892                         }
893                     }
894                 }
895                 // KEY
896                 else if (identifier == QLatin1String("key")) {
897                     addr.insertKey(parseKey(*lineIt));
898                 }
899                 // LABEL
900                 else if (identifier == QLatin1String("label")) {
901                     Address::Type type;
902 
903                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
904                     QStringList::ConstIterator end(types.end());
905                     for (QStringList::ConstIterator it = types.begin(); it != end; ++it) {
906                         type |= stringToAddressType((*it).toLower());
907                     }
908 
909                     bool available = false;
910                     KContacts::Address::List addressList = addr.addresses();
911                     for (KContacts::Address::List::Iterator it = addressList.begin(); it != addressList.end(); ++it) {
912                         if ((*it).type() == type) {
913                             (*it).setLabel((*lineIt).value().toString());
914                             addr.insertAddress(*it);
915                             available = true;
916                             break;
917                         }
918                     }
919 
920                     if (!available) { // a standalone LABEL tag
921                         KContacts::Address address(type);
922                         address.setLabel((*lineIt).value().toString());
923                         addr.insertAddress(address);
924                     }
925                 }
926                 // LOGO
927                 else if (identifier == QLatin1String("logo")) {
928                     Picture picture = parsePicture(*lineIt);
929                     if (addr.logo().isEmpty()) {
930                         addr.setLogo(picture);
931                     } else {
932                         addr.insertExtraLogo(picture);
933                     }
934                 }
935                 // MAILER
936                 else if (identifier == QLatin1String("mailer")) {
937                     addr.setMailer((*lineIt).value().toString());
938                 }
939                 // N
940                 else if (identifier == QLatin1Char('n')) {
941                     const QStringList nameParts = splitString(semicolonSep, (*lineIt).value().toString());
942                     const int numberOfParts(nameParts.count());
943                     if (numberOfParts > 0) {
944                         addr.setFamilyName(nameParts.at(0));
945                     }
946                     if (numberOfParts > 1) {
947                         addr.setGivenName(nameParts.at(1));
948                     }
949                     if (numberOfParts > 2) {
950                         addr.setAdditionalName(nameParts.at(2));
951                     }
952                     if (numberOfParts > 3) {
953                         addr.setPrefix(nameParts.at(3));
954                     }
955                     if (numberOfParts > 4) {
956                         addr.setSuffix(nameParts.at(4));
957                     }
958                     if (!(*lineIt).parameter(QStringLiteral("sort-as")).isEmpty()) {
959                         addr.setSortString((*lineIt).parameter(QStringLiteral("sort-as")));
960                     }
961                 }
962                 // NAME
963                 else if (identifier == QLatin1String("name")) {
964                     addr.setName((*lineIt).value().toString());
965                 }
966                 // NICKNAME
967                 else if (identifier == QLatin1String("nickname")) {
968                     NickName nickName((*lineIt).value().toString());
969                     nickName.setParams((*lineIt).parameterMap());
970                     addr.insertExtraNickName(nickName);
971                 }
972                 // NOTE
973                 else if (identifier == QLatin1String("note")) {
974                     addr.setNote((*lineIt).value().toString());
975                 }
976                 // ORGANIZATION
977                 else if (identifier == QLatin1String("org")) {
978                     const QStringList orgParts = splitString(semicolonSep, (*lineIt).value().toString());
979                     const int orgPartsCount(orgParts.count());
980                     if (orgPartsCount > 0) {
981                         Org organization(orgParts.at(0));
982                         organization.setParams((*lineIt).parameterMap());
983                         addr.insertExtraOrganization(organization);
984                     }
985                     if (orgPartsCount > 1) {
986                         addr.setDepartment(orgParts.at(1));
987                     }
988                     if (!(*lineIt).parameter(QStringLiteral("sort-as")).isEmpty()) {
989                         addr.setSortString((*lineIt).parameter(QStringLiteral("sort-as")));
990                     }
991                 }
992                 // PHOTO
993                 else if (identifier == QLatin1String("photo")) {
994                     Picture picture = parsePicture(*lineIt);
995                     if (addr.photo().isEmpty()) {
996                         addr.setPhoto(picture);
997                     } else {
998                         addr.insertExtraPhoto(picture);
999                     }
1000                 }
1001                 // PROID
1002                 else if (identifier == QLatin1String("prodid")) {
1003                     addr.setProductId((*lineIt).value().toString());
1004                 }
1005                 // REV
1006                 else if (identifier == QLatin1String("rev")) {
1007                     addr.setRevision(parseDateTime((*lineIt).value().toString()));
1008                 }
1009                 // ROLE
1010                 else if (identifier == QLatin1String("role")) {
1011                     Role role((*lineIt).value().toString());
1012                     role.setParams((*lineIt).parameterMap());
1013                     addr.insertExtraRole(role);
1014                 }
1015                 // SORT-STRING
1016                 else if (identifier == QLatin1String("sort-string")) {
1017                     addr.setSortString((*lineIt).value().toString());
1018                 }
1019                 // SOUND
1020                 else if (identifier == QLatin1String("sound")) {
1021                     Sound sound = parseSound(*lineIt);
1022                     if (addr.sound().isEmpty()) {
1023                         addr.setSound(sound);
1024                     } else {
1025                         addr.insertExtraSound(sound);
1026                     }
1027                 }
1028                 // TEL
1029                 else if (identifier == QLatin1String("tel")) {
1030                     PhoneNumber phone;
1031                     phone.setNumber((*lineIt).value().toString());
1032 
1033                     PhoneNumber::Type type;
1034                     bool foundType = false;
1035                     const QStringList types = (*lineIt).parameters(QStringLiteral("type"));
1036                     QStringList::ConstIterator typeEnd(types.constEnd());
1037                     for (QStringList::ConstIterator it = types.constBegin(); it != typeEnd; ++it) {
1038                         type |= stringToPhoneType((*it).toUpper());
1039                         foundType = true;
1040                     }
1041                     phone.setType(foundType ? type : PhoneNumber::Undefined);
1042                     phone.setParams((*lineIt).parameterMap());
1043 
1044                     addr.insertPhoneNumber(phone);
1045                 }
1046                 // TITLE
1047                 else if (identifier == QLatin1String("title")) {
1048                     Title title((*lineIt).value().toString());
1049                     title.setParams((*lineIt).parameterMap());
1050                     addr.insertExtraTitle(title);
1051                 }
1052                 // TZ
1053                 else if (identifier == QLatin1String("tz")) {
1054                     // TODO add vcard4 support
1055                     TimeZone tz;
1056                     const QString date = (*lineIt).value().toString();
1057 
1058                     if (!date.isEmpty()) {
1059 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1060                         const QStringView dateView(date);
1061                         int hours = dateView.mid(1, 2).toInt();
1062                         int minutes = dateView.mid(4, 2).toInt();
1063 #else
1064                         int hours = date.midRef(1, 2).toInt();
1065                         int minutes = date.midRef(4, 2).toInt();
1066 #endif
1067                         int offset = (hours * 60) + minutes;
1068                         offset = offset * (date[0] == QLatin1Char('+') ? 1 : -1);
1069 
1070                         tz.setOffset(offset);
1071                         addr.setTimeZone(tz);
1072                     }
1073                 }
1074                 // UID
1075                 else if (identifier == QLatin1String("uid")) {
1076                     addr.setUid((*lineIt).value().toString());
1077                 }
1078                 // URL
1079                 else if (identifier == QLatin1String("url")) {
1080                     const QUrl url = QUrl((*lineIt).value().toString());
1081                     ResourceLocatorUrl resourceLocatorUrl;
1082                     resourceLocatorUrl.setUrl(url);
1083                     resourceLocatorUrl.setParams((*lineIt).parameterMap());
1084                     addr.insertExtraUrl(resourceLocatorUrl);
1085                 }
1086                 // SOURCE
1087                 else if (identifier == QLatin1String("source")) {
1088                     const QUrl url = QUrl((*lineIt).value().toString());
1089                     addr.insertSourceUrl(url);
1090                 }
1091                 // MEMBER (vcard 4.0)
1092                 else if (identifier == QLatin1String("member")) {
1093                     addr.insertMember((*lineIt).value().toString());
1094                 }
1095                 // RELATED (vcard 4.0)
1096                 else if (identifier == QLatin1String("related")) {
1097                     Related related;
1098                     related.setRelated((*lineIt).value().toString());
1099                     related.setParams((*lineIt).parameterMap());
1100                     addr.insertRelationship(related);
1101                 }
1102                 // CLIENTPIDMAP (vcard 4.0)
1103                 else if (identifier == QLatin1String("clientpidmap")) {
1104                     ClientPidMap clientpidmap;
1105                     clientpidmap.setClientPidMap((*lineIt).value().toString());
1106                     clientpidmap.setParams((*lineIt).parameterMap());
1107                     addr.insertClientPidMap(clientpidmap);
1108                 }
1109                 // X-
1110                 // TODO import X-GENDER
1111                 else if (identifier.startsWith(QLatin1String("x-"))) {
1112                     QString ident = (*lineIt).identifier();
1113                     // clang-format off
1114                     //X-Evolution
1115                     // also normalize case of our own extensions, some backends "adjust" that
1116                     if (identifier == QLatin1String("x-evolution-spouse")
1117                         || identifier == QLatin1String("x-spouse")) {
1118                         ident = QStringLiteral("X-KADDRESSBOOK-X-SpousesName");
1119                     } else if (identifier == QLatin1String("x-evolution-blog-url") || identifier.compare(QLatin1String("X-KADDRESSBOOK-BLOGFEED"), Qt::CaseInsensitive) == 0) {
1120                         ident = QStringLiteral("X-KADDRESSBOOK-BlogFeed");
1121                     } else if (identifier == QLatin1String("x-evolution-assistant")
1122                                || identifier == QLatin1String("x-assistant")
1123                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-ASSISTANTSNAME"), Qt::CaseInsensitive) == 0) {
1124                         ident = QStringLiteral("X-KADDRESSBOOK-X-AssistantsName");
1125                     } else if (identifier == QLatin1String("x-evolution-anniversary")
1126                                || identifier == QLatin1String("x-anniversary")
1127                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-ANNIVERSARY"), Qt::CaseInsensitive) == 0) {
1128                         ident = QStringLiteral("X-KADDRESSBOOK-X-Anniversary");
1129                     } else if (identifier == QLatin1String("x-evolution-manager")
1130                                || identifier == QLatin1String("x-manager")
1131                                || identifier.compare(QLatin1String("X-KADDRESSBOOK-X-MANAGERSNAME"), Qt::CaseInsensitive) == 0) {
1132                         // clang-format on
1133                         ident = QStringLiteral("X-KADDRESSBOOK-X-ManagersName");
1134                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-PROFESSION"), Qt::CaseInsensitive) == 0) {
1135                         ident = QStringLiteral("X-KADDRESSBOOK-X-Profession");
1136                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-OFFICE"), Qt::CaseInsensitive) == 0) {
1137                         ident = QStringLiteral("X-KADDRESSBOOK-X-Office");
1138                     } else if (identifier.compare(QLatin1String("X-KADDRESSBOOK-X-SPOUSESNAME"), Qt::CaseInsensitive) == 0) {
1139                         ident = QStringLiteral("X-KADDRESSBOOK-X-SpousesName");
1140                     } else if (identifier == QLatin1String("x-aim")) {
1141                         ident = QStringLiteral("X-messaging/aim-All");
1142                     } else if (identifier == QLatin1String("x-icq")) {
1143                         ident = QStringLiteral("X-messaging/icq-All");
1144                     } else if (identifier == QLatin1String("x-jabber")) {
1145                         ident = QStringLiteral("X-messaging/xmpp-All");
1146                     } else if (identifier == QLatin1String("x-jabber")) {
1147                         ident = QStringLiteral("X-messaging/xmpp-All");
1148                     } else if (identifier == QLatin1String("x-msn")) {
1149                         ident = QStringLiteral("X-messaging/msn-All");
1150                     } else if (identifier == QLatin1String("x-yahoo")) {
1151                         ident = QStringLiteral("X-messaging/yahoo-All");
1152                     } else if (identifier == QLatin1String("x-gadugadu")) {
1153                         ident = QStringLiteral("X-messaging/gadu-All");
1154                     } else if (identifier == QLatin1String("x-skype")) {
1155                         ident = QStringLiteral("X-messaging/skype-All");
1156                     } else if (identifier == QLatin1String("x-groupwise")) {
1157                         ident = QStringLiteral("X-messaging/groupwise-All");
1158                     } else if (identifier == QLatin1String("x-sms")) {
1159                         ident = QStringLiteral("X-messaging/sms-All");
1160                     } else if (identifier == QLatin1String("x-meanwhile")) {
1161                         ident = QStringLiteral("X-messaging/meanwhile-All");
1162                     } else if (identifier == QLatin1String("x-irc")) {
1163                         ident = QStringLiteral("X-messaging/irc-All");
1164                     } else if (identifier == QLatin1String("x-gtalk")) {
1165                         ident = QStringLiteral("X-messaging/googletalk-All");
1166                     } else if (identifier == QLatin1String("x-twitter")) {
1167                         ident = QStringLiteral("X-messaging/twitter-All");
1168                     }
1169 
1170                     const QString key = ident.mid(2);
1171                     const int dash = key.indexOf(QLatin1Char('-'));
1172 
1173                     // convert legacy messaging fields into IMPP ones
1174                     if (key.startsWith(QLatin1String("messaging/"))) {
1175                         QUrl url;
1176                         url.setScheme(normalizeImppServiceType(key.mid(10, dash - 10)));
1177                         const auto values = (*lineIt).value().toString().split(QChar(0xE000), Qt::SkipEmptyParts);
1178                         for (const auto &value : values) {
1179                             url.setPath(value);
1180                             Impp impp;
1181                             impp.setParams((*lineIt).parameterMap());
1182                             impp.setAddress(url);
1183                             addr.insertImpp(impp);
1184                         }
1185                     } else {
1186                         addr.insertCustom(key.left(dash), key.mid(dash + 1), (*lineIt).value().toString());
1187                     }
1188                 }
1189             }
1190         }
1191 
1192         addrList.append(addr);
1193     }
1194 
1195     return addrList;
1196 }
1197 
parseDateTime(const QString & str,bool * timeValid)1198 QDateTime VCardTool::parseDateTime(const QString &str, bool *timeValid)
1199 {
1200     static const QLatin1Char sep('-');
1201 
1202     const int posT = str.indexOf(QLatin1Char('T'));
1203     QString dateString = posT >= 0 ? str.left(posT) : str;
1204     const bool noYear = dateString.startsWith(QLatin1String("--"));
1205     dateString.remove(QLatin1Char('-'));
1206     QDate date;
1207 
1208 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1209     const QStringView dstr{dateString};
1210 #else
1211     const QStringRef dstr(&dateString);
1212 #endif
1213     if (noYear) {
1214         date.setDate(-1, dstr.mid(0, 2).toInt(), dstr.mid(2, 2).toInt());
1215     } else {
1216         // E.g. 20160120
1217         date.setDate(dstr.mid(0, 4).toInt(), dstr.mid(4, 2).toInt(), dstr.mid(6, 2).toInt());
1218     }
1219 
1220     QTime time;
1221     Qt::TimeSpec spec = Qt::LocalTime;
1222     int offsetSecs = 0;
1223     if (posT >= 0) {
1224         QString timeString = str.mid(posT + 1);
1225         timeString.remove(QLatin1Char(':'));
1226         const int zPos = timeString.indexOf(QLatin1Char('Z'));
1227         const int plusPos = timeString.indexOf(QLatin1Char('+'));
1228         const int minusPos = timeString.indexOf(sep);
1229         const int tzPos = qMax(qMax(zPos, plusPos), minusPos);
1230 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1231         const QStringView hhmmssString = tzPos >= 0 ? QStringView(timeString).left(tzPos) : QStringView(timeString);
1232 #else
1233         const QStringRef hhmmssString = tzPos >= 0 ? timeString.leftRef(tzPos) : QStringRef(&timeString);
1234 #endif
1235         int hour = 0;
1236         int minutes = 0;
1237         int seconds = 0;
1238         switch (hhmmssString.size()) {
1239         case 2:
1240             hour = hhmmssString.toInt();
1241             break;
1242         case 4:
1243             hour = hhmmssString.mid(0, 2).toInt();
1244             minutes = hhmmssString.mid(2, 2).toInt();
1245             break;
1246         case 6:
1247             hour = hhmmssString.mid(0, 2).toInt();
1248             minutes = hhmmssString.mid(2, 2).toInt();
1249             seconds = hhmmssString.mid(4, 2).toInt();
1250             break;
1251         }
1252         time.setHMS(hour, minutes, seconds);
1253 
1254         if (tzPos >= 0) {
1255             if (zPos >= 0) {
1256                 spec = Qt::UTC;
1257             } else {
1258                 spec = Qt::OffsetFromUTC;
1259 
1260 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1261                 const auto offsetString = QStringView(timeString).mid(tzPos + 1);
1262 #else
1263                 const QStringRef offsetString = timeString.midRef(tzPos + 1);
1264 #endif
1265                 switch (offsetString.size()) {
1266                 case 2: // format: "hh"
1267                     offsetSecs = offsetString.left(2).toInt() * 3600;
1268                     break;
1269                 case 4: // format: "hhmm"
1270                     offsetSecs = offsetString.left(2).toInt() * 3600 + offsetString.mid(2, 2).toInt() * 60;
1271                     break;
1272                 }
1273             }
1274             if (minusPos >= 0) {
1275                 offsetSecs *= -1;
1276             }
1277         }
1278     }
1279     if (timeValid) {
1280         *timeValid = time.isValid();
1281     }
1282 
1283     return QDateTime(date, time, spec, offsetSecs);
1284 }
1285 
createDateTime(const QDateTime & dateTime,VCard::Version version,bool withTime)1286 QString VCardTool::createDateTime(const QDateTime &dateTime, VCard::Version version, bool withTime)
1287 {
1288     if (!dateTime.date().isValid()) {
1289         return QString();
1290     }
1291     QString str = createDate(dateTime.date(), version);
1292     if (!withTime) {
1293         return str;
1294     }
1295     str += createTime(dateTime.time(), version);
1296     if (dateTime.timeSpec() == Qt::UTC) {
1297         str += QLatin1Char('Z');
1298     } else if (dateTime.timeSpec() == Qt::OffsetFromUTC) {
1299         const int offsetSecs = dateTime.offsetFromUtc();
1300         if (offsetSecs >= 0) {
1301             str += QLatin1Char('+');
1302         } else {
1303             str += QLatin1Char('-');
1304         }
1305         QTime offsetTime = QTime(0, 0).addSecs(abs(offsetSecs));
1306         if (version == VCard::v4_0) {
1307             str += offsetTime.toString(QStringLiteral("HHmm"));
1308         } else {
1309             str += offsetTime.toString(QStringLiteral("HH:mm"));
1310         }
1311     }
1312     return str;
1313 }
1314 
createDate(const QDate & date,VCard::Version version)1315 QString VCardTool::createDate(const QDate &date, VCard::Version version)
1316 {
1317     QString format;
1318     if (date.year() > 0) {
1319         format = QStringLiteral("yyyyMMdd");
1320     } else {
1321         format = QStringLiteral("--MMdd");
1322     }
1323     if (version != VCard::v4_0) {
1324         format.replace(QStringLiteral("yyyy"), QStringLiteral("yyyy-"));
1325         format.replace(QStringLiteral("MM"), QStringLiteral("MM-"));
1326     }
1327     return date.toString(format);
1328 }
1329 
createTime(const QTime & time,VCard::Version version)1330 QString VCardTool::createTime(const QTime &time, VCard::Version version)
1331 {
1332     QString format;
1333     if (version == VCard::v4_0) {
1334         format = QStringLiteral("HHmmss");
1335     } else {
1336         format = QStringLiteral("HH:mm:ss");
1337     }
1338     return QLatin1Char('T') + time.toString(format);
1339 }
1340 
parsePicture(const VCardLine & line) const1341 Picture VCardTool::parsePicture(const VCardLine &line) const
1342 {
1343     Picture pic;
1344 
1345     const QStringList params = line.parameterList();
1346     QString type;
1347     if (params.contains(QLatin1String("type"))) {
1348         type = line.parameter(QStringLiteral("type"));
1349     }
1350     if (params.contains(QLatin1String("encoding"))) {
1351         pic.setRawData(line.value().toByteArray(), type);
1352     } else if (params.contains(QLatin1String("value"))) {
1353         if (line.parameter(QStringLiteral("value")).toLower() == QLatin1String("uri")) {
1354             pic.setUrl(line.value().toString());
1355         }
1356     }
1357 
1358     return pic;
1359 }
1360 
createPicture(const QString & identifier,const Picture & pic,VCard::Version version) const1361 VCardLine VCardTool::createPicture(const QString &identifier, const Picture &pic, VCard::Version version) const
1362 {
1363     VCardLine line(identifier);
1364 
1365     if (pic.isEmpty()) {
1366         return line;
1367     }
1368 
1369     if (pic.isIntern()) {
1370         line.setValue(pic.rawData());
1371         if (version == VCard::v2_1) {
1372             line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1373             line.addParameter(pic.type(), QString());
1374         } else { /*if (version == VCard::v3_0) */
1375             line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1376             line.addParameter(QStringLiteral("type"), pic.type());
1377 #if 0
1378         } else { //version 4.0
1379             line.addParameter(QStringLiteral("data") + QStringLiteral(":image/") + pic.type(), QStringLiteral("base64"));
1380 #endif
1381         }
1382     } else {
1383         line.setValue(pic.url());
1384         line.addParameter(QStringLiteral("value"), QStringLiteral("URI"));
1385     }
1386 
1387     return line;
1388 }
1389 
parseSound(const VCardLine & line) const1390 Sound VCardTool::parseSound(const VCardLine &line) const
1391 {
1392     Sound snd;
1393 
1394     const QStringList params = line.parameterList();
1395     if (params.contains(QLatin1String("encoding"))) {
1396         snd.setData(line.value().toByteArray());
1397     } else if (params.contains(QLatin1String("value"))) {
1398         if (line.parameter(QStringLiteral("value")).toLower() == QLatin1String("uri")) {
1399             snd.setUrl(line.value().toString());
1400         }
1401     }
1402 
1403     /* TODO: support sound types
1404       if ( params.contains( "type" ) )
1405         snd.setType( line.parameter( "type" ) );
1406     */
1407 
1408     return snd;
1409 }
1410 
createSound(const Sound & snd,VCard::Version version) const1411 VCardLine VCardTool::createSound(const Sound &snd, VCard::Version version) const
1412 {
1413     Q_UNUSED(version);
1414     VCardLine line(QStringLiteral("SOUND"));
1415 
1416     if (snd.isIntern()) {
1417         if (!snd.data().isEmpty()) {
1418             line.setValue(snd.data());
1419             if (version == VCard::v2_1) {
1420                 line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1421             } else {
1422                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1423             }
1424             // TODO: need to store sound type!!!
1425         }
1426     } else if (!snd.url().isEmpty()) {
1427         line.setValue(snd.url());
1428         line.addParameter(QStringLiteral("value"), QStringLiteral("URI"));
1429     }
1430 
1431     return line;
1432 }
1433 
parseKey(const VCardLine & line) const1434 Key VCardTool::parseKey(const VCardLine &line) const
1435 {
1436     Key key;
1437 
1438     const QStringList params = line.parameterList();
1439     if (params.contains(QLatin1String("encoding"))) {
1440         key.setBinaryData(line.value().toByteArray());
1441     } else {
1442         key.setTextData(line.value().toString());
1443     }
1444 
1445     if (params.contains(QLatin1String("type"))) {
1446         if (line.parameter(QStringLiteral("type")).toLower() == QLatin1String("x509")) {
1447             key.setType(Key::X509);
1448         } else if (line.parameter(QStringLiteral("type")).toLower() == QLatin1String("pgp")) {
1449             key.setType(Key::PGP);
1450         } else {
1451             key.setType(Key::Custom);
1452             key.setCustomTypeString(line.parameter(QStringLiteral("type")));
1453         }
1454     } else if (params.contains(QLatin1String("mediatype"))) {
1455         const QString param = line.parameter(QStringLiteral("mediatype")).toLower();
1456         if (param == QLatin1String("application/x-x509-ca-cert")) {
1457             key.setType(Key::X509);
1458         } else if (param == QLatin1String("application/pgp-keys")) {
1459             key.setType(Key::PGP);
1460         } else {
1461             key.setType(Key::Custom);
1462             key.setCustomTypeString(line.parameter(QStringLiteral("type")));
1463         }
1464     }
1465 
1466     return key;
1467 }
1468 
createKey(const Key & key,VCard::Version version) const1469 VCardLine VCardTool::createKey(const Key &key, VCard::Version version) const
1470 {
1471     VCardLine line(QStringLiteral("KEY"));
1472 
1473     if (key.isBinary()) {
1474         if (!key.binaryData().isEmpty()) {
1475             line.setValue(key.binaryData());
1476             if (version == VCard::v2_1) {
1477                 line.addParameter(QStringLiteral("ENCODING"), QStringLiteral("BASE64"));
1478             } else {
1479                 line.addParameter(QStringLiteral("encoding"), QStringLiteral("b"));
1480             }
1481         }
1482     } else if (!key.textData().isEmpty()) {
1483         line.setValue(key.textData());
1484     }
1485 
1486     if (version == VCard::v4_0) {
1487         if (key.type() == Key::X509) {
1488             line.addParameter(QStringLiteral("MEDIATYPE"), QStringLiteral("application/x-x509-ca-cert"));
1489         } else if (key.type() == Key::PGP) {
1490             line.addParameter(QStringLiteral("MEDIATYPE"), QStringLiteral("application/pgp-keys"));
1491         } else if (key.type() == Key::Custom) {
1492             line.addParameter(QStringLiteral("MEDIATYPE"), key.customTypeString());
1493         }
1494     } else {
1495         if (key.type() == Key::X509) {
1496             line.addParameter(QStringLiteral("type"), QStringLiteral("X509"));
1497         } else if (key.type() == Key::PGP) {
1498             line.addParameter(QStringLiteral("type"), QStringLiteral("PGP"));
1499         } else if (key.type() == Key::Custom) {
1500             line.addParameter(QStringLiteral("type"), key.customTypeString());
1501         }
1502     }
1503 
1504     return line;
1505 }
1506 
parseSecrecy(const VCardLine & line) const1507 Secrecy VCardTool::parseSecrecy(const VCardLine &line) const
1508 {
1509     Secrecy secrecy;
1510 
1511     const QString value = line.value().toString().toLower();
1512     if (value == QLatin1String("public")) {
1513         secrecy.setType(Secrecy::Public);
1514     } else if (value == QLatin1String("private")) {
1515         secrecy.setType(Secrecy::Private);
1516     } else if (value == QLatin1String("confidential")) {
1517         secrecy.setType(Secrecy::Confidential);
1518     }
1519 
1520     return secrecy;
1521 }
1522 
createSecrecy(const Secrecy & secrecy) const1523 VCardLine VCardTool::createSecrecy(const Secrecy &secrecy) const
1524 {
1525     VCardLine line(QStringLiteral("CLASS"));
1526 
1527     int type = secrecy.type();
1528 
1529     if (type == Secrecy::Public) {
1530         line.setValue(QStringLiteral("PUBLIC"));
1531     } else if (type == Secrecy::Private) {
1532         line.setValue(QStringLiteral("PRIVATE"));
1533     } else if (type == Secrecy::Confidential) {
1534         line.setValue(QStringLiteral("CONFIDENTIAL"));
1535     }
1536 
1537     return line;
1538 }
1539 
splitString(QChar sep,const QString & str) const1540 QStringList VCardTool::splitString(QChar sep, const QString &str) const
1541 {
1542     QStringList list;
1543     QString value(str);
1544 
1545     int start = 0;
1546     int pos = value.indexOf(sep, start);
1547 
1548     while (pos != -1) {
1549         if (pos == 0 || value[pos - 1] != QLatin1Char('\\')) {
1550             if (pos > start && pos <= value.length()) {
1551                 list << value.mid(start, pos - start);
1552             } else {
1553                 list << QString();
1554             }
1555 
1556             start = pos + 1;
1557             pos = value.indexOf(sep, start);
1558         } else {
1559             value.replace(pos - 1, 2, sep);
1560             pos = value.indexOf(sep, pos);
1561         }
1562     }
1563 
1564     int l = value.length() - 1;
1565     const QString mid = value.mid(start, l - start + 1);
1566     if (!mid.isEmpty()) {
1567         list << mid;
1568     } else {
1569         list << QString();
1570     }
1571 
1572     return list;
1573 }
1574 
normalizeImppServiceType(const QString & serviceType) const1575 QString VCardTool::normalizeImppServiceType(const QString &serviceType) const
1576 {
1577     if (serviceType == QLatin1String("jabber")) {
1578         return QStringLiteral("xmpp");
1579     }
1580     if (serviceType == QLatin1String("yahoo")) {
1581         return QStringLiteral("ymsgr");
1582     }
1583     if (serviceType == QLatin1String("gadugadu")) {
1584         return QStringLiteral("gg");
1585     }
1586     return serviceType;
1587 }
1588