1 /*
2 This file is part of the KContacts framework.
3 SPDX-FileCopyrightText: 2003 Helge Deller <deller@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 /*
9 Useful links:
10 - http://tldp.org/HOWTO/LDAP-Implementation-HOWTO/schemas.html
11 - http://www.faqs.org/rfcs/rfc2849.html
12
13 Not yet handled items:
14 - objectclass microsoftaddressbook
15 - info,
16 - initials,
17 - otherfacsimiletelephonenumber,
18 - otherpager,
19 - physicaldeliveryofficename,
20 */
21
22 #include "ldifconverter.h"
23 #include "address.h"
24 #include "kcontacts_debug.h"
25 #include "vcardconverter.h"
26
27 #include "ldif_p.h"
28
29 #include <KCountry>
30 #include <KLocalizedString>
31
32 #include <QStringList>
33 #include <QTextCodec>
34 #include <QTextStream>
35
36 using namespace KContacts;
37
38 /* internal functions - do not use !! */
39
40 namespace KContacts
41 {
42 /**
43 @internal
44
45 Evaluates @p fieldname and sets the @p value at the addressee or the address
46 objects when appropriate.
47
48 @param a The addressee to store information into
49 @param homeAddr The home address to store respective information into
50 @param workAddr The work address to store respective information into
51 @param fieldname LDIF field name to evaluate
52 @param value The value of the field addressed by @p fieldname
53 */
54 void evaluatePair(Addressee &a,
55 Address &homeAddr,
56 Address &workAddr,
57 QString &fieldname,
58 QString &value,
59 int &birthday,
60 int &birthmonth,
61 int &birthyear,
62 ContactGroup &contactGroup);
63 }
64
65 /* generate LDIF stream */
66
ldif_out(QTextStream & t,const QString & formatStr,const QString & value)67 static void ldif_out(QTextStream &t, const QString &formatStr, const QString &value)
68 {
69 if (value.isEmpty()) {
70 return;
71 }
72
73 const QByteArray txt = Ldif::assembleLine(formatStr, value, 72);
74
75 // write the string
76 t << QString::fromUtf8(txt) << "\n";
77 }
78
addresseeAndContactGroupToLDIF(const AddresseeList & addrList,const ContactGroup::List & contactGroupList,QString & str)79 bool LDIFConverter::addresseeAndContactGroupToLDIF(const AddresseeList &addrList, const ContactGroup::List &contactGroupList, QString &str)
80 {
81 bool result = addresseeToLDIF(addrList, str);
82 if (!contactGroupList.isEmpty()) {
83 result = (contactGroupToLDIF(contactGroupList, str) || result); // order matters
84 }
85 return result;
86 }
87
contactGroupToLDIF(const ContactGroup & contactGroup,QString & str)88 bool LDIFConverter::contactGroupToLDIF(const ContactGroup &contactGroup, QString &str)
89 {
90 if (contactGroup.dataCount() <= 0) {
91 return false;
92 }
93 QTextStream t(&str, QIODevice::WriteOnly | QIODevice::Append);
94 t.setCodec(QTextCodec::codecForName("UTF-8"));
95 t << "objectclass: top\n";
96 t << "objectclass: groupOfNames\n";
97
98 for (int i = 0; i < contactGroup.dataCount(); ++i) {
99 const ContactGroup::Data &data = contactGroup.data(i);
100 const QString value = QStringLiteral("cn=%1,mail=%2").arg(data.name(), data.email());
101 ldif_out(t, QStringLiteral("member"), value);
102 }
103
104 t << "\n";
105 return true;
106 }
107
contactGroupToLDIF(const ContactGroup::List & contactGroupList,QString & str)108 bool LDIFConverter::contactGroupToLDIF(const ContactGroup::List &contactGroupList, QString &str)
109 {
110 if (contactGroupList.isEmpty()) {
111 return false;
112 }
113
114 bool result = true;
115 for (const ContactGroup &group : contactGroupList) {
116 result = (contactGroupToLDIF(group, str) || result); // order matters
117 }
118 return result;
119 }
120
addresseeToLDIF(const AddresseeList & addrList,QString & str)121 bool LDIFConverter::addresseeToLDIF(const AddresseeList &addrList, QString &str)
122 {
123 if (addrList.isEmpty()) {
124 return false;
125 }
126
127 bool result = true;
128 for (const Addressee &addr : addrList) {
129 result = (addresseeToLDIF(addr, str) || result); // order matters
130 }
131 return result;
132 }
133
countryName(const QString & isoCodeOrName)134 static QString countryName(const QString &isoCodeOrName)
135 {
136 const auto c = KCountry::fromAlpha2(isoCodeOrName);
137 return c.isValid() ? c.name() : isoCodeOrName;
138 }
139
addresseeToLDIF(const Addressee & addr,QString & str)140 bool LDIFConverter::addresseeToLDIF(const Addressee &addr, QString &str)
141 {
142 if (addr.isEmpty()) {
143 return false;
144 }
145
146 QTextStream t(&str, QIODevice::WriteOnly | QIODevice::Append);
147 t.setCodec(QTextCodec::codecForName("UTF-8"));
148
149 const Address homeAddr = addr.address(Address::Home);
150 const Address workAddr = addr.address(Address::Work);
151
152 ldif_out(t, QStringLiteral("dn"), QStringLiteral("cn=%1,mail=%2").arg(addr.formattedName().simplified(), addr.preferredEmail()));
153 t << "objectclass: top\n";
154 t << "objectclass: person\n";
155 t << "objectclass: organizationalPerson\n";
156
157 ldif_out(t, QStringLiteral("givenname"), addr.givenName());
158 ldif_out(t, QStringLiteral("sn"), addr.familyName());
159 ldif_out(t, QStringLiteral("cn"), addr.formattedName().simplified());
160 ldif_out(t, QStringLiteral("uid"), addr.uid());
161 ldif_out(t, QStringLiteral("nickname"), addr.nickName());
162 ldif_out(t, QStringLiteral("xmozillanickname"), addr.nickName());
163 ldif_out(t, QStringLiteral("mozillanickname"), addr.nickName());
164
165 ldif_out(t, QStringLiteral("mail"), addr.preferredEmail());
166 const QStringList emails = addr.emails();
167 const int numEmails = emails.count();
168 for (int i = 1; i < numEmails; ++i) {
169 if (i == 0) {
170 // nothing
171 } else if (i == 1) {
172 ldif_out(t, QStringLiteral("mozillasecondemail"), emails[1]);
173 } else {
174 ldif_out(t, QStringLiteral("othermailbox"), emails[i]);
175 }
176 }
177 // ldif_out( t, "mozilla_AIMScreenName: %1\n", "screen_name" );
178
179 ldif_out(t, QStringLiteral("telephonenumber"), addr.phoneNumber(PhoneNumber::Work).number());
180 ldif_out(t, QStringLiteral("facsimiletelephonenumber"), addr.phoneNumber(PhoneNumber::Fax).number());
181 ldif_out(t, QStringLiteral("homephone"), addr.phoneNumber(PhoneNumber::Home).number());
182 ldif_out(t, QStringLiteral("mobile"),
183 addr.phoneNumber(PhoneNumber::Cell).number()); // Netscape 7
184 ldif_out(t, QStringLiteral("cellphone"),
185 addr.phoneNumber(PhoneNumber::Cell).number()); // Netscape 4.x
186 ldif_out(t, QStringLiteral("pager"), addr.phoneNumber(PhoneNumber::Pager).number());
187 ldif_out(t, QStringLiteral("pagerphone"), addr.phoneNumber(PhoneNumber::Pager).number());
188
189 ldif_out(t, QStringLiteral("streethomeaddress"), homeAddr.street());
190 ldif_out(t, QStringLiteral("postalcode"), workAddr.postalCode());
191 ldif_out(t, QStringLiteral("postofficebox"), workAddr.postOfficeBox());
192
193 QStringList streets = homeAddr.street().split(QLatin1Char('\n'));
194 const int numberOfStreets(streets.count());
195 if (numberOfStreets > 0) {
196 ldif_out(t, QStringLiteral("homepostaladdress"), streets.at(0)); // Netscape 7
197 }
198 if (numberOfStreets > 1) {
199 ldif_out(t, QStringLiteral("mozillahomepostaladdress2"), streets.at(1)); // Netscape 7
200 }
201 ldif_out(t, QStringLiteral("mozillahomelocalityname"), homeAddr.locality()); // Netscape 7
202 ldif_out(t, QStringLiteral("mozillahomestate"), homeAddr.region());
203 ldif_out(t, QStringLiteral("mozillahomepostalcode"), homeAddr.postalCode());
204 ldif_out(t, QStringLiteral("mozillahomecountryname"), countryName(homeAddr.country()));
205 ldif_out(t, QStringLiteral("locality"), workAddr.locality());
206 ldif_out(t, QStringLiteral("streetaddress"), workAddr.street()); // Netscape 4.x
207
208 streets = workAddr.street().split(QLatin1Char('\n'));
209 const int streetsCount = streets.count();
210 if (streetsCount > 0) {
211 ldif_out(t, QStringLiteral("street"), streets.at(0));
212 }
213 if (streetsCount > 1) {
214 ldif_out(t, QStringLiteral("mozillaworkstreet2"), streets.at(1));
215 }
216 ldif_out(t, QStringLiteral("countryname"), countryName(workAddr.country()));
217 ldif_out(t, QStringLiteral("l"), workAddr.locality());
218 ldif_out(t, QStringLiteral("c"), countryName(workAddr.country()));
219 ldif_out(t, QStringLiteral("st"), workAddr.region());
220
221 ldif_out(t, QStringLiteral("title"), addr.title());
222 ldif_out(t, QStringLiteral("vocation"), addr.prefix());
223 ldif_out(t, QStringLiteral("ou"), addr.role());
224 ldif_out(t, QStringLiteral("o"), addr.organization());
225 ldif_out(t, QStringLiteral("organization"), addr.organization());
226 ldif_out(t, QStringLiteral("organizationname"), addr.organization());
227
228 // Compatibility with older kabc versions.
229 if (!addr.department().isEmpty()) {
230 ldif_out(t, QStringLiteral("department"), addr.department());
231 } else {
232 ldif_out(t, QStringLiteral("department"), addr.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Department")));
233 }
234
235 ldif_out(t, QStringLiteral("workurl"), addr.url().url().toDisplayString());
236 ldif_out(t, QStringLiteral("homeurl"), addr.url().url().toDisplayString());
237 ldif_out(t, QStringLiteral("mozillahomeurl"), addr.url().url().toDisplayString());
238
239 ldif_out(t, QStringLiteral("description"), addr.note());
240 if (addr.revision().isValid()) {
241 ldif_out(t, QStringLiteral("modifytimestamp"), dateToVCardString(addr.revision()));
242 }
243
244 const QDate birthday = addr.birthday().date();
245 if (birthday.isValid()) {
246 const int year = birthday.year();
247 if (year > 0) {
248 ldif_out(t, QStringLiteral("birthyear"), QString::number(year));
249 }
250 ldif_out(t, QStringLiteral("birthmonth"), QString::number(birthday.month()));
251 ldif_out(t, QStringLiteral("birthday"), QString::number(birthday.day()));
252 }
253
254 t << "\n";
255
256 return true;
257 }
258
259 /* convert from LDIF stream */
LDIFToAddressee(const QString & str,AddresseeList & addrList,ContactGroup::List & contactGroupList,const QDateTime & dt)260 bool LDIFConverter::LDIFToAddressee(const QString &str, AddresseeList &addrList, ContactGroup::List &contactGroupList, const QDateTime &dt)
261 {
262 if (str.isEmpty()) {
263 return true;
264 }
265
266 bool endldif = false;
267 bool end = false;
268 Ldif ldif;
269 Ldif::ParseValue ret;
270 Addressee a;
271 Address homeAddr;
272 Address workAddr;
273 int birthday = -1;
274 int birthmonth = -1;
275 int birthyear = -1;
276 ContactGroup contactGroup;
277 ldif.setLdif(str.toLatin1());
278 QDateTime qdt = dt;
279 if (!qdt.isValid()) {
280 qdt = QDateTime::currentDateTime();
281 }
282 a.setRevision(qdt);
283 homeAddr = Address(Address::Home);
284 workAddr = Address(Address::Work);
285
286 do {
287 ret = ldif.nextItem();
288 switch (ret) {
289 case Ldif::Item: {
290 QString fieldname = ldif.attr().toLower();
291 QString value = QString::fromUtf8(ldif.value());
292 evaluatePair(a, homeAddr, workAddr, fieldname, value, birthday, birthmonth, birthyear, contactGroup);
293 break;
294 }
295 case Ldif::EndEntry:
296 if (contactGroup.count() == 0) {
297 // if the new address is not empty, append it
298 QDate birthDate(birthyear, birthmonth, birthday);
299 if (birthDate.isValid()) {
300 a.setBirthday(birthDate);
301 }
302
303 if (!a.formattedName().isEmpty() || !a.name().isEmpty() || !a.familyName().isEmpty()) {
304 if (!homeAddr.isEmpty()) {
305 a.insertAddress(homeAddr);
306 }
307 if (!workAddr.isEmpty()) {
308 a.insertAddress(workAddr);
309 }
310 addrList.append(a);
311 }
312 } else {
313 contactGroupList.append(contactGroup);
314 }
315 a = Addressee();
316 contactGroup = ContactGroup();
317 a.setRevision(qdt);
318 homeAddr = Address(Address::Home);
319 workAddr = Address(Address::Work);
320 break;
321 case Ldif::MoreData:
322 if (endldif) {
323 end = true;
324 } else {
325 ldif.endLdif();
326 endldif = true;
327 break;
328 }
329 default:
330 break;
331 }
332 } while (!end);
333
334 return true;
335 }
336
evaluatePair(Addressee & a,Address & homeAddr,Address & workAddr,QString & fieldname,QString & value,int & birthday,int & birthmonth,int & birthyear,ContactGroup & contactGroup)337 void KContacts::evaluatePair(Addressee &a,
338 Address &homeAddr,
339 Address &workAddr,
340 QString &fieldname,
341 QString &value,
342 int &birthday,
343 int &birthmonth,
344 int &birthyear,
345 ContactGroup &contactGroup)
346 {
347 if (fieldname == QLatin1String("dn")) { // ignore
348 return;
349 }
350
351 if (fieldname.startsWith(QLatin1Char('#'))) {
352 return;
353 }
354
355 if (fieldname.isEmpty() && !a.note().isEmpty()) {
356 // some LDIF export filters are broken and add additional
357 // comments on stand-alone lines. Just add them to the notes for now.
358 a.setNote(a.note() + QLatin1Char('\n') + value);
359 return;
360 }
361
362 if (fieldname == QLatin1String("givenname")) {
363 a.setGivenName(value);
364 return;
365 }
366
367 if (fieldname == QLatin1String("xmozillanickname") //
368 || fieldname == QLatin1String("nickname") //
369 || fieldname == QLatin1String("mozillanickname")) {
370 a.setNickName(value);
371 return;
372 }
373
374 if (fieldname == QLatin1String("sn")) {
375 a.setFamilyName(value);
376 return;
377 }
378
379 if (fieldname == QLatin1String("uid")) {
380 a.setUid(value);
381 return;
382 }
383 if (fieldname == QLatin1String("mail") //
384 || fieldname == QLatin1String("mozillasecondemail") /* mozilla */
385 || fieldname == QLatin1String("othermailbox") /*TheBat!*/) {
386 if (a.emails().indexOf(value) == -1) {
387 a.addEmail(value);
388 }
389 return;
390 }
391
392 if (fieldname == QLatin1String("title")) {
393 a.setTitle(value);
394 return;
395 }
396
397 if (fieldname == QLatin1String("vocation")) {
398 a.setPrefix(value);
399 return;
400 }
401
402 if (fieldname == QLatin1String("cn")) {
403 a.setFormattedName(value);
404 return;
405 }
406
407 if (fieldname == QLatin1Char('o') || fieldname == QLatin1String("organization") // Exchange
408 || fieldname == QLatin1String("organizationname")) { // Exchange
409 a.setOrganization(value);
410 return;
411 }
412
413 // clang-format off
414 if (fieldname == QLatin1String("description")
415 || fieldname == QLatin1String("mozillacustom1")
416 || fieldname == QLatin1String("mozillacustom2")
417 || fieldname == QLatin1String("mozillacustom3")
418 || fieldname == QLatin1String("mozillacustom4")
419 || fieldname == QLatin1String("custom1")
420 || fieldname == QLatin1String("custom2")
421 || fieldname == QLatin1String("custom3")
422 || fieldname == QLatin1String("custom4")) {
423 if (!a.note().isEmpty()) {
424 a.setNote(a.note() + QLatin1Char('\n'));
425 }
426 a.setNote(a.note() + value);
427 return;
428 }
429 // clang-format on
430
431 if (fieldname == QLatin1String("homeurl") //
432 || fieldname == QLatin1String("workurl") //
433 || fieldname == QLatin1String("mozillahomeurl")) {
434 if (a.url().url().isEmpty()) {
435 ResourceLocatorUrl url;
436 url.setUrl(QUrl(value));
437 a.setUrl(url);
438 return;
439 }
440 if (a.url().url().toDisplayString() == QUrl(value).toDisplayString()) {
441 return;
442 }
443 // TODO: current version of kabc only supports one URL.
444 // TODO: change this with KDE 4
445 }
446
447 if (fieldname == QLatin1String("homephone")) {
448 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Home));
449 return;
450 }
451
452 if (fieldname == QLatin1String("telephonenumber")) {
453 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Work));
454 return;
455 }
456 if (fieldname == QLatin1String("mobile") /* mozilla/Netscape 7 */
457 || fieldname == QLatin1String("cellphone")) {
458 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Cell));
459 return;
460 }
461
462 if (fieldname == QLatin1String("pager") // mozilla
463 || fieldname == QLatin1String("pagerphone")) { // mozilla
464 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Pager));
465 return;
466 }
467
468 if (fieldname == QLatin1String("facsimiletelephonenumber")) {
469 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Fax));
470 return;
471 }
472
473 if (fieldname == QLatin1String("xmozillaanyphone")) { // mozilla
474 a.insertPhoneNumber(PhoneNumber(value, PhoneNumber::Work));
475 return;
476 }
477
478 if (fieldname == QLatin1String("streethomeaddress") //
479 || fieldname == QLatin1String("mozillahomestreet")) { // thunderbird
480 homeAddr.setStreet(value);
481 return;
482 }
483
484 if (fieldname == QLatin1String("street") //
485 || fieldname == QLatin1String("postaladdress")) { // mozilla
486 workAddr.setStreet(value);
487 return;
488 }
489 if (fieldname == QLatin1String("mozillapostaladdress2") //
490 || fieldname == QLatin1String("mozillaworkstreet2")) { // mozilla
491 workAddr.setStreet(workAddr.street() + QLatin1Char('\n') + value);
492 return;
493 }
494
495 if (fieldname == QLatin1String("postalcode")) {
496 workAddr.setPostalCode(value);
497 return;
498 }
499
500 if (fieldname == QLatin1String("postofficebox")) {
501 workAddr.setPostOfficeBox(value);
502 return;
503 }
504
505 if (fieldname == QLatin1String("homepostaladdress")) { // Netscape 7
506 homeAddr.setStreet(value);
507 return;
508 }
509
510 if (fieldname == QLatin1String("mozillahomepostaladdress2")) { // mozilla
511 homeAddr.setStreet(homeAddr.street() + QLatin1Char('\n') + value);
512 return;
513 }
514
515 if (fieldname == QLatin1String("mozillahomelocalityname")) { // mozilla
516 homeAddr.setLocality(value);
517 return;
518 }
519
520 if (fieldname == QLatin1String("mozillahomestate")) { // mozilla
521 homeAddr.setRegion(value);
522 return;
523 }
524
525 if (fieldname == QLatin1String("mozillahomepostalcode")) { // mozilla
526 homeAddr.setPostalCode(value);
527 return;
528 }
529
530 if (fieldname == QLatin1String("mozillahomecountryname")) { // mozilla
531 if (value.length() <= 2) {
532 value = countryName(value);
533 }
534 homeAddr.setCountry(value);
535 return;
536 }
537
538 if (fieldname == QLatin1String("locality")) {
539 workAddr.setLocality(value);
540 return;
541 }
542
543 if (fieldname == QLatin1String("streetaddress")) { // Netscape 4.x
544 workAddr.setStreet(value);
545 return;
546 }
547
548 if (fieldname == QLatin1String("countryname") //
549 || fieldname == QLatin1Char('c')) { // mozilla
550 if (value.length() <= 2) {
551 value = countryName(value);
552 }
553 workAddr.setCountry(value);
554 return;
555 }
556
557 if (fieldname == QLatin1Char('l')) { // mozilla
558 workAddr.setLocality(value);
559 return;
560 }
561
562 if (fieldname == QLatin1String("st")) {
563 workAddr.setRegion(value);
564 return;
565 }
566
567 if (fieldname == QLatin1String("ou")) {
568 a.setRole(value);
569 return;
570 }
571
572 if (fieldname == QLatin1String("department")) {
573 a.setDepartment(value);
574 return;
575 }
576
577 if (fieldname == QLatin1String("member")) {
578 // this is a mozilla list member (cn=xxx, mail=yyy)
579 const QStringList list = value.split(QLatin1Char(','));
580 QString name;
581 QString email;
582
583 const QLatin1String cnTag("cn=");
584 const QLatin1String mailTag("mail=");
585 for (const auto &str : list) {
586 if (str.startsWith(cnTag)) {
587 name = QStringView(str).mid(cnTag.size()).trimmed().toString();
588 } else if (str.startsWith(mailTag)) {
589 email = QStringView(str).mid(mailTag.size()).trimmed().toString();
590 }
591 }
592
593 if (!name.isEmpty() && !email.isEmpty()) {
594 email = QLatin1String(" <") + email + QLatin1Char('>');
595 }
596 ContactGroup::Data data;
597 data.setEmail(email);
598 data.setName(name);
599 contactGroup.append(data);
600 return;
601 }
602
603 if (fieldname == QLatin1String("modifytimestamp")) {
604 if (value == QLatin1String("0Z")) { // ignore
605 return;
606 }
607 QDateTime dt = VCardStringToDate(value);
608 if (dt.isValid()) {
609 a.setRevision(dt);
610 return;
611 }
612 }
613
614 if (fieldname == QLatin1String("display-name")) {
615 contactGroup.setName(value);
616 return;
617 }
618
619 if (fieldname == QLatin1String("objectclass")) { // ignore
620 return;
621 }
622
623 if (fieldname == QLatin1String("birthyear")) {
624 bool ok;
625 birthyear = value.toInt(&ok);
626 if (!ok) {
627 birthyear = -1;
628 }
629 return;
630 }
631 if (fieldname == QLatin1String("birthmonth")) {
632 birthmonth = value.toInt();
633 return;
634 }
635 if (fieldname == QLatin1String("birthday")) {
636 birthday = value.toInt();
637 return;
638 }
639 if (fieldname == QLatin1String("xbatbirthday")) {
640 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
641 const QStringView str{value};
642 #else
643 const QStringRef str{&value};
644 #endif
645 QDate dt(str.mid(0, 4).toInt(), str.mid(4, 2).toInt(), str.mid(6, 2).toInt());
646 if (dt.isValid()) {
647 a.setBirthday(dt);
648 }
649 return;
650 }
651 qCWarning(KCONTACTS_LOG) << QStringLiteral("LDIFConverter: Unknown field for '%1': '%2=%3'\n").arg(a.formattedName(), fieldname, value);
652 }
653