[pim/ktnef] /: Separate format conversion from visual presentation

Volker Krause <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 7455f578714013c95395b3c2e78f3c97d6dc8ea1 by Volker Krause.
Committed on 02/08/2026 at 09:48.
Pushed by vkrause into branch 'master'.

Separate format conversion from visual presentation

This splits the API for converting TNEF messages into iCal or vCard from
API converting that to a specific HTML presentation for display. The former
belongs here, the latter belongs into KMail's plugin and will move there.

M  +1    -1    CMakeLists.txt
M  +311  -293  src/formatter.cpp
M  +23   -0    src/formatter.h

https://invent.kde.org/pim/ktnef/-/commit/7455f578714013c95395b3c2e78f3c97d6dc8ea1

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9b838c8..d1081c4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.29)
-set(PIM_VERSION "6.8.40")
+set(PIM_VERSION "6.8.41")
 
 project(KTnef VERSION ${PIM_VERSION})
 
diff --git a/src/formatter.cpp b/src/formatter.cpp
index d369a87..df957b7 100644
--- a/src/formatter.cpp
+++ b/src/formatter.cpp
@@ -49,12 +49,12 @@ using namespace KTnef;
 
 //-----------------------------------------------------------------------------
 //@cond IGNORE
-static QString stringProp(KTNEFMessage *tnefMsg, quint32 key, const QString &fallback = QString())
+static QString stringProp(const KTNEFMessage *tnefMsg, quint32 key, const QString &fallback = QString())
 {
     return tnefMsg->findProp(key < 0x10000 ? key & 0xFFFF : key >> 16, fallback);
 }
 
-static QString sNamedProp(KTNEFMessage *tnefMsg, const QString &name, const QString &fallback = QString())
+static QString sNamedProp(const KTNEFMessage *tnefMsg, const QString &name, const QString &fallback = QString())
 {
     return tnefMsg->findNamedProp(name, fallback);
 }
@@ -89,137 +89,106 @@ static QDateTime pureISOToLocalQDateTime(const QString &dtStr)
 }
 //@endcond
 
