File indexing completed on 2024-05-12 05:10:47

0001 /*
0002   SPDX-FileCopyrightText: 2002-2004 Klarälvdalens Datakonsult AB,
0003         <info@klaralvdalens-datakonsult.se>
0004 
0005   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
0006   SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0007 
0008   SPDX-FileCopyrightText: 2012 SĂ©rgio Martins <iamsergio@gmail.com>
0009 
0010   SPDX-License-Identifier: LGPL-2.0-or-later
0011 */
0012 
0013 #include "itiphandler.h"
0014 #include "calendarsettings.h"
0015 #include "itiphandler_p.h"
0016 #include "itiphandlerhelper_p.h"
0017 #include "mailclient_p.h"
0018 #include "publishdialog.h"
0019 #include "utils_p.h"
0020 
0021 #include <KCalUtils/Stringify>
0022 #include <KCalendarCore/Attendee>
0023 #include <KCalendarCore/ICalFormat>
0024 
0025 #include <Akonadi/MessageQueueJob>
0026 #include <KIdentityManagementCore/IdentityManager>
0027 
0028 #include "akonadicalendar_debug.h"
0029 #include <KMessageBox>
0030 
0031 using namespace Akonadi;
0032 
0033 // async emittion
0034 static void emitiTipMessageProcessed(ITIPHandler *handler, ITIPHandler::Result resultCode, const QString &errorString)
0035 {
0036     QMetaObject::invokeMethod(handler,
0037                               "iTipMessageProcessed",
0038                               Qt::QueuedConnection,
0039                               Q_ARG(Akonadi::ITIPHandler::Result, resultCode),
0040                               Q_ARG(QString, errorString));
0041 }
0042 
0043 GroupwareUiDelegate::GroupwareUiDelegate(QObject *parent)
0044     : QObject(parent)
0045 {
0046 }
0047 
0048 GroupwareUiDelegate::~GroupwareUiDelegate() = default;
0049 
0050 ITIPHandlerComponentFactory::ITIPHandlerComponentFactory(QObject *parent)
0051     : QObject(parent)
0052 {
0053 }
0054 
0055 ITIPHandlerComponentFactory::~ITIPHandlerComponentFactory() = default;
0056 
0057 Akonadi::MessageQueueJob *ITIPHandlerComponentFactory::createMessageQueueJob(const KCalendarCore::IncidenceBase::Ptr &incidence,
0058                                                                              const KIdentityManagementCore::Identity &identity,
0059                                                                              QObject *parent)
0060 {
0061     Q_UNUSED(incidence)
0062     Q_UNUSED(identity)
0063     return new Akonadi::MessageQueueJob(parent);
0064 }
0065 
0066 ITIPHandlerDialogDelegate *
0067 ITIPHandlerComponentFactory::createITIPHanderDialogDelegate(const KCalendarCore::Incidence::Ptr &incidence, KCalendarCore::iTIPMethod method, QWidget *parent)
0068 {
0069     return new ITIPHandlerDialogDelegate(incidence, method, parent);
0070 }
0071 
0072 ITIPHandler::ITIPHandler(QObject *parent)
0073     : QObject(parent)
0074     , d(new ITIPHandlerPrivate(/*factory=*/nullptr, this))
0075 {
0076     qRegisterMetaType<Akonadi::ITIPHandler::Result>("Akonadi::ITIPHandler::Result");
0077 }
0078 
0079 ITIPHandler::ITIPHandler(ITIPHandlerComponentFactory *factory, QObject *parent)
0080     : QObject(parent)
0081     , d(new ITIPHandlerPrivate(factory, this))
0082 {
0083     qRegisterMetaType<Akonadi::ITIPHandler::Result>("Akonadi::ITIPHandler::Result");
0084 }
0085 
0086 ITIPHandler::~ITIPHandler() = default;
0087 
0088 void ITIPHandler::processiTIPMessage(const QString &receiver, const QString &iCal, const QString &action)
0089 {
0090     qCDebug(AKONADICALENDAR_LOG) << "processiTIPMessage called with receiver=" << receiver << "; action=" << action;
0091 
0092     if (d->m_currentOperation != OperationNone) {
0093         d->m_currentOperation = OperationNone;
0094         qCritical() << "There can't be an operation in progress!" << d->m_currentOperation;
0095         return;
0096     }
0097 
0098     d->m_currentOperation = OperationProcessiTIPMessage;
0099 
0100     if (!d->isLoaded()) {
0101         d->m_queuedInvitation.receiver = receiver;
0102         d->m_queuedInvitation.iCal = iCal;
0103         d->m_queuedInvitation.action = action;
0104         return;
0105     }
0106 
0107     if (d->m_calendarLoadError) {
0108         d->m_currentOperation = OperationNone;
0109         qCritical() << "Error loading calendar";
0110         emitiTipMessageProcessed(this, ResultError, i18n("Error loading calendar."));
0111         return;
0112     }
0113 
0114     KCalendarCore::ICalFormat format;
0115     KCalendarCore::ScheduleMessage::Ptr message = format.parseScheduleMessage(d->calendar(), iCal);
0116 
0117     if (!message) {
0118         const QString errorMessage = format.exception() ? i18n("Error message: %1", KCalUtils::Stringify::errorMessage(*format.exception()))
0119                                                         : i18n("Unknown error while parsing iCal invitation");
0120 
0121         qCritical() << "Error parsing" << errorMessage;
0122 
0123         if (d->m_showDialogsOnError) {
0124             KMessageBox::detailedError(nullptr, // mParent, TODO
0125                                        i18n("Error while processing an invitation or update."),
0126                                        errorMessage);
0127         }
0128 
0129         d->m_currentOperation = OperationNone;
0130         emitiTipMessageProcessed(this, ResultError, errorMessage);
0131 
0132         return;
0133     }
0134 
0135     d->m_method = static_cast<KCalendarCore::iTIPMethod>(message->method());
0136 
0137     KCalendarCore::ScheduleMessage::Status status = message->status();
0138     d->m_incidence = message->event().dynamicCast<KCalendarCore::Incidence>();
0139     if (!d->m_incidence) {
0140         qCritical() << "Invalid incidence";
0141         d->m_currentOperation = OperationNone;
0142         emitiTipMessageProcessed(this, ResultError, i18n("Invalid incidence"));
0143         return;
0144     }
0145 
0146     if (action.startsWith(QLatin1StringView("accepted")) || action.startsWith(QLatin1StringView("tentative"))
0147         || action.startsWith(QLatin1StringView("delegated")) || action.startsWith(QLatin1StringView("counter"))) {
0148         // Find myself and set my status. This can't be done in the scheduler,
0149         // since this does not know the choice I made in the KMail bpf
0150         KCalendarCore::Attendee::List attendees = d->m_incidence->attendees();
0151         for (auto &attendee : attendees) {
0152             if (attendee.email() == receiver) {
0153                 if (action.startsWith(QLatin1StringView("accepted"))) {
0154                     attendee.setStatus(KCalendarCore::Attendee::Accepted);
0155                 } else if (action.startsWith(QLatin1StringView("tentative"))) {
0156                     attendee.setStatus(KCalendarCore::Attendee::Tentative);
0157                 } else if (CalendarSettings::self()->outlookCompatCounterProposals() && action.startsWith(QLatin1StringView("counter"))) {
0158                     attendee.setStatus(KCalendarCore::Attendee::Tentative);
0159                 } else if (action.startsWith(QLatin1StringView("delegated"))) {
0160                     attendee.setStatus(KCalendarCore::Attendee::Delegated);
0161                 }
0162                 break;
0163             }
0164         }
0165         d->m_incidence->setAttendees(attendees);
0166 
0167         if (CalendarSettings::self()->outlookCompatCounterProposals() || !action.startsWith(QLatin1StringView("counter"))) {
0168             d->m_scheduler->acceptTransaction(d->m_incidence, d->calendar(), d->m_method, status, receiver);
0169             return; // signal emitted in onSchedulerFinished().
0170         }
0171         // TODO: what happens here? we must emit a signal
0172     } else if (action.startsWith(QLatin1StringView("cancel"))) {
0173         // Delete the old incidence, if one is present
0174         KCalendarCore::Incidence::Ptr existingIncidence = d->calendar()->incidenceFromSchedulingID(d->m_incidence->uid());
0175         if (existingIncidence) {
0176             d->m_scheduler->acceptTransaction(d->m_incidence, d->calendar(), KCalendarCore::iTIPCancel, status, receiver);
0177             return; // signal emitted in onSchedulerFinished().
0178         } else {
0179             // We don't have the incidence, nothing to cancel
0180             qCWarning(AKONADICALENDAR_LOG) << "Couldn't find the incidence to delete.\n"
0181                                            << "You deleted it previously or didn't even accept the invitation it in the first place.\n"
0182                                            << "; uid=" << d->m_incidence->uid() << "; identifier=" << d->m_incidence->instanceIdentifier()
0183                                            << "; summary=" << d->m_incidence->summary();
0184 
0185             qCDebug(AKONADICALENDAR_LOG) << "\n Here's what we do have with such a summary:";
0186             const KCalendarCore::Incidence::List knownIncidences = calendar()->incidences();
0187             for (const KCalendarCore::Incidence::Ptr &knownIncidence : knownIncidences) {
0188                 if (knownIncidence->summary() == d->m_incidence->summary()) {
0189                     qCDebug(AKONADICALENDAR_LOG) << "\nFound: uid=" << knownIncidence->uid() << "; identifier=" << knownIncidence->instanceIdentifier()
0190                                                  << "; schedulingId" << knownIncidence->schedulingID();
0191                 }
0192             }
0193 
0194             emitiTipMessageProcessed(this, ResultSuccess, QString());
0195         }
0196     } else if (action.startsWith(QLatin1StringView("reply"))) {
0197         if (d->m_method != KCalendarCore::iTIPCounter) {
0198             d->m_scheduler->acceptTransaction(d->m_incidence, d->calendar(), d->m_method, status, QString());
0199         } else {
0200             d->m_scheduler->acceptCounterProposal(d->m_incidence, d->calendar());
0201         }
0202         return; // signal emitted in onSchedulerFinished().
0203     } else if (action.startsWith(QLatin1StringView("request"))) {
0204         d->m_scheduler->acceptTransaction(d->m_incidence, d->calendar(), d->m_method, status, receiver);
0205         return;
0206     } else {
0207         qCritical() << "Unknown incoming action" << action;
0208 
0209         d->m_currentOperation = OperationNone;
0210         emitiTipMessageProcessed(this, ResultError, i18n("Invalid action: %1", action));
0211     }
0212 
0213     if (action.startsWith(QLatin1StringView("counter"))) {
0214         if (d->m_uiDelegate) {
0215             Akonadi::Item item;
0216             item.setMimeType(d->m_incidence->mimeType());
0217             item.setPayload(KCalendarCore::Incidence::Ptr(d->m_incidence->clone()));
0218 
0219             // TODO_KDE5: This blocks because m_uiDelegate is not a QObject and can't emit a finished()
0220             // signal. Make async in kde5
0221             d->m_uiDelegate->requestIncidenceEditor(item);
0222             KCalendarCore::Incidence::Ptr newIncidence;
0223             if (item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0224                 newIncidence = item.payload<KCalendarCore::Incidence::Ptr>();
0225             }
0226 
0227             if (*newIncidence == *d->m_incidence) {
0228                 emitiTipMessageProcessed(this, ResultCancelled, QString());
0229             } else {
0230                 ITIPHandlerHelper::SendResult result = d->m_helper->sendCounterProposal(receiver, d->m_incidence, newIncidence);
0231                 if (result != ITIPHandlerHelper::ResultSuccess) {
0232                     // It gives success in all paths, this never happens
0233                     emitiTipMessageProcessed(this, ResultError, i18n("Error sending counter proposal"));
0234                     Q_ASSERT(false);
0235                 }
0236             }
0237         } else {
0238             // This should never happen
0239             qCWarning(AKONADICALENDAR_LOG) << "No UI delegate is set";
0240             emitiTipMessageProcessed(this, ResultError, i18n("Could not start editor to edit counter proposal"));
0241         }
0242     }
0243 }
0244 
0245 void ITIPHandler::sendiTIPMessage(KCalendarCore::iTIPMethod method, const KCalendarCore::Incidence::Ptr &incidence, QWidget *parentWidget)
0246 {
0247     if (!incidence) {
0248         Q_ASSERT(false);
0249         qCritical() << "Invalid incidence";
0250         return;
0251     }
0252 
0253     d->m_queuedInvitation.method = method;
0254     d->m_queuedInvitation.incidence = incidence;
0255     d->m_parentWidget = parentWidget;
0256 
0257     if (!d->isLoaded()) {
0258         // This method will be called again once the calendar is loaded.
0259         return;
0260     }
0261 
0262     Q_ASSERT(d->m_currentOperation == OperationNone);
0263     if (d->m_currentOperation != OperationNone) {
0264         qCritical() << "There can't be an operation in progress!" << d->m_currentOperation;
0265         return;
0266     }
0267 
0268     if (incidence->attendeeCount() == 0 && method != KCalendarCore::iTIPPublish) {
0269         if (d->m_showDialogsOnError) {
0270             KMessageBox::information(parentWidget,
0271                                      i18n("The item '%1' has no attendees. "
0272                                           "Therefore no groupware message will be sent.",
0273                                           incidence->summary()),
0274                                      i18nc("@title:window", "Message Not Sent"),
0275                                      QStringLiteral("ScheduleNoAttendees"));
0276         }
0277 
0278         return;
0279     }
0280 
0281     d->m_currentOperation = OperationSendiTIPMessage;
0282 
0283     KCalendarCore::Incidence *incidenceCopy = incidence->clone();
0284     incidenceCopy->registerObserver(nullptr);
0285     incidenceCopy->clearAttendees();
0286 
0287     d->m_scheduler->performTransaction(incidence, method);
0288 }
0289 
0290 void ITIPHandler::publishInformation(const KCalendarCore::Incidence::Ptr &incidence, QWidget *parentWidget)
0291 {
0292     Q_ASSERT(incidence);
0293     if (!incidence) {
0294         qCritical() << "Invalid incidence. Aborting.";
0295         return;
0296     }
0297 
0298     Q_ASSERT(d->m_currentOperation == OperationNone);
0299     if (d->m_currentOperation != OperationNone) {
0300         qCritical() << "There can't be an operation in progress!" << d->m_currentOperation;
0301         return;
0302     }
0303 
0304     d->m_queuedInvitation.incidence = incidence;
0305     d->m_parentWidget = parentWidget;
0306 
0307     d->m_currentOperation = OperationPublishInformation;
0308 
0309     QPointer<Akonadi::PublishDialog> publishdlg = new Akonadi::PublishDialog();
0310     if (incidence->attendeeCount() > 0) {
0311         KCalendarCore::Attendee::List attendees = incidence->attendees();
0312         KCalendarCore::Attendee::List::ConstIterator it;
0313         KCalendarCore::Attendee::List::ConstIterator end(attendees.constEnd());
0314         for (it = attendees.constBegin(); it != end; ++it) {
0315             publishdlg->addAttendee(*it);
0316         }
0317     }
0318     if (publishdlg->exec() == QDialog::Accepted && publishdlg) {
0319         d->m_scheduler->publish(incidence, publishdlg->addresses());
0320     } else {
0321         d->m_currentOperation = OperationNone;
0322         Q_EMIT informationPublished(ResultSuccess, QString()); // Canceled.
0323     }
0324     delete publishdlg;
0325 }
0326 
0327 void ITIPHandler::sendAsICalendar(const KCalendarCore::Incidence::Ptr &originalIncidence, QWidget *parentWidget)
0328 {
0329     Q_UNUSED(parentWidget)
0330     Q_ASSERT(originalIncidence);
0331     if (!originalIncidence) {
0332         qCritical() << "Invalid incidence";
0333         return;
0334     }
0335 
0336     // Clone so we can change organizer and recurid
0337     KCalendarCore::Incidence::Ptr incidence = KCalendarCore::Incidence::Ptr(originalIncidence->clone());
0338 
0339     QPointer<Akonadi::PublishDialog> publishdlg = new Akonadi::PublishDialog;
0340     if (publishdlg->exec() == QDialog::Accepted && publishdlg) {
0341         const QString recipients = publishdlg->addresses();
0342         if (incidence->organizer().isEmpty()) {
0343             incidence->setOrganizer(KCalendarCore::Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
0344         }
0345 
0346         if (incidence->hasRecurrenceId()) {
0347             // For an individual occurrence, recur id doesn't make sense, since we're not sending the whole recurrence series.
0348             incidence->setRecurrenceId({});
0349         }
0350 
0351         KCalendarCore::ICalFormat format;
0352         const QString from = Akonadi::CalendarUtils::email();
0353         const bool bccMe = Akonadi::CalendarSettings::self()->bcc();
0354         const QString messageText = format.createScheduleMessage(incidence, KCalendarCore::iTIPRequest);
0355         auto mailer = new MailClient(d->m_factory);
0356         d->m_queuedInvitation.incidence = incidence;
0357         connect(mailer, &MailClient::finished, d.get(), [this](Akonadi::MailClient::Result result, const QString &str) {
0358             d->finishSendAsICalendar(result, str);
0359         });
0360 
0361         mailer->mailTo(incidence,
0362                        KIdentityManagementCore::IdentityManager::self()->identityForAddress(from),
0363                        from,
0364                        KCalendarCore::iTIPRequest,
0365                        bccMe,
0366                        recipients,
0367                        messageText);
0368     }
0369     delete publishdlg;
0370 }
0371 
0372 void ITIPHandler::setGroupwareUiDelegate(GroupwareUiDelegate *delegate)
0373 {
0374     d->m_uiDelegate = delegate;
0375 }
0376 
0377 void ITIPHandler::setCalendar(const Akonadi::CalendarBase::Ptr &calendar)
0378 {
0379     if (d->m_calendar != calendar) {
0380         d->m_calendar = calendar;
0381     }
0382 }
0383 
0384 void ITIPHandler::setShowDialogsOnError(bool enable)
0385 {
0386     d->m_showDialogsOnError = enable;
0387 }
0388 
0389 Akonadi::CalendarBase::Ptr ITIPHandler::calendar() const
0390 {
0391     return d->m_calendar;
0392 }
0393 
0394 #include "moc_itiphandler.cpp"