File indexing completed on 2025-01-19 04:46:49

0001 /*
0002   This file is part of kdepim.
0003 
0004   SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org>
0005   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0006   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0007   SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>
0008 
0009   SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "attendeeselector.h"
0013 #include "calendarinterface.h"
0014 #include "delegateselector.h"
0015 #include "memorycalendarmemento.h"
0016 #include "reactiontoinvitationdialog.h"
0017 #include "syncitiphandler.h"
0018 
0019 #include <KIdentityManagementCore/IdentityManager>
0020 
0021 #include <MessageViewer/BodyPartURLHandler>
0022 #include <MessageViewer/HtmlWriter>
0023 #include <MessageViewer/MessagePartRenderPlugin>
0024 #include <MessageViewer/MessagePartRendererBase>
0025 #include <MessageViewer/MessageViewerSettings>
0026 #include <MessageViewer/Viewer>
0027 #include <MimeTreeParser/BodyPart>
0028 #include <MimeTreeParser/MessagePart>
0029 using namespace MessageViewer;
0030 
0031 #include <KCalendarCore/ICalFormat>
0032 using namespace KCalendarCore;
0033 
0034 #include <KCalUtils/IncidenceFormatter>
0035 
0036 #include <KMime/Message>
0037 
0038 #include <KIdentityManagementCore/Identity>
0039 
0040 #include <KEmailAddress>
0041 
0042 #include <Akonadi/MessageQueueJob>
0043 #include <MailTransport/TransportManager>
0044 
0045 #include "text_calendar_debug.h"
0046 
0047 #include <KIO/FileCopyJob>
0048 #include <KIO/JobUiDelegate>
0049 #include <KIO/OpenUrlJob>
0050 #include <KIO/StatJob>
0051 #include <KLocalizedString>
0052 #include <KMessageBox>
0053 
0054 #include <QDBusServiceWatcher>
0055 #include <QDesktopServices>
0056 #include <QFileDialog>
0057 #include <QIcon>
0058 #include <QInputDialog>
0059 #include <QMenu>
0060 #include <QMimeDatabase>
0061 #include <QPointer>
0062 #include <QTemporaryFile>
0063 #include <QUrl>
0064 
0065 using namespace MailTransport;
0066 
0067 namespace
0068 {
0069 static bool hasMyWritableEventsFolders(const QString &family)
0070 {
0071     Q_UNUSED(family)
0072 #if 0 // TODO port to Akonadi
0073     QString myfamily = family;
0074     if (family.isEmpty()) {
0075         myfamily = QStringLiteral("calendar");
0076     }
0077 
0078 #ifndef KDEPIM_NO_KRESOURCES
0079     CalendarResourceManager manager(myfamily);
0080     manager.readConfig();
0081 
0082     CalendarResourceManager::ActiveIterator it;
0083     for (it = manager.activeBegin(); it != manager.activeEnd(); ++it) {
0084         if ((*it)->readOnly()) {
0085             continue;
0086         }
0087 
0088         const QStringList subResources = (*it)->subresources();
0089         if (subResources.isEmpty()) {
0090             return true;
0091         }
0092 
0093         QStringList::ConstIterator subIt;
0094         for (subIt = subResources.begin(); subIt != subResources.end(); ++subIt) {
0095             if (!(*it)->subresourceActive((*subIt))) {
0096                 continue;
0097             }
0098             if ((*it)->type() == "imap" || (*it)->type() == "kolab") {
0099                 if ((*it)->subresourceType((*subIt)) == "todo"
0100                     || (*it)->subresourceType((*subIt)) == "journal"
0101                     || !(*subIt).contains("/.INBOX.directory/")) {
0102                     continue;
0103                 }
0104             }
0105             return true;
0106         }
0107     }
0108     return false;
0109 #endif
0110 #else
0111     qCDebug(TEXT_CALENDAR_LOG) << "Disabled code, port to Akonadi";
0112     return true;
0113 #endif
0114 }
0115 
0116 static bool occurredAlready(const Incidence::Ptr &incidence)
0117 {
0118     Q_ASSERT(incidence);
0119     const QDateTime now = QDateTime::currentDateTime();
0120     const QDate today = now.date();
0121 
0122     if (incidence->recurs()) {
0123         const QDateTime nextDate = incidence->recurrence()->getNextDateTime(now);
0124 
0125         return !nextDate.isValid();
0126     } else {
0127         const QDateTime incidenceDate = incidence->dateTime(Incidence::RoleDisplayEnd);
0128         if (incidenceDate.isValid()) {
0129             return incidence->allDay() ? (incidenceDate.date() < today) : (incidenceDate < QDateTime::currentDateTime());
0130         }
0131     }
0132 
0133     return false;
0134 }
0135 
0136 class KMInvitationFormatterHelper : public KCalUtils::InvitationFormatterHelper
0137 {
0138 public:
0139     KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr &bodyPart, const KCalendarCore::MemoryCalendar::Ptr &calendar)
0140         : mBodyPart(bodyPart)
0141         , mCalendar(calendar)
0142     {
0143     }
0144 
0145     QString generateLinkURL(const QString &id) override
0146     {
0147         return mBodyPart->makeLink(id);
0148     }
0149 
0150     [[nodiscard]] KCalendarCore::Calendar::Ptr calendar() const override
0151     {
0152         return mCalendar;
0153     }
0154 
0155 private:
0156     const MimeTreeParser::MessagePartPtr mBodyPart;
0157     const KCalendarCore::MemoryCalendar::Ptr mCalendar;
0158 };
0159 
0160 static QString getSender(const MimeTreeParser::MessagePart *msgPart)
0161 {
0162     if (auto msg = dynamic_cast<KMime::Message *>(msgPart->content()->topLevel()); msg != nullptr) {
0163         return msg->sender()->asUnicodeString();
0164     }
0165     return {};
0166 }
0167 
0168 class Formatter : public MessageViewer::MessagePartRendererBase
0169 {
0170 public:
0171     bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override
0172     {
0173         QMimeDatabase db;
0174         auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower()));
0175         if (!mt.isValid() || mt.name() != QLatin1StringView("text/calendar")) {
0176             return false;
0177         }
0178 
0179         auto nodeHelper = msgPart->nodeHelper();
0180         if (!nodeHelper) {
0181             return false;
0182         }
0183 
0184         /** Formatting is async now because we need to fetch incidences from akonadi.
0185             Basically this method (format()) will be called twice. The first time
0186             it creates the memento that fetches incidences and returns.
0187 
0188             When the memento finishes, this is called a second time, and we can proceed.
0189 
0190             BodyPartMementos are documented in MessageViewer/ObjectTreeParser
0191         */
0192         auto memento = dynamic_cast<MemoryCalendarMemento *>(msgPart->memento());
0193 
0194         if (memento) {
0195             if (memento->finished()) {
0196                 KMInvitationFormatterHelper helper(msgPart, memento->calendar());
0197                 QString source;
0198                 // If the bodypart does not have a charset specified, we need to fall back to utf8,
0199                 // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
0200                 if (msgPart->content()->contentType()->parameter(QStringLiteral("charset")).isEmpty()) {
0201                     const QByteArray &ba = msgPart->content()->decodedContent();
0202                     source = QString::fromUtf8(ba);
0203                 } else {
0204                     source = msgPart->text();
0205                 }
0206 
0207                 MemoryCalendar::Ptr cl(new MemoryCalendar(QTimeZone::systemTimeZone()));
0208 
0209                 const auto sender = getSender(msgPart.get());
0210                 const QString html = KCalUtils::IncidenceFormatter::formatICalInvitationNoHtml(source, cl, &helper, sender);
0211 
0212                 if (html.isEmpty()) {
0213                     return false;
0214                 }
0215                 writer->write(html);
0216             }
0217         } else {
0218             auto memento = new MemoryCalendarMemento();
0219             msgPart->setMemento(memento);
0220             QObject::connect(memento, &MemoryCalendarMemento::update, nodeHelper, &MimeTreeParser::NodeHelper::update);
0221         }
0222 
0223         return true;
0224     }
0225 };
0226 
0227 static QString directoryForStatus(Attendee::PartStat status)
0228 {
0229     QString dir;
0230     switch (status) {
0231     case Attendee::Accepted:
0232         dir = QStringLiteral("accepted");
0233         break;
0234     case Attendee::Tentative:
0235         dir = QStringLiteral("tentative");
0236         break;
0237     case Attendee::Declined:
0238         dir = QStringLiteral("cancel");
0239         break;
0240     case Attendee::Delegated:
0241         dir = QStringLiteral("delegated");
0242         break;
0243     case Attendee::NeedsAction:
0244         dir = QStringLiteral("request");
0245         break;
0246     default:
0247         break;
0248     }
0249     return dir;
0250 }
0251 
0252 static Incidence::Ptr stringToIncidence(const QString &iCal)
0253 {
0254     MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::systemTimeZone()));
0255     ICalFormat format;
0256     ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, iCal);
0257     if (!message) {
0258         // TODO: Error message?
0259         qCWarning(TEXT_CALENDAR_LOG) << "Can't parse this ical string: " << iCal;
0260         return {};
0261     }
0262 
0263     return message->event().dynamicCast<Incidence>();
0264 }
0265 
0266 class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler
0267 {
0268 public:
0269     UrlHandler()
0270     {
0271         // qCDebug(TEXT_CALENDAR_LOG) << "UrlHandler() (iCalendar)";
0272     }
0273 
0274     [[nodiscard]] QString name() const override
0275     {
0276         return QStringLiteral("calendar handler");
0277     }
0278 
0279     [[nodiscard]] Attendee findMyself(const Incidence::Ptr &incidence, const QString &receiver) const
0280     {
0281         const Attendee::List attendees = incidence->attendees();
0282         const auto idx = findMyself(attendees, receiver);
0283         if (idx >= 0) {
0284             return attendees.at(idx);
0285         }
0286         return {};
0287     }
0288 
0289     [[nodiscard]] int findMyself(const Attendee::List &attendees, const QString &receiver) const
0290     {
0291         // Find myself. There will always be all attendees listed, even if
0292         // only I need to answer it.
0293         for (int i = 0; i < attendees.size(); ++i) {
0294             // match only the email part, not the name
0295             if (KEmailAddress::compareEmail(attendees.at(i).email(), receiver, false)) {
0296                 // We are the current one, and even the receiver, note
0297                 // this and quit searching.
0298                 return i;
0299             }
0300         }
0301         return -1;
0302     }
0303 
0304     static bool heuristicalRSVP(const Incidence::Ptr &incidence)
0305     {
0306         bool rsvp = true; // better send superfluously than not at all
0307         const Attendee::List attendees = incidence->attendees();
0308         Attendee::List::ConstIterator it;
0309         Attendee::List::ConstIterator end(attendees.constEnd());
0310         for (it = attendees.constBegin(); it != end; ++it) {
0311             if (it == attendees.constBegin()) {
0312                 rsvp = (*it).RSVP(); // use what the first one has
0313             } else {
0314                 if ((*it).RSVP() != rsvp) {
0315                     rsvp = true; // they differ, default
0316                     break;
0317                 }
0318             }
0319         }
0320         return rsvp;
0321     }
0322 
0323     static Attendee::Role heuristicalRole(const Incidence::Ptr &incidence)
0324     {
0325         Attendee::Role role = Attendee::OptParticipant;
0326         const Attendee::List attendees = incidence->attendees();
0327         Attendee::List::ConstIterator it;
0328         Attendee::List::ConstIterator end = attendees.constEnd();
0329 
0330         for (it = attendees.constBegin(); it != end; ++it) {
0331             if (it == attendees.constBegin()) {
0332                 role = (*it).role(); // use what the first one has
0333             } else {
0334                 if ((*it).role() != role) {
0335                     role = Attendee::OptParticipant; // they differ, default
0336                     break;
0337                 }
0338             }
0339         }
0340         return role;
0341     }
0342 
0343     static Attachment findAttachment(const QString &name, const QString &iCal)
0344     {
0345         Incidence::Ptr incidence = stringToIncidence(iCal);
0346 
0347         // get the attachment by name from the incidence
0348         Attachment::List attachments = incidence->attachments();
0349         Attachment attachment;
0350         const Attachment::List::ConstIterator end = attachments.constEnd();
0351         for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) {
0352             if ((*it).label() == name) {
0353                 attachment = *it;
0354                 break;
0355             }
0356         }
0357 
0358         if (attachment.isEmpty()) {
0359             KMessageBox::error(nullptr, i18n("No attachment named \"%1\" found in the invitation.", name));
0360             return Attachment();
0361         }
0362 
0363         if (attachment.isUri()) {
0364             bool fileExists = false;
0365             QUrl attachmentUrl(attachment.uri());
0366             if (attachmentUrl.isLocalFile()) {
0367                 fileExists = QFile::exists(attachmentUrl.toLocalFile());
0368             } else {
0369                 auto job = KIO::stat(attachmentUrl, KIO::StatJob::SourceSide, KIO::StatBasic);
0370                 fileExists = job->exec();
0371             }
0372             if (!fileExists) {
0373                 KMessageBox::information(nullptr,
0374                                          i18n("The invitation attachment \"%1\" is a web link that "
0375                                               "is inaccessible from this computer. Please ask the event "
0376                                               "organizer to resend the invitation with this attachment "
0377                                               "stored inline instead of a link.",
0378                                               attachmentUrl.toDisplayString()));
0379                 return Attachment();
0380             }
0381         }
0382         return attachment;
0383     }
0384 
0385     static QString findReceiver(KMime::Content *node)
0386     {
0387         if (!node || !node->topLevel()) {
0388             return {};
0389         }
0390 
0391         QString receiver;
0392         KIdentityManagementCore::IdentityManager *im = KIdentityManagementCore::IdentityManager::self();
0393 
0394         KMime::Types::Mailbox::List addrs;
0395         if (auto header = node->topLevel()->header<KMime::Headers::To>()) {
0396             addrs = header->mailboxes();
0397         }
0398         int found = 0;
0399         QList<KMime::Types::Mailbox>::const_iterator end = addrs.constEnd();
0400         for (QList<KMime::Types::Mailbox>::const_iterator it = addrs.constBegin(); it != end; ++it) {
0401             if (im->identityForAddress(QLatin1StringView((*it).address())) != KIdentityManagementCore::Identity::null()) {
0402                 // Ok, this could be us
0403                 ++found;
0404                 receiver = QLatin1StringView((*it).address());
0405             }
0406         }
0407 
0408         KMime::Types::Mailbox::List ccaddrs;
0409         if (auto header = node->topLevel()->header<KMime::Headers::Cc>()) {
0410             ccaddrs = header->mailboxes();
0411         }
0412         end = ccaddrs.constEnd();
0413         for (QList<KMime::Types::Mailbox>::const_iterator it = ccaddrs.constBegin(); it != end; ++it) {
0414             if (im->identityForAddress(QLatin1StringView((*it).address())) != KIdentityManagementCore::Identity::null()) {
0415                 // Ok, this could be us
0416                 ++found;
0417                 receiver = QLatin1StringView((*it).address());
0418             }
0419         }
0420         if (found != 1) {
0421             QStringList possibleAddrs;
0422             bool ok;
0423             QString selectMessage;
0424             if (found == 0) {
0425                 selectMessage = i18n(
0426                     "<qt>None of your identities match the receiver of this message,<br/>"
0427                     "please choose which of the following addresses is yours,<br/> if any, "
0428                     "or select one of your identities to use in the reply:</qt>");
0429                 possibleAddrs += im->allEmails();
0430             } else {
0431                 selectMessage = i18n(
0432                     "<qt>Several of your identities match the receiver of this message,<br/>"
0433                     "please choose which of the following addresses is yours:</qt>");
0434                 possibleAddrs.reserve(addrs.count() + ccaddrs.count());
0435                 for (const KMime::Types::Mailbox &mbx : std::as_const(addrs)) {
0436                     possibleAddrs.append(QLatin1StringView(mbx.address()));
0437                 }
0438                 for (const KMime::Types::Mailbox &mbx : std::as_const(ccaddrs)) {
0439                     possibleAddrs.append(QLatin1StringView(mbx.address()));
0440                 }
0441             }
0442 
0443             // select default identity by default
0444             const QString defaultAddr = im->defaultIdentity().primaryEmailAddress();
0445             const int defaultIndex = qMax(0, possibleAddrs.indexOf(defaultAddr));
0446 
0447             receiver = QInputDialog::getItem(nullptr, i18n("Select Address"), selectMessage, possibleAddrs, defaultIndex, false, &ok);
0448 
0449             if (!ok) {
0450                 receiver.clear();
0451             }
0452         }
0453         return receiver;
0454     }
0455 
0456     [[nodiscard]] Attendee setStatusOnMyself(const Incidence::Ptr &incidence, const Attendee &myself, Attendee::PartStat status, const QString &receiver) const
0457     {
0458         QString name;
0459         QString email;
0460         KEmailAddress::extractEmailAddressAndName(receiver, email, name);
0461         if (name.isEmpty() && !myself.isNull()) {
0462             name = myself.name();
0463         }
0464         if (email.isEmpty() && !myself.isNull()) {
0465             email = myself.email();
0466         }
0467         Q_ASSERT(!email.isEmpty()); // delivery must be possible
0468 
0469         Attendee newMyself(name,
0470                            email,
0471                            true, // RSVP, otherwise we would not be here
0472                            status,
0473                            !myself.isNull() ? myself.role() : heuristicalRole(incidence),
0474                            myself.uid());
0475         if (!myself.isNull()) {
0476             newMyself.setDelegate(myself.delegate());
0477             newMyself.setDelegator(myself.delegator());
0478         }
0479 
0480         // Make sure only ourselves is in the event
0481         incidence->clearAttendees();
0482         if (!newMyself.isNull()) {
0483             incidence->addAttendee(newMyself);
0484         }
0485         return newMyself;
0486     }
0487 
0488     enum MailType {
0489         Answer,
0490         Delegation,
0491         Forward,
0492         DeclineCounter,
0493     };
0494 
0495     bool mailICal(const QString &receiver,
0496                   const QString &to,
0497                   const QString &iCal,
0498                   const QString &subject,
0499                   const QString &status,
0500                   bool delMessage,
0501                   Viewer *viewerInstance) const
0502     {
0503         qCDebug(TEXT_CALENDAR_LOG) << "Mailing message:" << iCal;
0504 
0505         KMime::Message::Ptr msg(new KMime::Message);
0506         if (MessageViewer::MessageViewerSettings::self()->exchangeCompatibleInvitations()) {
0507             msg->subject()->fromUnicodeString(status, "utf-8");
0508             QString tsubject = subject;
0509             tsubject.remove(i18n("Answer: "));
0510             if (status == QLatin1StringView("cancel")) {
0511                 msg->subject()->fromUnicodeString(i18nc("Not able to attend.", "Declined: %1", tsubject), "utf-8");
0512             } else if (status == QLatin1StringView("tentative")) {
0513                 msg->subject()->fromUnicodeString(i18nc("Unsure if it is possible to attend.", "Tentative: %1", tsubject), "utf-8");
0514             } else if (status == QLatin1StringView("accepted")) {
0515                 msg->subject()->fromUnicodeString(i18nc("Accepted the invitation.", "Accepted: %1", tsubject), "utf-8");
0516             } else {
0517                 msg->subject()->fromUnicodeString(subject, "utf-8");
0518             }
0519         } else {
0520             msg->subject()->fromUnicodeString(subject, "utf-8");
0521         }
0522         msg->to()->fromUnicodeString(to, "utf-8");
0523         msg->from()->fromUnicodeString(receiver, "utf-8");
0524         msg->date()->setDateTime(QDateTime::currentDateTime());
0525 
0526         if (MessageViewer::MessageViewerSettings::self()->legacyBodyInvites()) {
0527             auto ct = msg->contentType(); // create
0528             ct->setMimeType("text/calendar");
0529             ct->setCharset("utf-8");
0530             ct->setName(QStringLiteral("cal.ics"), "utf-8");
0531             ct->setParameter(QStringLiteral("method"), QStringLiteral("reply"));
0532 
0533             auto disposition = new KMime::Headers::ContentDisposition;
0534             disposition->setDisposition(KMime::Headers::CDinline);
0535             msg->setHeader(disposition);
0536             msg->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
0537             const QString answer = i18n("Invitation answer attached.");
0538             msg->setBody(answer.toUtf8());
0539         } else {
0540             // We need to set following 4 lines by hand else KMime::Content::addContent
0541             // will create a new Content instance for us to attach the main message
0542             // what we don't need cause we already have the main message instance where
0543             // 2 additional messages are attached.
0544             KMime::Headers::ContentType *ct = msg->contentType();
0545             ct->setMimeType("multipart/mixed");
0546             ct->setBoundary(KMime::multiPartBoundary());
0547 
0548             // Set the first multipart, the body message.
0549             auto bodyMessage = new KMime::Content;
0550             auto bodyDisposition = new KMime::Headers::ContentDisposition;
0551             bodyDisposition->setDisposition(KMime::Headers::CDinline);
0552             auto bodyMessageCt = bodyMessage->contentType();
0553             bodyMessageCt->setMimeType("text/plain");
0554             bodyMessageCt->setCharset("utf-8");
0555             bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
0556             const QString answer = i18n("Invitation answer attached.");
0557             bodyMessage->setBody(answer.toUtf8());
0558             bodyMessage->setHeader(bodyDisposition);
0559             msg->appendContent(bodyMessage);
0560 
0561             // Set the second multipart, the attachment.
0562             auto attachMessage = new KMime::Content;
0563             auto attachDisposition = new KMime::Headers::ContentDisposition;
0564             attachDisposition->setDisposition(KMime::Headers::CDattachment);
0565             auto attachCt = attachMessage->contentType();
0566             attachCt->setMimeType("text/calendar");
0567             attachCt->setCharset("utf-8");
0568             attachCt->setName(QStringLiteral("cal.ics"), "utf-8");
0569             attachCt->setParameter(QStringLiteral("method"), QStringLiteral("reply"));
0570             attachMessage->setHeader(attachDisposition);
0571             attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
0572             attachMessage->setBody(KMime::CRLFtoLF(iCal.toUtf8()));
0573             msg->appendContent(attachMessage);
0574         }
0575 
0576         // Try and match the receiver with an identity.
0577         // Setting the identity here is important, as that is used to select the correct
0578         // transport later
0579         KIdentityManagementCore::IdentityManager *im = KIdentityManagementCore::IdentityManager::self();
0580         const KIdentityManagementCore::Identity identity = im->identityForAddress(findReceiver(viewerInstance->message().data()));
0581 
0582         const bool nullIdentity = (identity == KIdentityManagementCore::Identity::null());
0583 
0584         if (!nullIdentity) {
0585             auto x_header = new KMime::Headers::Generic("X-KMail-Identity");
0586             x_header->from7BitString(QByteArray::number(identity.uoid()));
0587             msg->setHeader(x_header);
0588         }
0589 
0590         const bool identityHasTransport = !identity.transport().isEmpty();
0591         int transportId = -1;
0592         if (!nullIdentity && identityHasTransport) {
0593             transportId = identity.transport().toInt();
0594         } else {
0595             transportId = TransportManager::self()->defaultTransportId();
0596         }
0597         if (transportId == -1) {
0598             if (!TransportManager::self()->showTransportCreationDialog(nullptr, TransportManager::IfNoTransportExists)) {
0599                 return false;
0600             }
0601             transportId = TransportManager::self()->defaultTransportId();
0602         }
0603         auto header = new KMime::Headers::Generic("X-KMail-Transport");
0604         header->fromUnicodeString(QString::number(transportId), "utf-8");
0605         msg->setHeader(header);
0606 
0607         // Outlook will only understand the reply if the From: header is the
0608         // same as the To: header of the invitation message.
0609         if (!MessageViewer::MessageViewerSettings::self()->legacyMangleFromToHeaders()) {
0610             if (identity != KIdentityManagementCore::Identity::null()) {
0611                 msg->from()->fromUnicodeString(identity.fullEmailAddr(), "utf-8");
0612             }
0613             // Remove BCC from identity on ical invitations (kolab/issue474)
0614             msg->removeHeader<KMime::Headers::Bcc>();
0615         }
0616 
0617         msg->assemble();
0618         MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(transportId);
0619 
0620         auto job = new Akonadi::MessageQueueJob;
0621 
0622         job->addressAttribute().setTo(QStringList() << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(to)));
0623         job->transportAttribute().setTransportId(transport->id());
0624 
0625         if (transport->specifySenderOverwriteAddress()) {
0626             job->addressAttribute().setFrom(
0627                 KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress())));
0628         } else {
0629             job->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(msg->from()->asUnicodeString())));
0630         }
0631 
0632         job->setMessage(msg);
0633 
0634         if (!job->exec()) {
0635             qCWarning(TEXT_CALENDAR_LOG) << "Error queuing message in outbox:" << job->errorText();
0636             return false;
0637         }
0638         // We are not notified when mail was sent, so assume it was sent when queued.
0639         if (delMessage && MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
0640             viewerInstance->deleteMessage();
0641         }
0642         return true;
0643     }
0644 
0645     bool mail(Viewer *viewerInstance,
0646               const Incidence::Ptr &incidence,
0647               const QString &status,
0648               iTIPMethod method = iTIPReply,
0649               const QString &receiver = QString(),
0650               const QString &to = QString(),
0651               MailType type = Answer) const
0652     {
0653         // status is accepted/tentative/declined
0654         ICalFormat format;
0655         format.setTimeZone(QTimeZone::systemTimeZone());
0656         QString msg = format.createScheduleMessage(incidence, method);
0657         QString summary = incidence->summary();
0658         if (summary.isEmpty()) {
0659             summary = i18n("Incidence with no summary");
0660         }
0661         QString subject;
0662         switch (type) {
0663         case Answer:
0664             subject = i18n("Answer: %1", summary);
0665             break;
0666         case Delegation:
0667             subject = i18n("Delegated: %1", summary);
0668             break;
0669         case Forward:
0670             subject = i18n("Forwarded: %1", summary);
0671             break;
0672         case DeclineCounter:
0673             subject = i18n("Declined Counter Proposal: %1", summary);
0674             break;
0675         }
0676 
0677         // Set the organizer to the sender, if the ORGANIZER hasn't been set.
0678         if (incidence->organizer().isEmpty()) {
0679             QString tname;
0680             QString temail;
0681             KMime::Message::Ptr message = viewerInstance->message();
0682             KEmailAddress::extractEmailAddressAndName(message->sender()->asUnicodeString(), temail, tname);
0683             incidence->setOrganizer(Person(tname, temail));
0684         }
0685 
0686         QString recv = to;
0687         if (recv.isEmpty()) {
0688             recv = incidence->organizer().fullName();
0689         }
0690         return mailICal(receiver, recv, msg, subject, status, type != Forward, viewerInstance);
0691     }
0692 
0693     bool saveFile(const QString &receiver, const QString &iCal, const QString &type, MimeTreeParser::Interface::BodyPart *bodyPart) const
0694     {
0695         auto memento = dynamic_cast<MemoryCalendarMemento *>(bodyPart->memento());
0696         // This will block. There's no way to make it async without refactoring the memento mechanism
0697 
0698         auto itipHandler = new SyncItipHandler(receiver, iCal, type, memento->calendar());
0699 
0700         // If result is ResultCancelled, then we don't show the message box and return false so kmail
0701         // doesn't delete the e-mail.
0702         qCDebug(TEXT_CALENDAR_LOG) << "ITIPHandler result was " << itipHandler->result();
0703         const Akonadi::ITIPHandler::Result res = itipHandler->result();
0704         if (res == Akonadi::ITIPHandler::ResultError) {
0705             const QString errorMessage = itipHandler->errorMessage();
0706             if (!errorMessage.isEmpty()) {
0707                 qCCritical(TEXT_CALENDAR_LOG) << "Error while processing invitation: " << errorMessage;
0708                 KMessageBox::error(nullptr, errorMessage);
0709             }
0710             return false;
0711         }
0712 
0713         return res;
0714     }
0715 
0716     [[nodiscard]] bool cancelPastInvites(const Incidence::Ptr incidence, const QString &path) const
0717     {
0718         QString warnStr;
0719         QDateTime now = QDateTime::currentDateTime();
0720         QDate today = now.date();
0721         Incidence::IncidenceType type = Incidence::TypeUnknown;
0722         const bool occurred = occurredAlready(incidence);
0723         if (incidence->type() == Incidence::TypeEvent) {
0724             type = Incidence::TypeEvent;
0725             Event::Ptr event = incidence.staticCast<Event>();
0726             if (!event->allDay()) {
0727                 if (occurred) {
0728                     warnStr = i18n("\"%1\" occurred already.", event->summary());
0729                 } else if (event->dtStart() <= now && now <= event->dtEnd()) {
0730                     warnStr = i18n("\"%1\" is currently in-progress.", event->summary());
0731                 }
0732             } else {
0733                 if (occurred) {
0734                     warnStr = i18n("\"%1\" occurred already.", event->summary());
0735                 } else if (event->dtStart().date() <= today && today <= event->dtEnd().date()) {
0736                     warnStr = i18n("\"%1\", happening all day today, is currently in-progress.", event->summary());
0737                 }
0738             }
0739         } else if (incidence->type() == Incidence::TypeTodo) {
0740             type = Incidence::TypeTodo;
0741             Todo::Ptr todo = incidence.staticCast<Todo>();
0742             if (!todo->allDay()) {
0743                 if (todo->hasDueDate()) {
0744                     if (todo->dtDue() < now) {
0745                         warnStr = i18n("\"%1\" is past due.", todo->summary());
0746                     } else if (todo->hasStartDate() && todo->dtStart() <= now && now <= todo->dtDue()) {
0747                         warnStr = i18n("\"%1\" is currently in-progress.", todo->summary());
0748                     }
0749                 } else if (todo->hasStartDate()) {
0750                     if (todo->dtStart() < now) {
0751                         warnStr = i18n("\"%1\" has already started.", todo->summary());
0752                     }
0753                 }
0754             } else {
0755                 if (todo->hasDueDate()) {
0756                     if (todo->dtDue().date() < today) {
0757                         warnStr = i18n("\"%1\" is past due.", todo->summary());
0758                     } else if (todo->hasStartDate() && todo->dtStart().date() <= today && today <= todo->dtDue().date()) {
0759                         warnStr = i18n("\"%1\", happening all-day today, is currently in-progress.", todo->summary());
0760                     }
0761                 } else if (todo->hasStartDate()) {
0762                     if (todo->dtStart().date() < today) {
0763                         warnStr = i18n("\"%1\", happening all day, has already started.", todo->summary());
0764                     }
0765                 }
0766             }
0767         }
0768 
0769         if (!warnStr.isEmpty()) {
0770             QString queryStr;
0771             KGuiItem yesItem;
0772             KGuiItem noItem;
0773             if (path == QLatin1StringView("accept")) {
0774                 if (type == Incidence::TypeTodo) {
0775                     queryStr = i18n("Do you still want to accept the task?");
0776                 } else {
0777                     queryStr = i18n("Do you still want to accept the invitation?");
0778                 }
0779                 yesItem.setText(i18nc("@action:button", "Accept"));
0780                 yesItem.setIconName(QStringLiteral("dialog-ok"));
0781             } else if (path == QLatin1StringView("accept_conditionally")) {
0782                 if (type == Incidence::TypeTodo) {
0783                     queryStr = i18n("Do you still want to send conditional acceptance of the invitation?");
0784                 } else {
0785                     queryStr = i18n("Do you still want to send conditional acceptance of the task?");
0786                 }
0787                 yesItem.setText(i18nc("@action:button", "Send"));
0788                 yesItem.setIconName(QStringLiteral("mail-send"));
0789             } else if (path == QLatin1StringView("accept_counter")) {
0790                 queryStr = i18n("Do you still want to accept the counter proposal?");
0791                 yesItem.setText(i18nc("@action:button", "Accept"));
0792                 yesItem.setIconName(QStringLiteral("dialog-ok"));
0793             } else if (path == QLatin1StringView("counter")) {
0794                 queryStr = i18n("Do you still want to send a counter proposal?");
0795                 yesItem.setText(i18nc("@action:button", "Send"));
0796                 yesItem.setIconName(QStringLiteral("mail-send"));
0797             } else if (path == QLatin1StringView("decline")) {
0798                 queryStr = i18n("Do you still want to send a decline response?");
0799                 yesItem.setText(i18nc("@action:button", "Send"));
0800                 yesItem.setIconName(QStringLiteral("mail-send"));
0801             } else if (path == QLatin1StringView("decline_counter")) {
0802                 queryStr = i18n("Do you still want to decline the counter proposal?");
0803                 yesItem.setText(i18nc("@action:button", "Decline"));
0804             } else if (path == QLatin1StringView("reply")) {
0805                 queryStr = i18n("Do you still want to record this response in your calendar?");
0806                 yesItem.setText(i18nc("@action:button", "Record"));
0807             } else if (path == QLatin1StringView("delegate")) {
0808                 if (type == Incidence::TypeTodo) {
0809                     queryStr = i18n("Do you still want to delegate this task?");
0810                 } else {
0811                     queryStr = i18n("Do you still want to delegate this invitation?");
0812                 }
0813                 yesItem.setText(i18nc("@action:button", "Delegate"));
0814             } else if (path == QLatin1StringView("forward")) {
0815                 if (type == Incidence::TypeTodo) {
0816                     queryStr = i18n("Do you still want to forward this task?");
0817                 } else {
0818                     queryStr = i18n("Do you still want to forward this invitation?");
0819                 }
0820                 yesItem.setText(i18nc("@action:button", "Forward"));
0821                 yesItem.setIconName(QStringLiteral("mail-forward"));
0822             } else if (path == QLatin1StringView("cancel")) {
0823                 if (type == Incidence::TypeTodo) {
0824                     queryStr = i18n("Do you still want to cancel this task?");
0825                     yesItem.setText(i18nc("@action:button", "Cancel Task"));
0826                 } else {
0827                     queryStr = i18n("Do you still want to cancel this invitation?");
0828                     yesItem.setText(i18nc("@action:button", "Cancel Invitation"));
0829                 }
0830                 yesItem.setIconName(QStringLiteral("dialog-ok"));
0831                 noItem.setText(i18nc("@action:button", "Do Not Cancel"));
0832                 noItem.setIconName(QStringLiteral("dialog-cancel"));
0833             } else if (path == QLatin1StringView("check_calendar")) {
0834                 queryStr = i18n("Do you still want to check your calendar?");
0835                 yesItem.setText(i18nc("@action:button", "Check"));
0836             } else if (path == QLatin1StringView("record")) {
0837                 if (type == Incidence::TypeTodo) {
0838                     queryStr = i18n("Do you still want to record this task in your calendar?");
0839                 } else {
0840                     queryStr = i18n("Do you still want to record this invitation in your calendar?");
0841                 }
0842                 yesItem.setText(i18nc("@action:button", "Record"));
0843             } else if (path == QLatin1StringView("cancel")) {
0844                 if (type == Incidence::TypeTodo) {
0845                     queryStr = i18n("Do you really want to cancel this task?");
0846                     yesItem.setText(i18nc("@action:button", "Cancel Task"));
0847                 } else {
0848                     queryStr = i18n("Do you really want to cancel this invitation?");
0849                     yesItem.setText(i18nc("@action:button", "Cancel Invitation"));
0850                 }
0851                 yesItem.setIconName(QStringLiteral("dialog-ok"));
0852                 noItem.setText(i18nc("@action:button", "Do Not Cancel"));
0853                 noItem.setIconName(QStringLiteral("dialog-cancel"));
0854             } else if (path.startsWith(QLatin1StringView("ATTACH:"))) {
0855                 return false;
0856             } else {
0857                 queryStr = i18n("%1?", path);
0858                 yesItem = KStandardGuiItem::ok();
0859             }
0860 
0861             if (noItem.text().isEmpty()) {
0862                 noItem = KStandardGuiItem::cancel();
0863             }
0864             const int answer = KMessageBox::warningTwoActions(nullptr, i18n("%1\n%2", warnStr, queryStr), QString(), yesItem, noItem);
0865             if (answer == KMessageBox::ButtonCode::SecondaryAction) {
0866                 return true;
0867             }
0868         }
0869         return false;
0870     }
0871 
0872     bool handleInvitation(const QString &iCal, Attendee::PartStat status, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
0873     {
0874         bool ok = true;
0875         const QString receiver = findReceiver(part->content());
0876         qCDebug(TEXT_CALENDAR_LOG) << receiver;
0877 
0878         if (receiver.isEmpty()) {
0879             // Must be some error. Still return true though, since we did handle it
0880             return true;
0881         }
0882 
0883         Incidence::Ptr incidence = stringToIncidence(iCal);
0884         qCDebug(TEXT_CALENDAR_LOG) << "Handling invitation: uid is : " << incidence->uid() << "; schedulingId is:" << incidence->schedulingID()
0885                                    << "; Attendee::PartStat = " << status;
0886 
0887         // get comment for tentative acceptance
0888         if (askForComment(status)) {
0889             QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
0890             dlg->setWindowTitle(i18nc("@title:window", "Reaction to Invitation"));
0891             QString comment;
0892             if (dlg->exec()) {
0893                 comment = dlg->comment();
0894                 delete dlg;
0895             } else {
0896                 delete dlg;
0897                 return true;
0898             }
0899 
0900             if (comment.trimmed().isEmpty()) {
0901                 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks"));
0902                 return true;
0903             } else {
0904                 incidence->addComment(comment);
0905             }
0906         }
0907 
0908         // First, save it for KOrganizer to handle
0909         const QString dir = directoryForStatus(status);
0910         if (dir.isEmpty()) {
0911             qCWarning(TEXT_CALENDAR_LOG) << "Impossible to understand status: " << status;
0912             return true; // unknown status
0913         }
0914         if (status != Attendee::Delegated) {
0915             // we do that below for delegated incidences
0916             if (!saveFile(receiver, iCal, dir, part)) {
0917                 return false;
0918             }
0919         }
0920 
0921         QString delegateString;
0922         bool delegatorRSVP = false;
0923         if (status == Attendee::Delegated) {
0924             DelegateSelector dlg;
0925             if (dlg.exec() == QDialog::Rejected) {
0926                 return true;
0927             }
0928             delegateString = dlg.delegate();
0929             delegatorRSVP = dlg.rsvp();
0930             if (delegateString.isEmpty()) {
0931                 return true;
0932             }
0933             if (KEmailAddress::compareEmail(delegateString, incidence->organizer().email(), false)) {
0934                 KMessageBox::error(nullptr, i18n("Delegation to organizer is not possible."));
0935                 return true;
0936             }
0937         }
0938 
0939         if (!incidence) {
0940             return false;
0941         }
0942 
0943         const Attendee myself = findMyself(incidence, receiver);
0944 
0945         // find our delegator, we need to inform him as well
0946         QString delegator;
0947         if (status != Attendee::NeedsAction && !myself.isNull() && !myself.delegator().isEmpty()) {
0948             const Attendee::List attendees = incidence->attendees();
0949             Attendee::List::ConstIterator end = attendees.constEnd();
0950             for (Attendee::List::ConstIterator it = attendees.constBegin(); it != end; ++it) {
0951                 if (KEmailAddress::compareEmail((*it).fullName(), myself.delegator(), false) && (*it).status() == Attendee::Delegated) {
0952                     delegator = (*it).fullName();
0953                     delegatorRSVP = (*it).RSVP();
0954                     break;
0955                 }
0956             }
0957         }
0958 
0959         if (status != Attendee::NeedsAction
0960             && ((!myself.isNull() && (myself.RSVP() || myself.status() == Attendee::NeedsAction)) || heuristicalRSVP(incidence))) {
0961             Attendee newMyself = setStatusOnMyself(incidence, myself, status, receiver);
0962             if (!newMyself.isNull() && status == Attendee::Delegated) {
0963                 newMyself.setDelegate(delegateString);
0964                 newMyself.setRSVP(delegatorRSVP);
0965             }
0966             ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
0967 
0968             // check if we need to inform our delegator about this as well
0969             if (!newMyself.isNull() && (status == Attendee::Accepted || status == Attendee::Declined) && !delegator.isEmpty()) {
0970                 if (delegatorRSVP || status == Attendee::Declined) {
0971                     ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver, delegator);
0972                 }
0973             }
0974         } else if (myself.isNull() && (status != Attendee::Declined && status != Attendee::NeedsAction)) {
0975             // forwarded invitation
0976             QString name;
0977             QString email;
0978             KEmailAddress::extractEmailAddressAndName(receiver, email, name);
0979             if (!email.isEmpty()) {
0980                 Attendee newMyself(name,
0981                                    email,
0982                                    true, // RSVP, otherwise we would not be here
0983                                    status,
0984                                    heuristicalRole(incidence),
0985                                    QString());
0986                 incidence->clearAttendees();
0987                 incidence->addAttendee(newMyself);
0988                 ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
0989             }
0990         } else {
0991             if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
0992                 viewerInstance->deleteMessage();
0993             }
0994         }
0995 
0996         // create invitation for the delegate (same as the original invitation
0997         // with the delegate as additional attendee), we also use that for updating
0998         // our calendar
0999         if (status == Attendee::Delegated) {
1000             incidence = stringToIncidence(iCal);
1001             auto attendees = incidence->attendees();
1002             const int myselfIdx = findMyself(attendees, receiver);
1003             if (myselfIdx >= 0) {
1004                 attendees[myselfIdx].setStatus(status);
1005                 attendees[myselfIdx].setDelegate(delegateString);
1006                 incidence->setAttendees(attendees);
1007             }
1008             QString name;
1009             QString email;
1010             KEmailAddress::extractEmailAddressAndName(delegateString, email, name);
1011             Attendee delegate(name, email, true);
1012             delegate.setDelegator(receiver);
1013             incidence->addAttendee(delegate);
1014 
1015             ICalFormat format;
1016             format.setTimeZone(QTimeZone::systemTimeZone());
1017             const QString iCal = format.createScheduleMessage(incidence, iTIPRequest);
1018             if (!saveFile(receiver, iCal, dir, part)) {
1019                 return false;
1020             }
1021 
1022             ok = mail(viewerInstance, incidence, dir, iTIPRequest, receiver, delegateString, Delegation);
1023         }
1024         return ok;
1025     }
1026 
1027     void openAttachment(const QString &name, const QString &iCal) const
1028     {
1029         Attachment attachment(findAttachment(name, iCal));
1030         if (attachment.isEmpty()) {
1031             return;
1032         }
1033 
1034         if (attachment.isUri()) {
1035             QDesktopServices::openUrl(QUrl(attachment.uri()));
1036         } else {
1037             // put the attachment in a temporary file and launch it
1038             QTemporaryFile *file = nullptr;
1039             QMimeDatabase db;
1040             QStringList patterns = db.mimeTypeForName(attachment.mimeType()).globPatterns();
1041             if (!patterns.empty()) {
1042                 QString pattern = patterns.at(0);
1043                 file = new QTemporaryFile(QDir::tempPath() + QLatin1StringView("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
1044             } else {
1045                 file = new QTemporaryFile();
1046             }
1047             file->setAutoRemove(false);
1048             file->open();
1049             file->setPermissions(QFile::ReadUser);
1050             file->write(QByteArray::fromBase64(attachment.data()));
1051             file->close();
1052 
1053             auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file->fileName()), attachment.mimeType());
1054             job->setDeleteTemporaryFile(true);
1055             job->start();
1056             delete file;
1057         }
1058     }
1059 
1060     [[nodiscard]] bool saveAsAttachment(const QString &name, const QString &iCal) const
1061     {
1062         Attachment a(findAttachment(name, iCal));
1063         if (a.isEmpty()) {
1064             return false;
1065         }
1066 
1067         // get the saveas file name
1068         const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18n("Save Invitation Attachment"), name, QString());
1069 
1070         if (saveAsFile.isEmpty()) {
1071             return false;
1072         }
1073 
1074         bool stat = false;
1075         if (a.isUri()) {
1076             // save the attachment url
1077             auto job = KIO::file_copy(QUrl(a.uri()), QUrl::fromLocalFile(saveAsFile));
1078             stat = job->exec();
1079         } else {
1080             // put the attachment in a temporary file and save it
1081             QTemporaryFile *file{nullptr};
1082             QMimeDatabase db;
1083             QStringList patterns = db.mimeTypeForName(a.mimeType()).globPatterns();
1084             if (!patterns.empty()) {
1085                 QString pattern = patterns.at(0);
1086                 file = new QTemporaryFile(QDir::tempPath() + QLatin1StringView("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
1087             } else {
1088                 file = new QTemporaryFile();
1089             }
1090             file->setAutoRemove(false);
1091             file->open();
1092             file->setPermissions(QFile::ReadUser);
1093             file->write(QByteArray::fromBase64(a.data()));
1094             file->close();
1095             const QString filename = file->fileName();
1096             delete file;
1097 
1098             auto job = KIO::file_copy(QUrl::fromLocalFile(filename), QUrl::fromLocalFile(saveAsFile));
1099             stat = job->exec();
1100         }
1101         return stat;
1102     }
1103 
1104     void showCalendar(QDate date) const
1105     {
1106         // Start or activate KOrganizer. When Kontact is running it will switch to KOrganizer view
1107         const auto korgaService = KService::serviceByDesktopName(QStringLiteral("org.kde.korganizer"));
1108 
1109         if (!korgaService) {
1110             qCWarning(TEXT_CALENDAR_LOG) << "Could not find KOrganizer";
1111             return;
1112         }
1113 
1114         auto job = new KIO::ApplicationLauncherJob(korgaService);
1115         QObject::connect(job, &KJob::finished, job, [date](KJob *job) {
1116             if (job->error()) {
1117                 qCWarning(TEXT_CALENDAR_LOG) << "failed to run korganizer" << job->errorString();
1118                 return;
1119             }
1120 
1121             OrgKdeKorganizerCalendarInterface iface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QDBusConnection::sessionBus(), nullptr);
1122             if (!iface.isValid()) {
1123                 qCDebug(TEXT_CALENDAR_LOG) << "Calendar interface is not valid! " << iface.lastError().message();
1124                 return;
1125             }
1126             iface.showEventView();
1127             iface.showDate(date);
1128         });
1129 
1130         job->start();
1131     }
1132 
1133     bool handleIgnore(Viewer *viewerInstance) const
1134     {
1135         // simply move the message to trash
1136         viewerInstance->deleteMessage();
1137         return true;
1138     }
1139 
1140     bool handleDeclineCounter(const QString &iCal, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
1141     {
1142         const QString receiver(findReceiver(part->content()));
1143         if (receiver.isEmpty()) {
1144             return true;
1145         }
1146         Incidence::Ptr incidence(stringToIncidence(iCal));
1147         if (askForComment(Attendee::Declined)) {
1148             QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
1149             dlg->setWindowTitle(i18nc("@title:window", "Decline Counter Proposal"));
1150             QString comment;
1151             if (dlg->exec()) {
1152                 comment = dlg->comment();
1153                 delete dlg;
1154             } else {
1155                 delete dlg;
1156                 return true;
1157             }
1158 
1159             if (comment.trimmed().isEmpty()) {
1160                 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks"));
1161                 return true;
1162             } else {
1163                 incidence->addComment(comment);
1164             }
1165         }
1166         return mail(viewerInstance, incidence, QStringLiteral("declinecounter"), KCalendarCore::iTIPDeclineCounter, receiver, QString(), DeclineCounter);
1167     }
1168 
1169     bool counterProposal(const QString &iCal, MimeTreeParser::Interface::BodyPart *part) const
1170     {
1171         const QString receiver = findReceiver(part->content());
1172         if (receiver.isEmpty()) {
1173             return true;
1174         }
1175 
1176         // Don't delete the invitation here in any case, if the counter proposal
1177         // is declined you might need it again.
1178         return saveFile(receiver, iCal, QStringLiteral("counter"), part);
1179     }
1180 
1181     bool handleClick(Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const override
1182     {
1183         // filter out known paths that don't belong to this type of urlmanager.
1184         // kolab/issue4054 msg27201
1185         if (path.contains(QLatin1StringView("addToAddressBook:")) || path.contains(QLatin1StringView("updateToAddressBook"))) {
1186             return false;
1187         }
1188 
1189         if (!hasMyWritableEventsFolders(QStringLiteral("calendar"))) {
1190             KMessageBox::error(nullptr,
1191                                i18n("You have no writable calendar folders for invitations, "
1192                                     "so storing or saving a response will not be possible.\n"
1193                                     "Please create at least 1 writable events calendar and re-sync."));
1194             return false;
1195         }
1196 
1197         // If the bodypart does not have a charset specified, we need to fall back to utf8,
1198         // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
1199         QString iCal;
1200         if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) {
1201             const QByteArray &ba = part->content()->decodedContent();
1202             iCal = QString::fromUtf8(ba);
1203         } else {
1204             iCal = part->content()->decodedText();
1205         }
1206 
1207         Incidence::Ptr incidence = stringToIncidence(iCal);
1208         if (!incidence) {
1209             KMessageBox::error(nullptr,
1210                                i18n("The calendar invitation stored in this email message is broken in some way. "
1211                                     "Unable to continue."));
1212             return false;
1213         }
1214 
1215         bool result = false;
1216         if (cancelPastInvites(incidence, path)) {
1217             return result;
1218         }
1219 
1220         if (path == QLatin1StringView("accept")) {
1221             result = handleInvitation(iCal, Attendee::Accepted, part, viewerInstance);
1222         } else if (path == QLatin1StringView("accept_conditionally")) {
1223             result = handleInvitation(iCal, Attendee::Tentative, part, viewerInstance);
1224         } else if (path == QLatin1StringView("counter")) {
1225             result = counterProposal(iCal, part);
1226         } else if (path == QLatin1StringView("ignore")) {
1227             result = handleIgnore(viewerInstance);
1228         } else if (path == QLatin1StringView("decline")) {
1229             result = handleInvitation(iCal, Attendee::Declined, part, viewerInstance);
1230         } else if (path == QLatin1StringView("decline_counter")) {
1231             result = handleDeclineCounter(iCal, part, viewerInstance);
1232         } else if (path == QLatin1StringView("postpone")) {
1233             result = handleInvitation(iCal, Attendee::NeedsAction, part, viewerInstance);
1234         } else if (path == QLatin1StringView("delegate")) {
1235             result = handleInvitation(iCal, Attendee::Delegated, part, viewerInstance);
1236         } else if (path == QLatin1StringView("forward")) {
1237             AttendeeSelector dlg;
1238             if (dlg.exec() == QDialog::Rejected) {
1239                 return true;
1240             }
1241             QString fwdTo = dlg.attendees().join(QLatin1StringView(", "));
1242             if (fwdTo.isEmpty()) {
1243                 return true;
1244             }
1245             const QString receiver = findReceiver(part->content());
1246             result = mail(viewerInstance, incidence, QStringLiteral("forward"), iTIPRequest, receiver, fwdTo, Forward);
1247         } else if (path == QLatin1StringView("check_calendar")) {
1248             incidence = stringToIncidence(iCal);
1249             showCalendar(incidence->dtStart().date());
1250             return true;
1251         } else if (path == QLatin1StringView("reply") || path == QLatin1StringView("cancel") || path == QLatin1StringView("accept_counter")) {
1252             // These should just be saved with their type as the dir
1253             const QString p = (path == QLatin1StringView("accept_counter") ? QStringLiteral("reply") : path);
1254             if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, p, part)) {
1255                 if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
1256                     viewerInstance->deleteMessage();
1257                 }
1258                 result = true;
1259             }
1260         } else if (path == QLatin1StringView("record")) {
1261             incidence = stringToIncidence(iCal);
1262             QString summary;
1263             int response = KMessageBox::questionTwoActionsCancel(nullptr,
1264                                                                  i18nc("@info",
1265                                                                        "The organizer is not expecting a reply to this invitation "
1266                                                                        "but you can send them an email message if you desire.\n\n"
1267                                                                        "Would you like to send the organizer a message regarding this invitation?\n"
1268                                                                        "Press the [Cancel] button to cancel the recording operation."),
1269                                                                  i18nc("@title:window", "Send Email to Organizer"),
1270                                                                  KGuiItem(i18n("Do Not Send")),
1271                                                                  KGuiItem(i18n("Send EMail")));
1272 
1273             switch (response) {
1274             case KMessageBox::Cancel:
1275                 break;
1276             case KMessageBox::ButtonCode::SecondaryAction: { // means "send email"
1277                 summary = incidence->summary();
1278                 if (!summary.isEmpty()) {
1279                     summary = i18n("Re: %1", summary);
1280                 }
1281 
1282                 QUrlQuery query;
1283                 query.addQueryItem(QStringLiteral("to"), incidence->organizer().email());
1284                 query.addQueryItem(QStringLiteral("subject"), summary);
1285                 QUrl url;
1286                 url.setScheme(QStringLiteral("mailto"));
1287                 url.setQuery(query);
1288                 QDesktopServices::openUrl(url);
1289             }
1290             // fall through
1291             case KMessageBox::ButtonCode::PrimaryAction: // means "do not send"
1292                 if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, QStringLiteral("reply"), part)) {
1293                     if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
1294                         viewerInstance->deleteMessage();
1295                         result = true;
1296                     }
1297                 }
1298                 showCalendar(incidence->dtStart().date());
1299                 break;
1300             }
1301         } else if (path == QLatin1StringView("delete")) {
1302             viewerInstance->deleteMessage();
1303             result = true;
1304         }
1305 
1306         if (path.startsWith(QLatin1StringView("ATTACH:"))) {
1307             const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1308             openAttachment(name, iCal);
1309         }
1310 
1311         if (result) {
1312             // do not close the secondary window if an attachment was opened (kolab/issue4317)
1313             if (!path.startsWith(QLatin1StringView("ATTACH:"))) {
1314                 qCDebug(TEXT_CALENDAR_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO << "about closing if in a secondary window";
1315 #if 0 // TODO port to Akonadi
1316                 c.closeIfSecondaryWindow();
1317 #endif
1318             }
1319         }
1320         return result;
1321     }
1322 
1323     bool handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &point) const override
1324     {
1325         QString name = path;
1326         if (path.startsWith(QLatin1StringView("ATTACH:"))) {
1327             name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1328         } else {
1329             return false; // because it isn't an attachment invitation
1330         }
1331 
1332         QString iCal;
1333         if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) {
1334             const QByteArray &ba = part->content()->decodedContent();
1335             iCal = QString::fromUtf8(ba);
1336         } else {
1337             iCal = part->content()->decodedText();
1338         }
1339 
1340         auto menu = new QMenu();
1341         QAction *open = menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Attachment"));
1342         QAction *saveas = menu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Attachment As..."));
1343 
1344         QAction *a = menu->exec(point, nullptr);
1345         if (a == open) {
1346             openAttachment(name, iCal);
1347         } else if (a == saveas) {
1348             saveAsAttachment(name, iCal);
1349         }
1350         delete menu;
1351         return true;
1352     }
1353 
1354     QString statusBarMessage(MimeTreeParser::Interface::BodyPart *, const QString &path) const override
1355     {
1356         if (!path.isEmpty()) {
1357             if (path == QLatin1StringView("accept")) {
1358                 return i18n("Accept invitation");
1359             } else if (path == QLatin1StringView("accept_conditionally")) {
1360                 return i18n("Accept invitation conditionally");
1361             } else if (path == QLatin1StringView("accept_counter")) {
1362                 return i18n("Accept counter proposal");
1363             } else if (path == QLatin1StringView("counter")) {
1364                 return i18n("Create a counter proposal...");
1365             } else if (path == QLatin1StringView("ignore")) {
1366                 return i18n("Throw mail away");
1367             } else if (path == QLatin1StringView("decline")) {
1368                 return i18n("Decline invitation");
1369             } else if (path == QLatin1StringView("postpone")) {
1370                 return i18n("Postpone");
1371             } else if (path == QLatin1StringView("decline_counter")) {
1372                 return i18n("Decline counter proposal");
1373             } else if (path == QLatin1StringView("check_calendar")) {
1374                 return i18n("Check my calendar...");
1375             } else if (path == QLatin1StringView("reply")) {
1376                 return i18n("Record response into my calendar");
1377             } else if (path == QLatin1StringView("record")) {
1378                 return i18n("Record invitation into my calendar");
1379             } else if (path == QLatin1StringView("delete")) {
1380                 return i18n("Move this invitation to my trash folder");
1381             } else if (path == QLatin1StringView("delegate")) {
1382                 return i18n("Delegate invitation");
1383             } else if (path == QLatin1StringView("forward")) {
1384                 return i18n("Forward invitation");
1385             } else if (path == QLatin1StringView("cancel")) {
1386                 return i18n("Remove invitation from my calendar");
1387             } else if (path.startsWith(QLatin1StringView("ATTACH:"))) {
1388                 const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1389                 return i18n("Open attachment \"%1\"", name);
1390             }
1391         }
1392 
1393         return {};
1394     }
1395 
1396     [[nodiscard]] bool askForComment(Attendee::PartStat status) const
1397     {
1398         if (status != Attendee::NeedsAction
1399             && ((status != Attendee::Accepted
1400                  && MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation()
1401                      == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AskForAllButAcceptance)
1402                 || (MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation()
1403                     == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AlwaysAsk))) {
1404             return true;
1405         }
1406         return false;
1407     }
1408 };
1409 
1410 class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin
1411 {
1412     Q_OBJECT
1413     Q_INTERFACES(MessageViewer::MessagePartRenderPlugin)
1414     Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "text_calendar.json")
1415 public:
1416     MessageViewer::MessagePartRendererBase *renderer(int idx) override
1417     {
1418         if (idx < 2) {
1419             return new Formatter();
1420         } else {
1421             return nullptr;
1422         }
1423     }
1424 
1425     [[nodiscard]] const MessageViewer::Interface::BodyPartURLHandler *urlHandler(int idx) const override
1426     {
1427         if (idx == 0) {
1428             return new UrlHandler();
1429         } else {
1430             return nullptr;
1431         }
1432     }
1433 };
1434 }
1435 
1436 #include "text_calendar.moc"