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

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