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 }