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

source code of kcontacts/src/converter/ldifconverter.cpp