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