-QString KTnef::msTNEFToVPart(const QByteArray &tnef)
+QString KTnef::messageToIcal(const KTNEFMessage *tnefMsg)
 {
-    KTNEFParser parser;
-    QByteArray b(tnef);
-    QBuffer buf(&b);
-    MemoryCalendar::Ptr cal(new MemoryCalendar(QTimeZone::utc()));
-    KContacts::Addressee addressee;
-    ICalFormat calFormat;
-    Event::Ptr event(new Event());
-
-    if (parser.openDevice(&buf)) {
-        KTNEFMessage *tnefMsg = parser.message();
-        // QMap<int,KTNEFProperty*> props = parser.message()->properties();
-
-        // Everything depends from property PR_MESSAGE_CLASS
-        // (this is added by KTNEFParser):
-        QString msgClass = tnefMsg->findProp(0x001A, QString(), true).toUpper();
-        if (!msgClass.isEmpty()) {
-            // Match the old class names that might be used by Outlook for
-            // compatibility with Microsoft Mail for Windows for Workgroups 3.1.
-            bool bCompatClassAppointment = false;
-            bool bCompatMethodRequest = false;
-            bool bCompatMethodCancled = false;
-            bool bCompatMethodAccepted = false;
-            bool bCompatMethodAcceptedCond = false;
-            bool bCompatMethodDeclined = false;
-            if (msgClass.startsWith("IPM.MICROSOFT SCHEDULE."_L1)) {
-                bCompatClassAppointment = true;
-                if (msgClass.endsWith(".MTGREQ"_L1)) {
-                    bCompatMethodRequest = true;
-                } else if (msgClass.endsWith(".MTGCNCL"_L1)) {
-                    bCompatMethodCancled = true;
-                } else if (msgClass.endsWith(".MTGRESPP"_L1)) {
-                    bCompatMethodAccepted = true;
-                } else if (msgClass.endsWith(".MTGRESPA"_L1)) {
-                    bCompatMethodAcceptedCond = true;
-                } else if (msgClass.endsWith(".MTGRESPN"_L1)) {
-                    bCompatMethodDeclined = true;
-                }
+    // QMap<int,KTNEFProperty*> props = parser.message()->properties();
+
+    // Everything depends from property PR_MESSAGE_CLASS
+    // (this is added by KTNEFParser):
+    QString msgClass = tnefMsg->findProp(0x001A, QString(), true).toUpper();
+    if (!msgClass.isEmpty()) {
+        // Match the old class names that might be used by Outlook for
+        // compatibility with Microsoft Mail for Windows for Workgroups 3.1.
+        bool bCompatClassAppointment = false;
+        bool bCompatMethodRequest = false;
+        bool bCompatMethodCancled = false;
+        bool bCompatMethodAccepted = false;
+        bool bCompatMethodAcceptedCond = false;
+        bool bCompatMethodDeclined = false;
+        if (msgClass.startsWith("IPM.MICROSOFT SCHEDULE."_L1)) {
+            bCompatClassAppointment = true;
+            if (msgClass.endsWith(".MTGREQ"_L1)) {
+                bCompatMethodRequest = true;
+            } else if (msgClass.endsWith(".MTGCNCL"_L1)) {
+                bCompatMethodCancled = true;
+            } else if (msgClass.endsWith(".MTGRESPP"_L1)) {
+                bCompatMethodAccepted = true;
+            } else if (msgClass.endsWith(".MTGRESPA"_L1)) {
+                bCompatMethodAcceptedCond = true;
+            } else if (msgClass.endsWith(".MTGRESPN"_L1)) {
+                bCompatMethodDeclined = true;
             }
-            bool bCompatClassNote = (msgClass == "IPM.MICROSOFT MAIL.NOTE"_L1);
-
-            if (bCompatClassAppointment || "IPM.APPOINTMENT"_L1 == msgClass) {
-                // Compose a vCal
-                bool bIsReply = false;
-                QString prodID = u"-//Microsoft Corporation//Outlook "_s;
-                prodID += tnefMsg->findNamedProp(u"0x8554"_s, u"9.0"_s);
-                prodID += "MIMEDIR/EN\n"_L1;
-                prodID += "VERSION:2.0\n"_L1;
-                calFormat.setApplication(u"Outlook"_s, prodID);
-
-                // iTIPMethod method;
-                if (bCompatMethodRequest) {
-                    // method = iTIPRequest;
-                } else if (bCompatMethodCancled) {
-                    // method = iTIPCancel;
-                } else if (bCompatMethodAccepted || bCompatMethodAcceptedCond || bCompatMethodDeclined) {
-                    // method = iTIPReply;
+        }
+
+        if (bCompatClassAppointment || "IPM.APPOINTMENT"_L1 == msgClass) {
+            // Compose a vCal
+            bool bIsReply = false;
+            QString prodID = u"-//Microsoft Corporation//Outlook "_s;
+            prodID += tnefMsg->findNamedProp(u"0x8554"_s, u"9.0"_s);
+            prodID += "MIMEDIR/EN\n"_L1;
+            prodID += "VERSION:2.0\n"_L1;
+            ICalFormat calFormat;
+            calFormat.setApplication(u"Outlook"_s, prodID);
+
+            // iTIPMethod method;
+            if (bCompatMethodRequest) {
+                // method = iTIPRequest;
+            } else if (bCompatMethodCancled) {
+                // method = iTIPCancel;
+            } else if (bCompatMethodAccepted || bCompatMethodAcceptedCond || bCompatMethodDeclined) {
+                // method = iTIPReply;
+                bIsReply = true;
+            } else {
+                // pending(khz): verify whether "0x0c17" is the right tag ???
+                //
+                // at the moment we think there are REQUESTS and UPDATES
+                //
+                // but WHAT ABOUT REPLIES ???
+                //
+                //
+
+                if (tnefMsg->findProp(0x0c17) == u'1') {
                     bIsReply = true;
-                } else {
-                    // pending(khz): verify whether "0x0c17" is the right tag ???
-                    //
-                    // at the moment we think there are REQUESTS and UPDATES
-                    //
-                    // but WHAT ABOUT REPLIES ???
-                    //
-                    //
-
-                    if (tnefMsg->findProp(0x0c17) == u'1') {
-                        bIsReply = true;
-                    }
-                    // method = iTIPRequest;
                 }
+                // method = iTIPRequest;
+            }
 
-                /// ###  FIXME Need to get this attribute written
-                // ScheduleMessage schedMsg( event, method, ScheduleMessage::Unknown );
+            /// ###  FIXME Need to get this attribute written
+            // ScheduleMessage schedMsg( event, method, ScheduleMessage::Unknown );
 
-                QString sSenderSearchKeyEmail(tnefMsg->findProp(0x0C1D));
-                if (sSenderSearchKeyEmail.isEmpty()) {
-                    sSenderSearchKeyEmail = tnefMsg->findProp(0x0C1f);
-                }
+            QString sSenderSearchKeyEmail(tnefMsg->findProp(0x0C1D));
+            if (sSenderSearchKeyEmail.isEmpty()) {
+                sSenderSearchKeyEmail = tnefMsg->findProp(0x0C1f);
+            }
 
-                if (!sSenderSearchKeyEmail.isEmpty()) {
-                    const int colon = sSenderSearchKeyEmail.indexOf(u':');
-                    // May be e.g. "SMTP:[email protected]"
-                    if (colon == -1) {
-                        sSenderSearchKeyEmail.remove(0, colon + 1);
-                    }
+            if (!sSenderSearchKeyEmail.isEmpty()) {
+                const int colon = sSenderSearchKeyEmail.indexOf(u':');
+                // May be e.g. "SMTP:[email protected]"
+                if (colon == -1) {
+                    sSenderSearchKeyEmail.remove(0, colon + 1);
                 }
+            }
 
-                QString s(tnefMsg->findProp(0x8189));
-                const QStringList attendees = s.split(u';');
-                if (!attendees.isEmpty()) {
-                    for (auto it = attendees.cbegin(), end = attendees.cend(); it != end; ++it) {
-                        // Skip all entries that have no '@' since these are
-                        // no mail addresses
-                        if (!(*it).contains(u'@')) {
-                            s = (*it).trimmed();
-
-                            Attendee attendee(s, s, true);
-                            if (bIsReply) {
-                                if (bCompatMethodAccepted) {
-                                    attendee.setStatus(Attendee::Accepted);
-                                }
-                                if (bCompatMethodDeclined) {
-                                    attendee.setStatus(Attendee::Declined);
-                                }
-                                if (bCompatMethodAcceptedCond) {
-                                    attendee.setStatus(Attendee::Tentative);
-                                }
-                            } else {
-                                attendee.setStatus(Attendee::NeedsAction);
-                                attendee.setRole(Attendee::ReqParticipant);
-                            }
-                            event->addAttendee(attendee);
-                        }
-                    }
-                } else {
-                    // Oops, no attendees?
-                    // This must be old style, let us use the PR_SENDER_SEARCH_KEY.
-                    s = sSenderSearchKeyEmail;
-                    if (!s.isEmpty()) {
-                        Attendee attendee(QString(), QString(), true);
+            Event::Ptr event(new Event());
+
+            QString s(tnefMsg->findProp(0x8189));
+            const QStringList attendees = s.split(u';');
+            if (!attendees.isEmpty()) {
+                for (auto it = attendees.cbegin(), end = attendees.cend(); it != end; ++it) {
+                    // Skip all entries that have no '@' since these are
+                    // no mail addresses
+                    if (!(*it).contains(u'@')) {
+                        s = (*it).trimmed();
+
+                        Attendee attendee(s, s, true);
                         if (bIsReply) {
                             if (bCompatMethodAccepted) {
                                 attendee.setStatus(Attendee::Accepted);
                             }
-                            if (bCompatMethodAcceptedCond) {
+                            if (bCompatMethodDeclined) {
                                 attendee.setStatus(Attendee::Declined);
                             }
-                            if (bCompatMethodDeclined) {
+                            if (bCompatMethodAcceptedCond) {
                                 attendee.setStatus(Attendee::Tentative);
                             }
                         } else {
@@ -229,195 +198,244 @@ QString KTnef::msTNEFToVPart(const QByteArray &tnef)
                         event->addAttendee(attendee);
                     }
                 }
-                s = tnefMsg->findProp(0x3ff8); // look for organizer property
-                if (s.isEmpty() && !bIsReply) {
-                    s = sSenderSearchKeyEmail;
-                }
-                // TODO: Use the common name?
+            } else {
+                // Oops, no attendees?
+                // This must be old style, let us use the PR_SENDER_SEARCH_KEY.
+                s = sSenderSearchKeyEmail;
                 if (!s.isEmpty()) {
-                    event->setOrganizer(s);
+                    Attendee attendee(QString(), QString(), true);
+                    if (bIsReply) {
+                        if (bCompatMethodAccepted) {
+                            attendee.setStatus(Attendee::Accepted);
+                        }
+                        if (bCompatMethodAcceptedCond) {
+                            attendee.setStatus(Attendee::Declined);
+                        }
+                        if (bCompatMethodDeclined) {
+                            attendee.setStatus(Attendee::Tentative);
+                        }
+                    } else {
+                        attendee.setStatus(Attendee::NeedsAction);
+                        attendee.setRole(Attendee::ReqParticipant);
+                    }
+                    event->addAttendee(attendee);
                 }
+            }
+            s = tnefMsg->findProp(0x3ff8); // look for organizer property
+            if (s.isEmpty() && !bIsReply) {
+                s = sSenderSearchKeyEmail;
+            }
+            // TODO: Use the common name?
+            if (!s.isEmpty()) {
+                event->setOrganizer(s);
+            }
 
-                QDateTime dt = tnefMsg->property(0x819b).toDateTime();
-                if (!dt.isValid()) {
-                    dt = tnefMsg->property(0x0060).toDateTime();
-                }
-                event->setDtStart(dt); // ## Format??
+            QDateTime dt = tnefMsg->property(0x819b).toDateTime();
+            if (!dt.isValid()) {
+                dt = tnefMsg->property(0x0060).toDateTime();
+            }
+            event->setDtStart(dt); // ## Format??
 
-                dt = tnefMsg->property(0x819c).toDateTime();
-                if (!dt.isValid()) {
-                    dt = tnefMsg->property(0x0061).toDateTime();
-                }
-                event->setDtEnd(dt);
-
-                s = tnefMsg->findProp(0x810d);
-                event->setLocation(s);
-                // is it OK to set this to OPAQUE always ??
-                // vPart += "TRANSP:OPAQUE\n"; ###FIXME, portme!
-                // vPart += "SEQUENCE:0\n";
-
-                // is "0x0023" OK  -  or should we look for "0x0003" ??
-                s = tnefMsg->findProp(0x0062);
-                event->setUid(s);
-
-                // PENDING(khz): is this value in local timezone? Must it be
-                // adjusted? Most likely this is a bug in the server or in
-                // Outlook - we ignore it for now.
-                s = tnefMsg->findProp(0x8202).remove(u'-').remove(u':');
-                // ### kcal always uses currentDateTime()
-                // event->setDtStamp( QDateTime::fromString( s ) );
-
-                s = tnefMsg->findNamedProp(u"Keywords"_s);
-                event->setCategories(s);
-
-                s = tnefMsg->findProp(0x1000);
-                if (s.isEmpty()) {
-                    s = tnefMsg->findProp(0x3fd9);
-                }
-                event->setDescription(s);
+            dt = tnefMsg->property(0x819c).toDateTime();
+            if (!dt.isValid()) {
+                dt = tnefMsg->property(0x0061).toDateTime();
+            }
+            event->setDtEnd(dt);
+
+            s = tnefMsg->findProp(0x810d);
+            event->setLocation(s);
+            // is it OK to set this to OPAQUE always ??
+            // vPart += "TRANSP:OPAQUE\n"; ###FIXME, portme!
+            // vPart += "SEQUENCE:0\n";
+
+            // is "0x0023" OK  -  or should we look for "0x0003" ??
+            s = tnefMsg->findProp(0x0062);
+            event->setUid(s);
+
+            // PENDING(khz): is this value in local timezone? Must it be
+            // adjusted? Most likely this is a bug in the server or in
+            // Outlook - we ignore it for now.
+            s = tnefMsg->findProp(0x8202).remove(u'-').remove(u':');
+            // ### kcal always uses currentDateTime()
+            // event->setDtStamp( QDateTime::fromString( s ) );
+
+            s = tnefMsg->findNamedProp(u"Keywords"_s);
+            event->setCategories(s);
+
+            s = tnefMsg->findProp(0x1000);
+            if (s.isEmpty()) {
+                s = tnefMsg->findProp(0x3fd9);
+            }
+            event->setDescription(s);
 
-                s = tnefMsg->findProp(0x0070);
-                if (s.isEmpty()) {
-                    s = tnefMsg->findProp(0x0037);
+            s = tnefMsg->findProp(0x0070);
+            if (s.isEmpty()) {
+                s = tnefMsg->findProp(0x0037);
+            }
+            event->setSummary(s);
+
+            s = tnefMsg->findProp(0x0026);
+            event->setPriority(s.toInt());
+            // is reminder flag set ?
+            if (!tnefMsg->findProp(0x8503).isEmpty()) {
+                Alarm::Ptr alarm(new Alarm(event.data())); // TODO: fix when KCalendarCore::Alarm is fixed
+                QDateTime highNoonTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8502).remove(u'-').remove(u':'));
+                QDateTime wakeMeUpTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8560, QString()).remove(u'-').remove(u':'));
+                alarm->setTime(wakeMeUpTime);
+
+                if (highNoonTime.isValid() && wakeMeUpTime.isValid()) {
+                    alarm->setStartOffset(Duration(highNoonTime, wakeMeUpTime));
+                } else {
+                    // default: wake them up 15 minutes before the appointment
+                    alarm->setStartOffset(Duration(15 * 60));
                 }
-                event->setSummary(s);
-
-                s = tnefMsg->findProp(0x0026);
-                event->setPriority(s.toInt());
-                // is reminder flag set ?
-                if (!tnefMsg->findProp(0x8503).isEmpty()) {
-                    Alarm::Ptr alarm(new Alarm(event.data())); // TODO: fix when KCalendarCore::Alarm is fixed
-                    QDateTime highNoonTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8502).remove(u'-').remove(u':'));
-                    QDateTime wakeMeUpTime = pureISOToLocalQDateTime(tnefMsg->findProp(0x8560, QString()).remove(u'-').remove(u':'));
-                    alarm->setTime(wakeMeUpTime);
-
-                    if (highNoonTime.isValid() && wakeMeUpTime.isValid()) {
-                        alarm->setStartOffset(Duration(highNoonTime, wakeMeUpTime));
-                    } else {
-                        // default: wake them up 15 minutes before the appointment
-                        alarm->setStartOffset(Duration(15 * 60));
-                    }
-                    alarm->setDisplayAlarm(i18n("Reminder"));
+                alarm->setDisplayAlarm(i18n("Reminder"));
 
-                    // Sorry: the different action types are not known (yet)
-                    //        so we always set 'DISPLAY' (no sounds, no images...)
-                    event->addAlarm(alarm);
-                }
-                // ensure we have a uid for this event
-                if (event->uid().isEmpty()) {
-                    event->setUid(CalFormat::createUniqueId());
-                }
-                cal->addEvent(event);
-                // bOk = true;
-                // we finished composing a vCal
-            } else if (bCompatClassNote || "IPM.CONTACT"_L1 == msgClass) {
-                addressee.setUid(stringProp(tnefMsg, attMSGID));
-                addressee.setFormattedName(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME));
-                KContacts::Email email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL1EMAILADDRESS)));
-                email.setPreferred(true);
-                addressee.addEmail(email);
-                addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL2EMAILADDRESS))));
-                addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL3EMAILADDRESS))));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-IMAddress"_s, sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_IMADDRESS)));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-SpousesName"_s, stringProp(tnefMsg, MAPI_TAG_PR_SPOUSE_NAME));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-ManagersName"_s, stringProp(tnefMsg, MAPI_TAG_PR_MANAGER_NAME));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-AssistantsName"_s, stringProp(tnefMsg, MAPI_TAG_PR_ASSISTANT));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Department"_s, stringProp(tnefMsg, MAPI_TAG_PR_DEPARTMENT_NAME));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Office"_s, stringProp(tnefMsg, MAPI_TAG_PR_OFFICE_LOCATION));
-                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Profession"_s, stringProp(tnefMsg, MAPI_TAG_PR_PROFESSION));
-
-                QString s = tnefMsg->findProp(MAPI_TAG_PR_WEDDING_ANNIVERSARY).remove(u'-').remove(u':');
-                if (!s.isEmpty()) {
-                    addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Anniversary"_s, s);
-                }
+                // Sorry: the different action types are not known (yet)
+                //        so we always set 'DISPLAY' (no sounds, no images...)
+                event->addAlarm(alarm);
+            }
+            // ensure we have a uid for this event
+            if (event->uid().isEmpty()) {
+                event->setUid(CalFormat::createUniqueId());
+            }
 
