File indexing completed on 2024-04-28 05:19:25
0001 /* 0002 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org> 0003 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 0004 SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 /** 0009 @file 0010 This file is part of the API for handling TNEF data and provides 0011 static Formatter helpers. 0012 0013 @brief 0014 Provides helpers too format @acronym TNEF attachments into different 0015 formats like eg. a HTML representation. 0016 0017 @author Cornelius Schumacher 0018 @author Reinhold Kainhofer 0019 @author Rafal Rzepecki 0020 */ 0021 0022 #include "formatter.h" 0023 #include "ktnefdefs.h" 0024 #include "ktnefmessage.h" 0025 #include "ktnefparser.h" 0026 0027 #include <KContacts/PhoneNumber> 0028 #include <KContacts/VCardConverter> 0029 0030 #include <KCalUtils/IncidenceFormatter> 0031 #include <KCalendarCore/Calendar> 0032 #include <KCalendarCore/ICalFormat> 0033 0034 #include <KLocalizedString> 0035 0036 #include <QBuffer> 0037 #include <QTimeZone> 0038 0039 #include <ctime> 0040 0041 using namespace KCalendarCore; 0042 using namespace KTnef; 0043 0044 /******************************************************************* 0045 * Helper functions for the msTNEF -> VPart converter 0046 *******************************************************************/ 0047 0048 //----------------------------------------------------------------------------- 0049 //@cond IGNORE 0050 static QString stringProp(KTNEFMessage *tnefMsg, quint32 key, const QString &fallback = QString()) 0051 { 0052 return tnefMsg->findProp(key < 0x10000 ? key & 0xFFFF : key >> 16, fallback); 0053 } 0054 0055 static QString sNamedProp(KTNEFMessage *tnefMsg, const QString &name, const QString &fallback = QString()) 0056 { 0057 return tnefMsg->findNamedProp(name, fallback); 0058 } 0059 0060 static QDateTime pureISOToLocalQDateTime(const QString &dtStr) 0061 { 0062 const QStringView dtView{dtStr}; 0063 const int year = dtView.left(4).toInt(); 0064 const int month = dtView.mid(4, 2).toInt(); 0065 const int day = dtView.mid(6, 2).toInt(); 0066 const int hour = dtView.mid(9, 2).toInt(); 0067 const int minute = dtView.mid(11, 2).toInt(); 0068 const int second = dtView.mid(13, 2).toInt(); 0069 QDate tmpDate; 0070 tmpDate.setDate(year, month, day); 0071 QTime tmpTime; 0072 tmpTime.setHMS(hour, minute, second); 0073 0074 if (tmpDate.isValid() && tmpTime.isValid()) { 0075 QDateTime dT = QDateTime(tmpDate, tmpTime); 0076 0077 // correct for GMT ( == Zulu time == UTC ) 0078 if (dtStr.at(dtStr.length() - 1) == QLatin1Char('Z')) { 0079 // dT = dT.addSecs( 60 * KRFCDate::localUTCOffset() ); 0080 // localUTCOffset( dT ) ); 0081 dT = dT.toLocalTime(); 0082 } 0083 return dT; 0084 } else { 0085 return {}; 0086 } 0087 } 0088 //@endcond 0089 0090 QString KTnef::msTNEFToVPart(const QByteArray &tnef) 0091 { 0092 KTNEFParser parser; 0093 QByteArray b(tnef); 0094 QBuffer buf(&b); 0095 MemoryCalendar::Ptr cal(new MemoryCalendar(QTimeZone::utc())); 0096 KContacts::Addressee addressee; 0097 ICalFormat calFormat; 0098 Event::Ptr event(new Event()); 0099 0100 if (parser.openDevice(&buf)) { 0101 KTNEFMessage *tnefMsg = parser.message(); 0102 // QMap<int,KTNEFProperty*> props = parser.message()->properties(); 0103 0104 // Everything depends from property PR_MESSAGE_CLASS 0105 // (this is added by KTNEFParser): 0106 QString msgClass = tnefMsg->findProp(0x001A, QString(), true).toUpper(); 0107 if (!msgClass.isEmpty()) { 0108 // Match the old class names that might be used by Outlook for 0109 // compatibility with Microsoft Mail for Windows for Workgroups 3.1. 0110 bool bCompatClassAppointment = false; 0111 bool bCompatMethodRequest = false; 0112 bool bCompatMethodCancled = false; 0113 bool bCompatMethodAccepted = false; 0114 bool bCompatMethodAcceptedCond = false; 0115 bool bCompatMethodDeclined = false; 0116 if (msgClass.startsWith(QLatin1StringView("IPM.MICROSOFT SCHEDULE."))) { 0117 bCompatClassAppointment = true; 0118 if (msgClass.endsWith(QLatin1StringView(".MTGREQ"))) { 0119 bCompatMethodRequest = true; 0120 } else if (msgClass.endsWith(QLatin1StringView(".MTGCNCL"))) { 0121 bCompatMethodCancled = true; 0122 } else if (msgClass.endsWith(QLatin1StringView(".MTGRESPP"))) { 0123 bCompatMethodAccepted = true; 0124 } else if (msgClass.endsWith(QLatin1StringView(".MTGRESPA"))) { 0125 bCompatMethodAcceptedCond = true; 0126 } else if (msgClass.endsWith(QLatin1StringView(".MTGRESPN"))) { 0127 bCompatMethodDeclined = true; 0128 } 0129 } 0130 bool bCompatClassNote = (msgClass == QLatin1StringView("IPM.MICROSOFT MAIL.NOTE")); 0131 0132 if (bCompatClassAppointment || QLatin1StringView("IPM.APPOINTMENT") == msgClass) { 0133 // Compose a vCal 0134 bool bIsReply = false; 0135 QString prodID = QStringLiteral("-//Microsoft Corporation//Outlook "); 0136 prodID += tnefMsg->findNamedProp(QStringLiteral("0x8554"), QStringLiteral("9.0")); 0137 prodID += QLatin1StringView("MIMEDIR/EN\n"); 0138 prodID += QLatin1StringView("VERSION:2.0\n"); 0139 calFormat.setApplication(QStringLiteral("Outlook"), prodID); 0140 0141 // iTIPMethod method; 0142 if (bCompatMethodRequest) { 0143 // method = iTIPRequest; 0144 } else if (bCompatMethodCancled) { 0145 // method = iTIPCancel; 0146 } else if (bCompatMethodAccepted || bCompatMethodAcceptedCond || bCompatMethodDeclined) { 0147 // method = iTIPReply; 0148 bIsReply = true; 0149 } else { 0150 // pending(khz): verify whether "0x0c17" is the right tag ??? 0151 // 0152 // at the moment we think there are REQUESTS and UPDATES 0153 // 0154 // but WHAT ABOUT REPLIES ??? 0155 // 0156 // 0157 0158 if (tnefMsg->findProp(0x0c17) == QLatin1Char('1')) { 0159 bIsReply = true; 0160 } 0161 // method = iTIPRequest; 0162 } 0163 0164 /// ### FIXME Need to get this attribute written 0165 // ScheduleMessage schedMsg( event, method, ScheduleMessage::Unknown ); 0166 0167 QString sSenderSearchKeyEmail(tnefMsg->findProp(0x0C1D)); 0168 if (sSenderSearchKeyEmail.isEmpty()) { 0169 sSenderSearchKeyEmail = tnefMsg->findProp(0x0C1f); 0170 } 0171 0172 if (!sSenderSearchKeyEmail.isEmpty()) { 0173 const int colon = sSenderSearchKeyEmail.indexOf(QLatin1Char(':')); 0174 // May be e.g. "SMTP:KHZ@KDE.ORG" 0175 if (colon == -1) { 0176 sSenderSearchKeyEmail.remove(0, colon + 1); 0177 } 0178 } 0179 0180 QString s(tnefMsg->findProp(0x8189)); 0181 const QStringList attendees = s.split(QLatin1Char(';')); 0182 if (!attendees.isEmpty()) { 0183 for (auto it = attendees.cbegin(), end = attendees.cend(); it != end; ++it) { 0184 // Skip all entries that have no '@' since these are 0185 // no mail addresses 0186 if (!(*it).contains(QLatin1Char('@'))) { 0187 s = (*it).trimmed(); 0188 0189 Attendee attendee(s, s, true); 0190 if (bIsReply) { 0191 if (bCompatMethodAccepted) { 0192 attendee.setStatus(Attendee::Accepted); 0193 } 0194 if (bCompatMethodDeclined) { 0195 attendee.setStatus(Attendee::Declined); 0196 } 0197 if (bCompatMethodAcceptedCond) { 0198 attendee.setStatus(Attendee::Tentative); 0199 } 0200 } else { 0201 attendee.setStatus(Attendee::NeedsAction); 0202 attendee.setRole(Attendee::ReqParticipant); 0203 } 0204 event->addAttendee(attendee); 0205 } 0206 } 0207 } else { 0208 // Oops, no attendees? 0209 // This must be old style, let us use the PR_SENDER_SEARCH_KEY. 0210 s = sSenderSearchKeyEmail; 0211 if (!s.isEmpty()) { 0212 Attendee attendee(QString(), QString(), true); 0213 if (bIsReply) { 0214 if (bCompatMethodAccepted) { 0215 attendee.setStatus(Attendee::Accepted); 0216 } 0217 if (bCompatMethodAcceptedCond) { 0218 attendee.setStatus(Attendee::Declined); 0219 } 0220 if (bCompatMethodDeclined) { 0221 attendee.setStatus(Attendee::Tentative); 0222 } 0223 } else { 0224 attendee.setStatus(Attendee::NeedsAction); 0225 attendee.setRole(Attendee::ReqParticipant); 0226 } 0227 event->addAttendee(attendee); 0228 } 0229 } 0230 s = tnefMsg->findProp(0x3ff8); // look for organizer property 0231 if (s.isEmpty() && !bIsReply) { 0232 s = sSenderSearchKeyEmail; 0233 } 0234 // TODO: Use the common name? 0235 if (!s.isEmpty()) { 0236 event->setOrganizer(s); 0237 } 0238 0239 QDateTime dt = tnefMsg->property(0x819b).toDateTime(); 0240 if (!dt.isValid()) { 0241 dt = tnefMsg->property(0x0060).toDateTime(); 0242 } 0243 event->setDtStart(dt); // ## Format?? 0244 0245 dt = tnefMsg->property(0x819c).toDateTime(); 0246 if (!dt.isValid()) { 0247 dt = tnefMsg->property(0x0061).toDateTime(); 0248 } 0249 event->setDtEnd(dt); 0250 0251 s = tnefMsg->findProp(0x810d); 0252 event->setLocation(s); 0253 // is it OK to set this to OPAQUE always ?? 0254 // vPart += "TRANSP:OPAQUE\n"; ###FIXME, portme! 0255 // vPart += "SEQUENCE:0\n"; 0256 0257 // is "0x0023" OK - or should we look for "0x0003" ?? 0258 s = tnefMsg->findProp(0x0062); 0259 event->setUid(s); 0260 0261 // PENDING(khz): is this value in local timezone? Must it be 0262 // adjusted? Most likely this is a bug in the server or in 0263 // Outlook - we ignore it for now. 0264 s = tnefMsg->findProp(0x8202).remove(QLatin1Char('-')).remove(QLatin1Char(':')); 0265 // ### kcal always uses currentDateTime() 0266 // event->setDtStamp( QDateTime::fromString( s ) ); 0267 0268 s = tnefMsg->findNamedProp(QStringLiteral("Keywords")); 0269 event->setCategories(s); 0270 0271 s = tnefMsg->findProp(0x1000); 0272 if (s.isEmpty()) { 0273 s = tnefMsg->findProp(0x3fd9); 0274 } 0275 event->setDescription(s); 0276 0277 s = tnefMsg->findProp(0x0070); 0278 if (s.isEmpty()) { 0279 s = tnefMsg->findProp(0x0037); 0280 } 0281 event->setSummary(s); 0282 0283 s = tnefMsg->findProp(0x0026); 0284 event->setPriority(s.toInt()); 0285 // is reminder flag set ? 0286 if (!tnefMsg->findProp(0x8503).isEmpty()) { 0287 Alarm::Ptr alarm(new Alarm(event.data())); // TODO: fix when KCalendarCore::Alarm is fixed 0288 QDateTime highNoonTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8502).remove(QLatin1Char('-')).remove(QLatin1Char(':'))); 0289 QDateTime wakeMeUpTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8560, QString()).remove(QLatin1Char('-')).remove(QLatin1Char(':'))); 0290 alarm->setTime(wakeMeUpTime); 0291 0292 if (highNoonTime.isValid() && wakeMeUpTime.isValid()) { 0293 alarm->setStartOffset(Duration(highNoonTime, wakeMeUpTime)); 0294 } else { 0295 // default: wake them up 15 minutes before the appointment 0296 alarm->setStartOffset(Duration(15 * 60)); 0297 } 0298 alarm->setDisplayAlarm(i18n("Reminder")); 0299 0300 // Sorry: the different action types are not known (yet) 0301 // so we always set 'DISPLAY' (no sounds, no images...) 0302 event->addAlarm(alarm); 0303 } 0304 // ensure we have a uid for this event 0305 if (event->uid().isEmpty()) { 0306 event->setUid(CalFormat::createUniqueId()); 0307 } 0308 cal->addEvent(event); 0309 // bOk = true; 0310 // we finished composing a vCal 0311 } else if (bCompatClassNote || QLatin1StringView("IPM.CONTACT") == msgClass) { 0312 addressee.setUid(stringProp(tnefMsg, attMSGID)); 0313 addressee.setFormattedName(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME)); 0314 KContacts::Email email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL1EMAILADDRESS))); 0315 email.setPreferred(true); 0316 addressee.addEmail(email); 0317 addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL2EMAILADDRESS)))); 0318 addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL3EMAILADDRESS)))); 0319 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), 0320 QStringLiteral("X-IMAddress"), 0321 sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_IMADDRESS))); 0322 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"), stringProp(tnefMsg, MAPI_TAG_PR_SPOUSE_NAME)); 0323 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-ManagersName"), stringProp(tnefMsg, MAPI_TAG_PR_MANAGER_NAME)); 0324 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-AssistantsName"), stringProp(tnefMsg, MAPI_TAG_PR_ASSISTANT)); 0325 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Department"), stringProp(tnefMsg, MAPI_TAG_PR_DEPARTMENT_NAME)); 0326 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Office"), stringProp(tnefMsg, MAPI_TAG_PR_OFFICE_LOCATION)); 0327 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"), stringProp(tnefMsg, MAPI_TAG_PR_PROFESSION)); 0328 0329 QString s = tnefMsg->findProp(MAPI_TAG_PR_WEDDING_ANNIVERSARY).remove(QLatin1Char('-')).remove(QLatin1Char(':')); 0330 if (!s.isEmpty()) { 0331 addressee.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"), s); 0332 } 0333 0334 KContacts::ResourceLocatorUrl url; 0335 url.setUrl(QUrl(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_WEBPAGE)))); 0336 0337 addressee.setUrl(url); 0338 0339 // collect parts of Name entry 0340 addressee.setFamilyName(stringProp(tnefMsg, MAPI_TAG_PR_SURNAME)); 0341 addressee.setGivenName(stringProp(tnefMsg, MAPI_TAG_PR_GIVEN_NAME)); 0342 addressee.setAdditionalName(stringProp(tnefMsg, MAPI_TAG_PR_MIDDLE_NAME)); 0343 addressee.setPrefix(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME_PREFIX)); 0344 addressee.setSuffix(stringProp(tnefMsg, MAPI_TAG_PR_GENERATION)); 0345 0346 addressee.setNickName(stringProp(tnefMsg, MAPI_TAG_PR_NICKNAME)); 0347 addressee.setRole(stringProp(tnefMsg, MAPI_TAG_PR_TITLE)); 0348 addressee.setOrganization(stringProp(tnefMsg, MAPI_TAG_PR_COMPANY_NAME)); 0349 /* 0350 the MAPI property ID of this (multiline) )field is unknown: 0351 vPart += stringProp(tnefMsg, "\n","NOTE", ... , "" ); 0352 */ 0353 0354 KContacts::Address adr; 0355 adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_PO_BOX)); 0356 adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STREET)); 0357 adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_CITY)); 0358 adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STATE_OR_PROVINCE)); 0359 adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_POSTAL_CODE)); 0360 adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_COUNTRY)); 0361 adr.setType(KContacts::Address::Home); 0362 addressee.insertAddress(adr); 0363 0364 adr.setPostOfficeBox(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOBOX))); 0365 adr.setStreet(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTREET))); 0366 adr.setLocality(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCITY))); 0367 adr.setRegion(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTATE))); 0368 adr.setPostalCode(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOSTALCODE))); 0369 adr.setCountry(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCOUNTRY))); 0370 adr.setType(KContacts::Address::Work); 0371 addressee.insertAddress(adr); 0372 0373 adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_PO_BOX)); 0374 adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STREET)); 0375 adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_CITY)); 0376 adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STATE_OR_PROVINCE)); 0377 adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_POSTAL_CODE)); 0378 adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_COUNTRY)); 0379 adr.setType(KContacts::Address::Dom); 0380 addressee.insertAddress(adr); 0381 0382 // problem: the 'other' address was stored by KOrganizer in 0383 // a line looking like the following one: 0384 // vPart += "\nADR;TYPE=dom;TYPE=intl;TYPE=parcel;TYPE=postal;TYPE=work;" 0385 // "TYPE=home:other_pobox;;other_str1\nother_str2;other_loc;other_region;" 0386 // "other_pocode;other_country" 0387 0388 QString nr; 0389 nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_TELEPHONE_NUMBER); 0390 addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Home)); 0391 nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_TELEPHONE_NUMBER); 0392 addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Work)); 0393 nr = stringProp(tnefMsg, MAPI_TAG_PR_MOBILE_TELEPHONE_NUMBER); 0394 addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Cell)); 0395 nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_FAX_NUMBER); 0396 addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Home)); 0397 nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_FAX_NUMBER); 0398 addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Work)); 0399 0400 s = tnefMsg->findProp(MAPI_TAG_PR_BIRTHDAY).remove(QLatin1Char('-')).remove(QLatin1Char(':')); 0401 if (!s.isEmpty()) { 0402 addressee.setBirthday(QDateTime::fromString(s)); 0403 } 0404 0405 // bOk = (!addressee.isEmpty()); 0406 } else if (QLatin1StringView("IPM.NOTE") == msgClass) { 0407 } // else if ... and so on ... 0408 } 0409 } 0410 0411 // Compose return string 0412 const QString iCal = calFormat.toString(cal); 0413 if (!iCal.isEmpty()) { 0414 // This was an iCal 0415 return iCal; 0416 } 0417 0418 // Not an iCal - try a vCard 0419 KContacts::VCardConverter converter; 0420 return QString::fromUtf8(converter.createVCard(addressee)); 0421 } 0422 0423 QString KTnef::formatTNEFInvitation(const QByteArray &tnef, const MemoryCalendar::Ptr &cal, KCalUtils::InvitationFormatterHelper *h) 0424 { 0425 const QString vPart = msTNEFToVPart(tnef); 0426 QString iCal = KCalUtils::IncidenceFormatter::formatICalInvitation(vPart, cal, h); 0427 if (!iCal.isEmpty()) { 0428 return iCal; 0429 } else { 0430 return vPart; 0431 } 0432 }