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