File indexing completed on 2025-01-19 04:51:59

0001 /*
0002  *  Copyright (C) 2017 Michael Bohlender, <michael.bohlender@kdemail.net>
0003  *  Copyright (C) 2018 Christian Mollekopf, <mollekopf@kolabsys.com>
0004  *
0005  *  This program is free software; you can redistribute it and/or modify
0006  *  it under the terms of the GNU General Public License as published by
0007  *  the Free Software Foundation; either version 2 of the License, or
0008  *  (at your option) any later version.
0009  *
0010  *  This program is distributed in the hope that it will be useful,
0011  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0013  *  GNU General Public License for more details.
0014  *
0015  *  You should have received a copy of the GNU General Public License along
0016  *  with this program; if not, write to the Free Software Foundation, Inc.,
0017  *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
0018  */
0019 
0020 #include "invitationcontroller.h"
0021 
0022 #include <sink/applicationdomaintype.h>
0023 #include <sink/store.h>
0024 #include <sink/log.h>
0025 
0026 #include <KCalendarCore/ICalFormat>
0027 #include <KCalendarCore/MemoryCalendar>
0028 #include <KCalendarCore/Event>
0029 #include <QUuid>
0030 
0031 #include "mailtemplates.h"
0032 #include "sinkutils.h"
0033 
0034 using namespace Sink::ApplicationDomain;
0035 
0036 InvitationController::InvitationController()
0037     : EventController(),
0038     action_accept{new Kube::ControllerAction{this, &InvitationController::accept}},
0039     action_decline{new Kube::ControllerAction{this, &InvitationController::decline}}
0040 {
0041 }
0042 
0043 static QString assembleEmailAddress(const QString &name, const QString &email) {
0044     KMime::Types::Mailbox mb;
0045     mb.setName(name);
0046     mb.setAddress(email.toUtf8());
0047     return mb.prettyAddress();
0048 }
0049 
0050 static KAsync::Job<std::pair<Sink::ApplicationDomain::Event, KCalendarCore::Event::Ptr>> findExistingEvents(const QByteArray &uid, const QString &instanceIdentifier)
0051 {
0052     using namespace Sink;
0053     using namespace Sink::ApplicationDomain;
0054     Query query;
0055     query.request<Event::Uid>();
0056     query.request<Event::Ical>();
0057     query.filter<Event::Uid>(uid);
0058     return Store::fetchAll<Event>(query).then([=](const QList<Event::Ptr> &events) {
0059         //Find the matching occurrence in case of exceptions
0060         for (const auto &e : events) {
0061             auto ical = KCalendarCore::ICalFormat().readIncidence(e->getIcal()).dynamicCast<KCalendarCore::Event>();
0062             if (ical && ical->instanceIdentifier() == instanceIdentifier) {
0063                 return std::pair(*e, ical);
0064             }
0065         }
0066         return std::pair<Event, KCalendarCore::Event::Ptr>{};
0067     });
0068 }
0069 
0070 void InvitationController::handleReply(KCalendarCore::Event::Ptr icalEvent)
0071 {
0072     using namespace Sink;
0073     using namespace Sink::ApplicationDomain;
0074 
0075     setMethod(InvitationMethod::Reply);
0076 
0077     auto attendees = icalEvent->attendees();
0078 
0079     if (!attendees.isEmpty()) {
0080         auto attendee = attendees.first();
0081         if (attendee.status() == KCalendarCore::Attendee::Declined) {
0082             setState(ParticipantStatus::Declined);
0083         } else if (attendee.status() == KCalendarCore::Attendee::Accepted) {
0084             setState(ParticipantStatus::Accepted);
0085         } else {
0086             setState(ParticipantStatus::Unknown);
0087         }
0088         setName(assembleEmailAddress(attendee.name(), attendee.email()));
0089     }
0090 
0091     populateFromEvent(*icalEvent);
0092     setStart(icalEvent->dtStart());
0093     setEnd(icalEvent->dtEnd());
0094     setUid(icalEvent->uid().toUtf8());
0095 }
0096 
0097 void InvitationController::handleCancellation(KCalendarCore::Event::Ptr icalEvent)
0098 {
0099     using namespace Sink;
0100     using namespace Sink::ApplicationDomain;
0101 
0102     setMethod(InvitationMethod::Cancel);
0103     setState(InvitationController::Cancelled);
0104 
0105     findExistingEvents(icalEvent->uid().toUtf8(), icalEvent->instanceIdentifier())
0106     .then([this, icalEvent](const std::pair<Event, KCalendarCore::Event::Ptr> &pair) {
0107         const auto [event, localEvent] = pair;
0108         if (localEvent) {
0109             mExistingEvent = event;
0110             if (icalEvent->revision() > localEvent->revision()) {
0111                 setEventState(InvitationController::Update);
0112             } else {
0113                 setEventState(InvitationController::Existing);
0114             }
0115         } else {
0116             //Already removed the event?
0117             setEventState(InvitationController::Existing);
0118         }
0119 
0120         if (icalEvent->recurrenceId().isValid()) {
0121             setRecurrenceId(icalEvent->recurrenceId());
0122         }
0123 
0124         populateFromEvent(*icalEvent);
0125         setStart(icalEvent->dtStart());
0126         setEnd(icalEvent->dtEnd());
0127         setUid(icalEvent->uid().toUtf8());
0128     }).exec();
0129 
0130 }
0131 
0132 KAsync::Job<EventController::ParticipantStatus> InvitationController::findAttendeeStatus()
0133 {
0134     using namespace Sink;
0135     using namespace Sink::ApplicationDomain;
0136 
0137     Query query;
0138     query.request<ApplicationDomain::Identity::Name>()
0139         .request<ApplicationDomain::Identity::Address>()
0140         .request<ApplicationDomain::Identity::Account>();
0141     auto job = Store::fetchAll<ApplicationDomain::Identity>(query)
0142         .then([this] (const QList<Identity::Ptr> &list) {
0143             if (list.isEmpty()) {
0144                 SinkWarning() << "Failed to find an identity";
0145             }
0146             for (const auto &identity : list) {
0147                 const auto id = attendeesController()->findByProperty("email", identity->getAddress());
0148                 if (!id.isEmpty()) {
0149                     return attendeesController()->value(id, "status").value<EventController::ParticipantStatus>();
0150                 } else {
0151                     SinkLog() << "No attendee found for " << identity->getAddress();
0152                 }
0153             }
0154             SinkWarning() << "Failed to find matching identity in list of attendees.";
0155             return EventController::NoMatch;
0156         });
0157     return job;
0158 }
0159 
0160 void InvitationController::handleRequest(KCalendarCore::Event::Ptr icalEvent)
0161 {
0162     using namespace Sink;
0163     using namespace Sink::ApplicationDomain;
0164 
0165     setMethod(InvitationMethod::Request);
0166 
0167     findExistingEvents(icalEvent->uid().toUtf8(), icalEvent->instanceIdentifier())
0168     .then([this, icalEvent](const std::pair<Event, KCalendarCore::Event::Ptr> &pair) {
0169         const auto [event, localEvent] = pair;
0170         if (localEvent) {
0171             mExistingEvent = event;
0172             if (icalEvent->revision() > localEvent->revision()) {
0173                 setEventState(InvitationController::Update);
0174                 //The invitation is more recent, this is an update to an existing event
0175                 populateFromEvent(*icalEvent);
0176                 if (icalEvent->recurrenceId().isValid()) {
0177                     setRecurrenceId(icalEvent->recurrenceId());
0178                 }
0179                 setStart(icalEvent->dtStart());
0180                 setEnd(icalEvent->dtEnd());
0181                 setUid(icalEvent->uid().toUtf8());
0182             } else {
0183                 setEventState(InvitationController::Existing);
0184                 //Our local copy is more recent (we probably already dealt with the invitation)
0185                 populateFromEvent(*localEvent);
0186                 setStart(localEvent->dtStart());
0187                 setEnd(localEvent->dtEnd());
0188                 setUid(localEvent->uid().toUtf8());
0189             }
0190         } else {
0191             mExistingEvent = {};
0192             if (icalEvent->recurrenceId().isValid()) {
0193                 setRecurrenceId(icalEvent->recurrenceId());
0194                 setEventState(InvitationController::Update);
0195             } else {
0196                 setEventState(InvitationController::New);
0197             }
0198             //We don't even have a local copy, this is a new event
0199             populateFromEvent(*icalEvent);
0200             setStart(icalEvent->dtStart());
0201             setEnd(icalEvent->dtEnd());
0202             setUid(icalEvent->uid().toUtf8());
0203         }
0204 
0205         return findAttendeeStatus()
0206             .guard(this)
0207             .then([this] (ParticipantStatus status) {
0208                 setState(status);
0209             });
0210 
0211     }).exec();
0212 }
0213 
0214 void InvitationController::loadICal(const QString &ical)
0215 {
0216     using namespace Sink;
0217     using namespace Sink::ApplicationDomain;
0218 
0219     KCalendarCore::Calendar::Ptr calendar(new KCalendarCore::MemoryCalendar{QTimeZone::systemTimeZone()});
0220     auto msg = KCalendarCore::ICalFormat{}.parseScheduleMessage(calendar, ical.toUtf8());
0221     if (!msg) {
0222         SinkWarning() << "Invalid schedule message to process, ignoring...";
0223         return;
0224     }
0225     auto icalEvent = msg->event().dynamicCast<KCalendarCore::Event>();
0226     if(!icalEvent) {
0227         SinkWarning() << "Invalid ICal to process, ignoring...";
0228         return;
0229     }
0230 
0231     mLoadedIcalEvent = icalEvent;
0232 
0233     switch (msg->method()) {
0234         case KCalendarCore::iTIPRequest:
0235             //Roundcube sends cancellations not as METHOD=CANCEL, but instead updates the event status.
0236             if (icalEvent->status() == KCalendarCore::Incidence::StatusCanceled) {
0237                 handleCancellation(icalEvent);
0238                 break;
0239             }
0240 
0241             handleRequest(icalEvent);
0242             break;
0243         case KCalendarCore::iTIPReply:
0244             handleReply(icalEvent);
0245             break;
0246         case KCalendarCore::iTIPCancel:
0247             handleCancellation(icalEvent);
0248             break;
0249         default:
0250             SinkWarning() << "Invalid method " << msg->method();
0251     }
0252 
0253 }
0254 
0255 static void sendIMipReply(const QByteArray &accountId, const QString &from, const QString &fromName, KCalendarCore::Event::Ptr event, KCalendarCore::Attendee::PartStat status)
0256 {
0257     const auto organizerEmail = event->organizer().fullName();
0258 
0259     if (organizerEmail.isEmpty()) {
0260         SinkWarning() << "Failed to find the organizer to send the reply to " << organizerEmail;
0261         return;
0262     }
0263 
0264     auto reply = KCalendarCore::Event::Ptr::create(*event);
0265     reply->clearAttendees();
0266     reply->addAttendee(KCalendarCore::Attendee(fromName, from, false, status));
0267 
0268     QString body;
0269     if (status == KCalendarCore::Attendee::Accepted) {
0270         body.append(QObject::tr("%1 has accepted the invitation to the following event").arg(fromName));
0271     } else {
0272         body.append(QObject::tr("%1 has declined the invitation to the following event").arg(fromName));
0273     }
0274     body.append("\n\n");
0275     body.append(EventController::eventToBody(*reply));
0276 
0277     QString subject;
0278     if (status == KCalendarCore::Attendee::Accepted) {
0279         subject = QObject::tr("\"%1\" has been accepted by %2").arg(event->summary()).arg(fromName);
0280     } else {
0281         subject = QObject::tr("\"%1\" has been declined by %2").arg(event->summary()).arg(fromName);
0282     }
0283 
0284     const auto msg = MailTemplates::createIMipMessage(
0285         from,
0286         {{organizerEmail}, {}, {}},
0287         subject,
0288         body,
0289         KCalendarCore::ICalFormat{}.createScheduleMessage(reply, KCalendarCore::iTIPReply)
0290     );
0291 
0292     SinkTrace() << "Msg " << msg->encodedContent();
0293 
0294     SinkUtils::sendMail(msg->encodedContent(true), accountId)
0295         .then([&] (const KAsync::Error &error) {
0296             if (error) {
0297                 SinkWarning() << "Failed to send message " << error;
0298             }
0299         }).exec();
0300 }
0301 
0302 void InvitationController::storeEvent(ParticipantStatus status)
0303 {
0304     using namespace Sink;
0305     using namespace Sink::ApplicationDomain;
0306 
0307     Query query;
0308     query.request<ApplicationDomain::Identity::Name>()
0309         .request<ApplicationDomain::Identity::Address>()
0310         .request<ApplicationDomain::Identity::Account>();
0311     auto job = Store::fetchAll<ApplicationDomain::Identity>(query)
0312         .guard(this)
0313         .then([this, status] (const QList<Identity::Ptr> &list) {
0314             if (list.isEmpty()) {
0315                 SinkWarning() << "Failed to find an identity";
0316             }
0317             QString fromAddress;
0318             QString fromName;
0319             QByteArray accountId;
0320             bool foundMatch = false;
0321             for (const auto &identity : list) {
0322                 const auto id = attendeesController()->findByProperty("email", identity->getAddress());
0323                 if (!id.isEmpty()) {
0324                     auto participantStatus = status == InvitationController::Accepted ? EventController::Accepted : EventController::Declined;
0325                     attendeesController()->setValue(id, "status", participantStatus);
0326                     fromAddress = identity->getAddress();
0327                     fromName = identity->getName();
0328                     accountId = identity->getAccount();
0329                     foundMatch = true;
0330                 } else {
0331                     SinkLog() << "No identity found for " << identity->getAddress();
0332                 }
0333             }
0334             if (!foundMatch) {
0335                 SinkWarning() << "Failed to find a matching identity.";
0336                 return KAsync::error("Failed to find a matching identity");
0337             }
0338 
0339             auto calcoreEvent = mLoadedIcalEvent;
0340             calcoreEvent->setUid(getUid());
0341             saveToEvent(*calcoreEvent);
0342 
0343             sendIMipReply(accountId, fromAddress, fromName, calcoreEvent, status == InvitationController::Accepted ? KCalendarCore::Attendee::Accepted : KCalendarCore::Attendee::Declined);
0344 
0345             if (mExistingEvent.identifier().isEmpty()) {
0346                 const auto calendar = getCalendar();
0347                 if (!calendar) {
0348                     SinkWarning() << "No calendar selected";
0349                     return KAsync::error("No calendar selected");
0350                 }
0351 
0352                 Event event(calendar->resourceInstanceIdentifier());
0353                 event.setIcal(KCalendarCore::ICalFormat().toICalString(calcoreEvent).toUtf8());
0354                 event.setCalendar(*calendar);
0355 
0356                 return Store::create(event)
0357                     .then([=] (const KAsync::Error &error) {
0358                         if (error) {
0359                             SinkWarning() << "Failed to save the event: " << error;
0360                         }
0361                         setState(status);
0362                         emit done();
0363                     });
0364             } else {
0365                 Event event(mExistingEvent);
0366                 event.setIcal(KCalendarCore::ICalFormat().toICalString(calcoreEvent).toUtf8());
0367 
0368                 return Store::modify(event)
0369                     .then([=] (const KAsync::Error &error) {
0370                         if (error) {
0371                             SinkWarning() << "Failed to update the event: " << error;
0372                         }
0373                         setState(status);
0374                         setEventState(InvitationController::Existing);
0375                         emit done();
0376                     });
0377             }
0378         });
0379 
0380     run(job);
0381 }
0382 
0383 void InvitationController::accept()
0384 {
0385     if (getMethod() == Cancel) {
0386         storeEvent(ParticipantStatus::Cancelled);
0387     } else {
0388         storeEvent(ParticipantStatus::Accepted);
0389     }
0390 }
0391 
0392 void InvitationController::decline()
0393 {
0394     storeEvent(ParticipantStatus::Declined);
0395 }
0396