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

source code of kcontacts/src/vcardtool.cpp