-                KContacts::ResourceLocatorUrl url;
-                url.setUrl(QUrl(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_WEBPAGE))));
-
-                addressee.setUrl(url);
-
-                // collect parts of Name entry
-                addressee.setFamilyName(stringProp(tnefMsg, MAPI_TAG_PR_SURNAME));
-                addressee.setGivenName(stringProp(tnefMsg, MAPI_TAG_PR_GIVEN_NAME));
-                addressee.setAdditionalName(stringProp(tnefMsg, MAPI_TAG_PR_MIDDLE_NAME));
-                addressee.setPrefix(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME_PREFIX));
-                addressee.setSuffix(stringProp(tnefMsg, MAPI_TAG_PR_GENERATION));
-
-                addressee.setNickName(stringProp(tnefMsg, MAPI_TAG_PR_NICKNAME));
-                addressee.setRole(stringProp(tnefMsg, MAPI_TAG_PR_TITLE));
-                addressee.setOrganization(stringProp(tnefMsg, MAPI_TAG_PR_COMPANY_NAME));
-                /*
-                the MAPI property ID of this (multiline) )field is unknown:
-                vPart += stringProp(tnefMsg, "\n","NOTE", ... , "" );
-                */
-
-                KContacts::Address adr;
-                adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_PO_BOX));
-                adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STREET));
-                adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_CITY));
-                adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STATE_OR_PROVINCE));
-                adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_POSTAL_CODE));
-                adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_COUNTRY));
-                adr.setType(KContacts::Address::Home);
-                addressee.insertAddress(adr);
-
-                adr.setPostOfficeBox(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOBOX)));
-                adr.setStreet(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTREET)));
-                adr.setLocality(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCITY)));
-                adr.setRegion(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTATE)));
-                adr.setPostalCode(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOSTALCODE)));
-                adr.setCountry(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCOUNTRY)));
-                adr.setType(KContacts::Address::Work);
-                addressee.insertAddress(adr);
-
-                adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_PO_BOX));
-                adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STREET));
-                adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_CITY));
-                adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STATE_OR_PROVINCE));
-                adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_POSTAL_CODE));
-                adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_COUNTRY));
-                adr.setType(KContacts::Address::Dom);
-                addressee.insertAddress(adr);
-
-                // problem: the 'other' address was stored by KOrganizer in
-                //          a line looking like the following one:
-                // vPart += "\nADR;TYPE=dom;TYPE=intl;TYPE=parcel;TYPE=postal;TYPE=work;"
-                //          "TYPE=home:other_pobox;;other_str1\nother_str2;other_loc;other_region;"
-                //          "other_pocode;other_country"
-
-                QString nr;
-                nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_TELEPHONE_NUMBER);
-                addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Home));
-                nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_TELEPHONE_NUMBER);
-                addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Work));
-                nr = stringProp(tnefMsg, MAPI_TAG_PR_MOBILE_TELEPHONE_NUMBER);
-                addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Cell));
-                nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_FAX_NUMBER);
-                addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Home));
-                nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_FAX_NUMBER);
-                addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Work));
-
-                s = tnefMsg->findProp(MAPI_TAG_PR_BIRTHDAY).remove(u'-').remove(u':');
-                if (!s.isEmpty()) {
-                    addressee.setBirthday(QDateTime::fromString(s));
-                }
+            MemoryCalendar::Ptr cal(new MemoryCalendar(QTimeZone::utc()));
+            cal->addEvent(event);
+            // bOk = true;
+            // we finished composing a vCal
+            return calFormat.toString(cal);
+        }
+    }
 
