KTnef

formatter.cpp
Go to the documentation of this file.
1 /*
2  SPDX-FileCopyrightText: 2001 Cornelius Schumacher <[email protected]>
3  SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <[email protected]>
4  SPDX-FileCopyrightText: 2005 Rafal Rzepecki <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
22 #include "formatter.h"
23 #include "ktnefparser.h"
24 #include "ktnefmessage.h"
25 #include "ktnefdefs.h"
26 
27 #include <kcontacts/phonenumber.h>
28 #include <kcontacts/vcardconverter.h>
29 
30 #include <KCalendarCore/Calendar>
31 #include <KCalendarCore/ICalFormat>
32 #include <KCalUtils/IncidenceFormatter>
33 
34 #include <KLocalizedString>
35 
36 #include <QBuffer>
37 #include <QTimeZone>
38 
39 #include <time.h>
40 
41 using namespace KCalendarCore;
42 using namespace KTnef;
43 
44 /*******************************************************************
45  * Helper functions for the msTNEF -> VPart converter
46  *******************************************************************/
47 
48 //-----------------------------------------------------------------------------
49 //@cond IGNORE
50 static QString stringProp(KTNEFMessage *tnefMsg, quint32 key,
51  const QString &fallback = QString())
52 {
53  return tnefMsg->findProp(key < 0x10000 ? key & 0xFFFF : key >> 16, fallback);
54 }
55 
56 static QString sNamedProp(KTNEFMessage *tnefMsg, const QString &name,
57  const QString &fallback = QString())
58 {
59  return tnefMsg->findNamedProp(name, fallback);
60 }
61 
62 static QDateTime pureISOToLocalQDateTime(const QString &dtStr)
63 {
64  const int year = dtStr.leftRef(4).toInt();
65  const int month = dtStr.midRef(4, 2).toInt();
66  const int day = dtStr.midRef(6, 2).toInt();
67  const int hour = dtStr.midRef(9, 2).toInt();
68  const int minute = dtStr.midRef(11, 2).toInt();
69  const int second = dtStr.midRef(13, 2).toInt();
70  QDate tmpDate;
71  tmpDate.setDate(year, month, day);
72  QTime tmpTime;
73  tmpTime.setHMS(hour, minute, second);
74 
75  if (tmpDate.isValid() && tmpTime.isValid()) {
76  QDateTime dT = QDateTime(tmpDate, tmpTime);
77 
78  // correct for GMT ( == Zulu time == UTC )
79  if (dtStr.at(dtStr.length() - 1) == QLatin1Char('Z')) {
80  //dT = dT.addSecs( 60 * KRFCDate::localUTCOffset() );
81  //localUTCOffset( dT ) );
82  dT = dT.toLocalTime();
83  }
84  return dT;
85  } else {
86  return QDateTime();
87  }
88 }
89 //@endcond
90 
92 {
93  KTNEFParser parser;
94  QByteArray b(tnef);
95  QBuffer buf(&b);
97  KContacts::Addressee addressee;
98  ICalFormat calFormat;
99  Event::Ptr event(new Event());
100 
101  if (parser.openDevice(&buf)) {
102  KTNEFMessage *tnefMsg = parser.message();
103  //QMap<int,KTNEFProperty*> props = parser.message()->properties();
104 
105  // Everything depends from property PR_MESSAGE_CLASS
106  // (this is added by KTNEFParser):
107  QString msgClass = tnefMsg->findProp(0x001A, QString(), true).toUpper();
108  if (!msgClass.isEmpty()) {
109  // Match the old class names that might be used by Outlook for
110  // compatibility with Microsoft Mail for Windows for Workgroups 3.1.
111  bool bCompatClassAppointment = false;
112  bool bCompatMethodRequest = false;
113  bool bCompatMethodCancled = false;
114  bool bCompatMethodAccepted = false;
115  bool bCompatMethodAcceptedCond = false;
116  bool bCompatMethodDeclined = false;
117  if (msgClass.startsWith(QLatin1String("IPM.MICROSOFT SCHEDULE."))) {
118  bCompatClassAppointment = true;
119  if (msgClass.endsWith(QLatin1String(".MTGREQ"))) {
120  bCompatMethodRequest = true;
121  } else if (msgClass.endsWith(QLatin1String(".MTGCNCL"))) {
122  bCompatMethodCancled = true;
123  } else if (msgClass.endsWith(QLatin1String(".MTGRESPP"))) {
124  bCompatMethodAccepted = true;
125  } else if (msgClass.endsWith(QLatin1String(".MTGRESPA"))) {
126  bCompatMethodAcceptedCond = true;
127  } else if (msgClass.endsWith(QLatin1String(".MTGRESPN"))) {
128  bCompatMethodDeclined = true;
129  }
130  }
131  bool bCompatClassNote = (msgClass == QLatin1String("IPM.MICROSOFT MAIL.NOTE"));
132 
133  if (bCompatClassAppointment || QLatin1String("IPM.APPOINTMENT") == msgClass) {
134  // Compose a vCal
135  bool bIsReply = false;
136  QString prodID = QStringLiteral("-//Microsoft Corporation//Outlook ");
137  prodID += tnefMsg->findNamedProp(QStringLiteral("0x8554"), QStringLiteral("9.0"));
138  prodID += QLatin1String("MIMEDIR/EN\n");
139  prodID += QLatin1String("VERSION:2.0\n");
140  calFormat.setApplication(QStringLiteral("Outlook"), prodID);
141 
142  //iTIPMethod method;
143  if (bCompatMethodRequest) {
144  //method = iTIPRequest;
145  } else if (bCompatMethodCancled) {
146  //method = iTIPCancel;
147  } else if (bCompatMethodAccepted || bCompatMethodAcceptedCond ||
148  bCompatMethodDeclined) {
149  //method = iTIPReply;
150  bIsReply = true;
151  } else {
152  // pending(khz): verify whether "0x0c17" is the right tag ???
153  //
154  // at the moment we think there are REQUESTS and UPDATES
155  //
156  // but WHAT ABOUT REPLIES ???
157  //
158  //
159 
160  if (tnefMsg->findProp(0x0c17) == QLatin1Char('1')) {
161  bIsReply = true;
162  }
163  //method = iTIPRequest;
164  }
165 
167  //ScheduleMessage schedMsg( event, method, ScheduleMessage::Unknown );
168 
169  QString sSenderSearchKeyEmail(tnefMsg->findProp(0x0C1D));
170  if (sSenderSearchKeyEmail.isEmpty()) {
171  sSenderSearchKeyEmail = tnefMsg->findProp(0x0C1f);
172  }
173 
174  if (!sSenderSearchKeyEmail.isEmpty()) {
175  const int colon = sSenderSearchKeyEmail.indexOf(QLatin1Char(':'));
176  // May be e.g. "SMTP:[email protected]"
177  if (colon == -1) {
178  sSenderSearchKeyEmail.remove(0, colon + 1);
179  }
180  }
181 
182  QString s(tnefMsg->findProp(0x8189));
183  const QStringList attendees = s.split(QLatin1Char(';'));
184  if (!attendees.isEmpty()) {
185  for (auto it = attendees.cbegin(), end = attendees.cend(); it != end; ++it) {
186  // Skip all entries that have no '@' since these are
187  // no mail addresses
188  if (!(*it).contains(QLatin1Char('@'))) {
189  s = (*it).trimmed();
190 
191  Attendee attendee(s, s, true);
192  if (bIsReply) {
193  if (bCompatMethodAccepted) {
194  attendee.setStatus(Attendee::Accepted);
195  }
196  if (bCompatMethodDeclined) {
197  attendee.setStatus(Attendee::Declined);
198  }
199  if (bCompatMethodAcceptedCond) {
200  attendee.setStatus(Attendee::Tentative);
201  }
202  } else {
205  }
206  event->addAttendee(attendee);
207  }
208  }
209  } else {
210  // Oops, no attendees?
211  // This must be old style, let us use the PR_SENDER_SEARCH_KEY.
212  s = sSenderSearchKeyEmail;
213  if (!s.isEmpty()) {
214  Attendee attendee(QString(), QString(), true);
215  if (bIsReply) {
216  if (bCompatMethodAccepted) {
217  attendee.setStatus(Attendee::Accepted);
218  }
219  if (bCompatMethodAcceptedCond) {
220  attendee.setStatus(Attendee::Declined);
221  }
222  if (bCompatMethodDeclined) {
223  attendee.setStatus(Attendee::Tentative);
224  }
225  } else {
228  }
229  event->addAttendee(attendee);
230  }
231  }
232  s = tnefMsg->findProp(0x3ff8); // look for organizer property
233  if (s.isEmpty() && !bIsReply) {
234  s = sSenderSearchKeyEmail;
235  }
236  // TODO: Use the common name?
237  if (!s.isEmpty()) {
238  event->setOrganizer(s);
239  }
240 
241  QDateTime dt = tnefMsg->property(0x819b).toDateTime();
242  if (!dt.isValid()) {
243  dt = tnefMsg->property(0x0060).toDateTime();
244  }
245  event->setDtStart(dt); // ## Format??
246 
247  dt = tnefMsg->property(0x819c).toDateTime();
248  if (!dt.isValid())
249  {
250  dt = tnefMsg->property(0x0061).toDateTime();
251  }
252  event->setDtEnd(dt);
253 
254  s = tnefMsg->findProp(0x810d);
255  event->setLocation(s);
256  // is it OK to set this to OPAQUE always ??
257  //vPart += "TRANSP:OPAQUE\n"; ###FIXME, portme!
258  //vPart += "SEQUENCE:0\n";
259 
260  // is "0x0023" OK - or should we look for "0x0003" ??
261  s = tnefMsg->findProp(0x0062);
262  event->setUid(s);
263 
264  // PENDING(khz): is this value in local timezone? Must it be
265  // adjusted? Most likely this is a bug in the server or in
266  // Outlook - we ignore it for now.
267  s = tnefMsg->findProp(0x8202).remove(QLatin1Char('-')).remove(QLatin1Char(':'));
268  // ### kcal always uses currentDateTime()
269  // event->setDtStamp( QDateTime::fromString( s ) );
270 
271  s = tnefMsg->findNamedProp(QStringLiteral("Keywords"));
272  event->setCategories(s);
273 
274  s = tnefMsg->findProp(0x1000);
275  if (s.isEmpty()) {
276  s = tnefMsg->findProp(0x3fd9);
277  }
278  event->setDescription(s);
279 
280  s = tnefMsg->findProp(0x0070);
281  if (s.isEmpty()) {
282  s = tnefMsg->findProp(0x0037);
283  }
284  event->setSummary(s);
285 
286  s = tnefMsg->findProp(0x0026);
287  event->setPriority(s.toInt());
288  // is reminder flag set ?
289  if (!tnefMsg->findProp(0x8503).isEmpty()) {
290  Alarm::Ptr alarm(new Alarm(event.data())); // TODO: fix when KCalendarCore::Alarm is fixed
291  QDateTime highNoonTime =
292  pureISOToLocalQDateTime(tnefMsg->findProp(0x8502).
293  remove(QLatin1Char('-')).remove(QLatin1Char(':')));
294  QDateTime wakeMeUpTime =
295  pureISOToLocalQDateTime(tnefMsg->findProp(0x8560, QString()).
296  remove(QLatin1Char('-')).remove(QLatin1Char(':')));
297  alarm->setTime(wakeMeUpTime);
298 
299  if (highNoonTime.isValid() && wakeMeUpTime.isValid()) {
300  alarm->setStartOffset(Duration(highNoonTime, wakeMeUpTime));
301  } else {
302  // default: wake them up 15 minutes before the appointment
303  alarm->setStartOffset(Duration(15 * 60));
304  }
305  alarm->setDisplayAlarm(i18n("Reminder"));
306 
307  // Sorry: the different action types are not known (yet)
308  // so we always set 'DISPLAY' (no sounds, no images...)
309  event->addAlarm(alarm);
310  }
311  //ensure we have a uid for this event
312  if (event->uid().isEmpty()) {
313  event->setUid(CalFormat::createUniqueId());
314  }
315  cal->addEvent(event);
316  //bOk = true;
317  // we finished composing a vCal
318  } else if (bCompatClassNote || QLatin1String("IPM.CONTACT") == msgClass) {
319  addressee.setUid(stringProp(tnefMsg, attMSGID));
320  addressee.setFormattedName(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME));
321  addressee.insertEmail(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL1EMAILADDRESS)), true);
322  addressee.insertEmail(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL2EMAILADDRESS)), false);
323  addressee.insertEmail(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL3EMAILADDRESS)), false);
324  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-IMAddress"),
325  sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_IMADDRESS)));
326  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"),
327  stringProp(tnefMsg, MAPI_TAG_PR_SPOUSE_NAME));
328  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-ManagersName"),
329  stringProp(tnefMsg, MAPI_TAG_PR_MANAGER_NAME));
330  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-AssistantsName"),
331  stringProp(tnefMsg, MAPI_TAG_PR_ASSISTANT));
332  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Department"),
333  stringProp(tnefMsg, MAPI_TAG_PR_DEPARTMENT_NAME));
334  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Office"),
335  stringProp(tnefMsg, MAPI_TAG_PR_OFFICE_LOCATION));
336  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"),
337  stringProp(tnefMsg, MAPI_TAG_PR_PROFESSION));
338 
339  QString s = tnefMsg->findProp(MAPI_TAG_PR_WEDDING_ANNIVERSARY).
340  remove(QLatin1Char('-')).remove(QLatin1Char(':'));
341  if (!s.isEmpty()) {
342  addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"), s);
343  }
344 
346  url.setUrl(QUrl(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_WEBPAGE))));
347 
348  addressee.setUrl(url);
349 
350  // collect parts of Name entry
351  addressee.setFamilyName(stringProp(tnefMsg, MAPI_TAG_PR_SURNAME));
352  addressee.setGivenName(stringProp(tnefMsg, MAPI_TAG_PR_GIVEN_NAME));
353  addressee.setAdditionalName(stringProp(tnefMsg, MAPI_TAG_PR_MIDDLE_NAME));
354  addressee.setPrefix(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME_PREFIX));
355  addressee.setSuffix(stringProp(tnefMsg, MAPI_TAG_PR_GENERATION));
356 
357  addressee.setNickName(stringProp(tnefMsg, MAPI_TAG_PR_NICKNAME));
358  addressee.setRole(stringProp(tnefMsg, MAPI_TAG_PR_TITLE));
359  addressee.setOrganization(stringProp(tnefMsg, MAPI_TAG_PR_COMPANY_NAME));
360  /*
361  the MAPI property ID of this (multiline) )field is unknown:
362  vPart += stringProp(tnefMsg, "\n","NOTE", ... , "" );
363  */
364 
365  KContacts::Address adr;
366  adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_PO_BOX));
367  adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STREET));
368  adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_CITY));
369  adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STATE_OR_PROVINCE));
370  adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_POSTAL_CODE));
371  adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_COUNTRY));
373  addressee.insertAddress(adr);
374 
375  adr.setPostOfficeBox(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOBOX)));
376  adr.setStreet(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTREET)));
377  adr.setLocality(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCITY)));
378  adr.setRegion(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTATE)));
379  adr.setPostalCode(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOSTALCODE)));
380  adr.setCountry(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCOUNTRY)));
382  addressee.insertAddress(adr);
383 
384  adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_PO_BOX));
385  adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STREET));
386  adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_CITY));
387  adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STATE_OR_PROVINCE));
388  adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_POSTAL_CODE));
389  adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_COUNTRY));
391  addressee.insertAddress(adr);
392 
393  // problem: the 'other' address was stored by KOrganizer in
394  // a line looking like the following one:
395  // vPart += "\nADR;TYPE=dom;TYPE=intl;TYPE=parcel;TYPE=postal;TYPE=work;"
396  // "TYPE=home:other_pobox;;other_str1\nother_str2;other_loc;other_region;"
397  // "other_pocode;other_country"
398 
399  QString nr;
400  nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_TELEPHONE_NUMBER);
401  addressee.insertPhoneNumber(
403  nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_TELEPHONE_NUMBER);
404  addressee.insertPhoneNumber(
406  nr = stringProp(tnefMsg, MAPI_TAG_PR_MOBILE_TELEPHONE_NUMBER);
407  addressee.insertPhoneNumber(
409  nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_FAX_NUMBER);
410  addressee.insertPhoneNumber(
412  nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_FAX_NUMBER);
413  addressee.insertPhoneNumber(
415 
416  s = tnefMsg->findProp(MAPI_TAG_PR_BIRTHDAY).
417  remove(QLatin1Char('-')).remove(QLatin1Char(':'));
418  if (!s.isEmpty()) {
419  addressee.setBirthday(QDateTime::fromString(s));
420  }
421 
422  //bOk = (!addressee.isEmpty());
423  } else if (QLatin1String("IPM.NOTE") == msgClass) {
424 
425  } // else if ... and so on ...
426  }
427  }
428 
429  // Compose return string
430  const QString iCal = calFormat.toString(cal, QString());
431  if (!iCal.isEmpty()) {
432  // This was an iCal
433  return iCal;
434  }
435 
436  // Not an iCal - try a vCard
437  KContacts::VCardConverter converter;
438  return QString::fromUtf8(converter.createVCard(addressee));
439 }
440 
442  const MemoryCalendar::Ptr &cal,
444 {
445  const QString vPart = msTNEFToVPart(tnef);
447  if (!iCal.isEmpty()) {
448  return iCal;
449  } else {
450  return vPart;
451  }
452 }
void setFormattedName(const QString &formattedName)
int toInt(bool *ok, int base) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QVariant property(int key) const
Returns the property associated with the specified key.
QString toUpper() const const
void setLocality(const QString &locality)
void insertPhoneNumber(const PhoneNumber &phoneNumber)
void setStatus(PartStat status)
QString findProp(int key, const QString &fallback=QString(), bool convertToUpper=false) const
Finds a property by key, returning a formatted value.
bool setHMS(int h, int m, int s, int ms)
void setSuffix(const QString &suffix)
KCALUTILS_EXPORT QString formatICalInvitation(const QString &invitation, const KCalendarCore::MemoryCalendar::Ptr &calendar, InvitationFormatterHelper *helper)
QDateTime toDateTime() const const
static QString createUniqueId()
void setUid(const QString &uid)
static void setApplication(const QString &application, const QString &productID)
bool isValid() const const
void setStreet(const QString &street)
void setPrefix(const QString &prefix)
void setGivenName(const QString &givenName)
This file is part of the API for handling TNEF data and defines the KTNEFMessage class.
QString & remove(int position, int n)
KTNEF_EXPORT QString msTNEFToVPart(const QByteArray &tnef)
Transforms a TNEF attachment to an iCal or vCard.
Definition: formatter.cpp:91
T * data() const const
void setAdditionalName(const QString &additionalName)
void insertEmail(const QString &email, bool preferred=false, const QMap< QString, QStringList > &param=QMap< QString, QStringList >())
void setUrl(const ResourceLocatorUrl &url)
void setType(Type type)
Represents a TNEF message.
Definition: ktnefmessage.h:38
void setOrganization(const QString &organization)
This file is part of the API for handling TNEF data and provides some basic definitions for general u...
This file is part of the API for handling TNEF data and provides static Formatter helpers...
QString fromUtf8(const char *str, int size)
QStringRef leftRef(int n) const const
QTimeZone utc()
void setFamilyName(const QString &familyName)
Provides an TNEF parser.
Definition: ktnefparser.h:37
This file is part of the API for handling TNEF data and defines the KTNEFParser class.
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool isValid() const const
void setRegion(const QString &region)
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
void setRole(Role role)
QString findNamedProp(const QString &name, const QString &fallback=QString(), bool convertToUpper=false) const
Finds a property by name, returning a formatted value.
void setNickName(const QString &nickName)
bool openDevice(QIODevice *device)
Opens the QIODevice device for parsing.
QDateTime fromString(const QString &string, Qt::DateFormat format)
QStringRef midRef(int position, int n) const const
void setPostalCode(const QString &code)
KTNEF_EXPORT QString formatTNEFInvitation(const QByteArray &tnef, const KCalendarCore::MemoryCalendar::Ptr &cal, KCalUtils::InvitationFormatterHelper *h)
Formats a TNEF attachment to an HTML mail.
Definition: formatter.cpp:441
bool isValid() const const
QString i18n(const char *text, const TYPE &arg...)
void insertCustom(const QString &app, const QString &name, const QString &value)
bool setDate(int year, int month, int day)
QString toString(const Calendar::Ptr &calendar, const QString &notebook=QString(), bool deleted=false) override
void setPostOfficeBox(const QString &postOfficeBox)
void setCountry(const QString &country)
const QChar at(int position) const const
void setBirthday(const QDateTime &birthday, bool withTime=true)
int length() const const
QDateTime toLocalTime() const const
KTNEFMessage * message() const
Returns the KTNEFMessage used in the parsing process.
void setRole(const QString &role)
void insertAddress(const Address &address)
QByteArray createVCard(const Addressee &addr, Version version=v3_0) const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Tue Jan 19 2021 23:21:20 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.