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