-                // bOk = (!addressee.isEmpty());
-            } else if ("IPM.NOTE"_L1 == msgClass) {
-            } // else if ... and so on ...
+    return {};
+}
+
+QByteArray KTnef::messageToVcard(const KTNEFMessage *tnefMsg)
+{
+    KContacts::Addressee addressee;
+
+    // Everything depends from property PR_MESSAGE_CLASS
+    // (this is added by KTNEFParser):
+    QString msgClass = tnefMsg->findProp(0x001A, QString(), true).toUpper();
+    if (!msgClass.isEmpty()) {
+        bool bCompatClassNote = (msgClass == "IPM.MICROSOFT MAIL.NOTE"_L1);
+        if (bCompatClassNote || "IPM.CONTACT"_L1 == msgClass) {
+            addressee.setUid(stringProp(tnefMsg, attMSGID));
+            addressee.setFormattedName(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME));
+            KContacts::Email email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL1EMAILADDRESS)));
+            email.setPreferred(true);
+            addressee.addEmail(email);
+            addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL2EMAILADDRESS))));
+            addressee.addEmail(KContacts::Email(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_EMAIL3EMAILADDRESS))));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-IMAddress"_s, sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_IMADDRESS)));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-SpousesName"_s, stringProp(tnefMsg, MAPI_TAG_PR_SPOUSE_NAME));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-ManagersName"_s, stringProp(tnefMsg, MAPI_TAG_PR_MANAGER_NAME));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-AssistantsName"_s, stringProp(tnefMsg, MAPI_TAG_PR_ASSISTANT));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Department"_s, stringProp(tnefMsg, MAPI_TAG_PR_DEPARTMENT_NAME));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Office"_s, stringProp(tnefMsg, MAPI_TAG_PR_OFFICE_LOCATION));
+            addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Profession"_s, stringProp(tnefMsg, MAPI_TAG_PR_PROFESSION));
+
+            QString s = tnefMsg->findProp(MAPI_TAG_PR_WEDDING_ANNIVERSARY).remove(u'-').remove(u':');
+            if (!s.isEmpty()) {
+                addressee.insertCustom(u"KADDRESSBOOK"_s, u"X-Anniversary"_s, s);
+            }
+
+            KContacts::ResourceLocatorUrl url;
+            url.setUrl(QUrl(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_WEBPAGE))));
+
+            addressee.setUrl(url);
+
+            // collect parts of Name entry
+            addressee.setFamilyName(stringProp(tnefMsg, MAPI_TAG_PR_SURNAME));
+            addressee.setGivenName(stringProp(tnefMsg, MAPI_TAG_PR_GIVEN_NAME));
+            addressee.setAdditionalName(stringProp(tnefMsg, MAPI_TAG_PR_MIDDLE_NAME));
+            addressee.setPrefix(stringProp(tnefMsg, MAPI_TAG_PR_DISPLAY_NAME_PREFIX));
+            addressee.setSuffix(stringProp(tnefMsg, MAPI_TAG_PR_GENERATION));
+
+            addressee.setNickName(stringProp(tnefMsg, MAPI_TAG_PR_NICKNAME));
+            addressee.setRole(stringProp(tnefMsg, MAPI_TAG_PR_TITLE));
+            addressee.setOrganization(stringProp(tnefMsg, MAPI_TAG_PR_COMPANY_NAME));
+            /*
+            the MAPI property ID of this (multiline) )field is unknown:
+            vPart += stringProp(tnefMsg, "\n","NOTE", ... , "" );
+            */
+
+            KContacts::Address adr;
+            adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_PO_BOX));
+            adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STREET));
+            adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_CITY));
+            adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_STATE_OR_PROVINCE));
+            adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_POSTAL_CODE));
+            adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_HOME_ADDRESS_COUNTRY));
+            adr.setType(KContacts::Address::Home);
+            addressee.insertAddress(adr);
+
+            adr.setPostOfficeBox(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOBOX)));
+            adr.setStreet(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTREET)));
+            adr.setLocality(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCITY)));
+            adr.setRegion(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSSTATE)));
+            adr.setPostalCode(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSPOSTALCODE)));
+            adr.setCountry(sNamedProp(tnefMsg, QStringLiteral(MAPI_TAG_CONTACT_BUSINESSADDRESSCOUNTRY)));
+            adr.setType(KContacts::Address::Work);
+            addressee.insertAddress(adr);
+
+            adr.setPostOfficeBox(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_PO_BOX));
+            adr.setStreet(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STREET));
+            adr.setLocality(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_CITY));
+            adr.setRegion(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_STATE_OR_PROVINCE));
+            adr.setPostalCode(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_POSTAL_CODE));
+            adr.setCountry(stringProp(tnefMsg, MAPI_TAG_PR_OTHER_ADDRESS_COUNTRY));
+            adr.setType(KContacts::Address::Dom);
+            addressee.insertAddress(adr);
+
+            // problem: the 'other' address was stored by KOrganizer in
+            //          a line looking like the following one:
+            // vPart += "\nADR;TYPE=dom;TYPE=intl;TYPE=parcel;TYPE=postal;TYPE=work;"
+            //          "TYPE=home:other_pobox;;other_str1\nother_str2;other_loc;other_region;"
+            //          "other_pocode;other_country"
+
+            QString nr;
+            nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_TELEPHONE_NUMBER);
+            addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Home));
+            nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_TELEPHONE_NUMBER);
+            addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Work));
+            nr = stringProp(tnefMsg, MAPI_TAG_PR_MOBILE_TELEPHONE_NUMBER);
+            addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Cell));
+            nr = stringProp(tnefMsg, MAPI_TAG_PR_HOME_FAX_NUMBER);
+            addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Home));
+            nr = stringProp(tnefMsg, MAPI_TAG_PR_BUSINESS_FAX_NUMBER);
+            addressee.insertPhoneNumber(KContacts::PhoneNumber(nr, KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Work));
+
+            s = tnefMsg->findProp(MAPI_TAG_PR_BIRTHDAY).remove(u'-').remove(u':');
+            if (!s.isEmpty()) {
+                addressee.setBirthday(QDateTime::fromString(s));
+            }
+
+            // bOk = (!addressee.isEmpty());
+            KContacts::VCardConverter converter;
+            return converter.createVCard(addressee);
         }
     }
 
-    // Compose return string
-    const QString iCal = calFormat.toString(cal);
-    if (!iCal.isEmpty()) {
-        // This was an iCal
-        return iCal;
+    return {};
+}
+
+QString KTnef::msTNEFToVPart(const QByteArray &tnef)
+{
+    KTNEFParser parser;
+    QByteArray b(tnef);
+    QBuffer buf(&b);
+
+    if (parser.openDevice(&buf)) {
+        KTNEFMessage *tnefMsg = parser.message();
+        if (const auto ical = messageToIcal(tnefMsg); !ical.isEmpty()) {
+            return ical;
+        }
+        return QString::fromUtf8(messageToVcard(tnefMsg));
     }
 
-    // Not an iCal - try a vCard
-    KContacts::VCardConverter converter;
-    return QString::fromUtf8(converter.createVCard(addressee));
+    return {};
 }
 
 QString KTnef::formatTNEFInvitation(const QByteArray &tnef, const MemoryCalendar::Ptr &cal, KCalUtils::InvitationFormatterHelper *h)
diff --git a/src/formatter.h b/src/formatter.h
index 87a8a98..3b4cac7 100644
--- a/src/formatter.h
+++ b/src/formatter.h
@@ -31,6 +31,29 @@ class InvitationFormatterHelper;
 
 namespace KTnef
 {
+
+class KTNEFMessage;
+
+/*!
+    Create an iCal representation of an event, todo or invitation in a TNEF message.
+
+    If the message does not contain a calendar object, an empty string is returned.
+
+    \a tnefMsg the TNEF message to convert.
+    \since 26.12
+*/
+[[nodiscard]] KTNEF_EXPORT QString messageToIcal(const KTnef::KTNEFMessage *tnefMsg);
+
+/*!
+    Create a vCard representation of a contact contained in a TNEF message.
+
+    If the message does not contain a contact, an empty string is returned.
+
+    \a tnefMsg the TNEF message to convert.
+    \since 26.12
+*/
+[[nodiscard]] KTNEF_EXPORT QByteArray messageToVcard(const KTnef::KTNEFMessage *tnefMsg);
+
 /*!
     Formats a @acronym TNEF attachment to an HTML mail.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.