File indexing completed on 2025-01-19 04:51:58
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 "eventcontroller.h" 0021 0022 #include <sink/applicationdomaintype.h> 0023 #include <sink/store.h> 0024 #include <sink/log.h> 0025 0026 #include <KMime/Message> 0027 #include <KCalendarCore/ICalFormat> 0028 #include <KCalendarCore/Event> 0029 #include <QUuid> 0030 0031 #include "eventoccurrencemodel.h" 0032 #include "recepientautocompletionmodel.h" 0033 #include "identitiesmodel.h" 0034 #include "mailtemplates.h" 0035 #include "sinkutils.h" 0036 0037 using namespace Sink::ApplicationDomain; 0038 0039 static std::pair<QString, QString> parseEmailAddress(const QString &email) { 0040 KMime::Types::Mailbox mb; 0041 mb.fromUnicodeString(email); 0042 return {mb.name(), mb.address()}; 0043 } 0044 0045 static QString assembleEmailAddress(const QString &name, const QString &email) { 0046 KMime::Types::Mailbox mb; 0047 mb.setName(name); 0048 mb.setAddress(email.toUtf8()); 0049 return mb.prettyAddress(); 0050 } 0051 0052 static std::pair<QStringList, QStringList> getRecipients(const QString &organizerEmail, const KCalendarCore::Attendee::List &attendees) 0053 { 0054 QStringList to; 0055 QStringList cc; 0056 for (const auto &a : attendees) { 0057 const auto email = a.email(); 0058 if (email.isEmpty()) { 0059 SinkTrace() << "Attendee has no email: " << a.fullName(); 0060 continue; 0061 } 0062 0063 //Don't send ourselves an email if part of attendees 0064 if (organizerEmail == email ) { 0065 SinkTrace() << "This is us: " << a.fullName(); 0066 continue; 0067 } 0068 0069 //No updates if the attendee has already declined 0070 if (a.status() == KCalendarCore::Attendee::Declined) { 0071 SinkTrace() << "Already declined: " << a.fullName(); 0072 continue; 0073 } 0074 0075 const auto prettyAddress = assembleEmailAddress(a.name(), email); 0076 0077 if (a.role() == KCalendarCore::Attendee::OptParticipant || 0078 a.role() == KCalendarCore::Attendee::NonParticipant) { 0079 cc << prettyAddress; 0080 } else { 0081 to << prettyAddress; 0082 } 0083 } 0084 return {to, cc}; 0085 } 0086 0087 QString EventController::eventToBody(const KCalendarCore::Event &event) 0088 { 0089 QString body; 0090 body.append(QObject::tr("== %1 ==").arg(event.summary())); 0091 body.append("\n\n"); 0092 body.append(QObject::tr("When: %1").arg(event.dtStart().toString())); 0093 // body.append(QObject::tr("Repeats: %1").arg(event->dtStart().toString())); 0094 if (!event.location().isEmpty()) { 0095 body.append("\n"); 0096 body.append(QObject::tr("Where: %1").arg(event.location())); 0097 } 0098 body.append("\n"); 0099 body.append(QObject::tr("Attendees:")); 0100 body.append("\n"); 0101 for (const auto &attendee : event.attendees()) { 0102 body.append(" " + attendee.fullName()); 0103 } 0104 return body; 0105 } 0106 0107 static void sendInvitation(const QByteArray &accountId, const QString &from, KCalendarCore::Event::Ptr event, bool isUpdate = false) 0108 { 0109 const auto attendees = event->attendees(); 0110 if (attendees.isEmpty()) { 0111 SinkLog() << "No attendees"; 0112 return; 0113 } 0114 0115 if (from.isEmpty()) { 0116 SinkWarning() << "Failed to find the organizer to send the reply from"; 0117 return; 0118 } 0119 0120 const auto [to, cc] = getRecipients(from, attendees); 0121 if(to.isEmpty() && cc.isEmpty()) { 0122 SinkWarning() << "There are really no attendees to e-mail"; 0123 return; 0124 } 0125 0126 QString subject; 0127 if (isUpdate) { 0128 subject = QObject::tr("\"%1\" has been updated").arg(event->summary()); 0129 } else { 0130 subject = QObject::tr("You've been invited to: \"%1\"").arg(event->summary()); 0131 } 0132 0133 QString body = EventController::eventToBody(*event); 0134 body.append("\n\n"); 0135 body.append(QObject::tr("Please find attached an iCalendar file with all the event details which you can import to your calendar application.")); 0136 0137 auto msg = MailTemplates::createIMipMessage( 0138 from, 0139 {to, cc, {}}, 0140 subject, 0141 body, 0142 KCalendarCore::ICalFormat{}.createScheduleMessage(event, KCalendarCore::iTIPRequest) 0143 ); 0144 0145 SinkTrace() << "Msg " << msg->encodedContent(); 0146 0147 SinkUtils::sendMail(msg->encodedContent(true), accountId) 0148 .then([&] (const KAsync::Error &error) { 0149 if (error) { 0150 SinkWarning() << "Failed to send message " << error; 0151 } 0152 }).exec(); 0153 } 0154 0155 class OrganizerSelector : public Selector { 0156 Q_OBJECT 0157 public: 0158 explicit OrganizerSelector(EventController &controller) : Selector(new IdentitiesModel), mController(controller) 0159 { 0160 } 0161 0162 void setCurrent(const QModelIndex &index) Q_DECL_OVERRIDE 0163 { 0164 if (index.isValid()) { 0165 auto currentAccountId = index.data(IdentitiesModel::AccountId).toByteArray(); 0166 const auto email = assembleEmailAddress(index.data(IdentitiesModel::Username).toString(), index.data(IdentitiesModel::Address).toString().toUtf8()); 0167 SinkLog() << "Setting current identity: " << email << "Account: " << currentAccountId; 0168 mController.setOrganizer(email); 0169 mController.setAccountId(currentAccountId); 0170 } else { 0171 SinkWarning() << "No valid identity for index: " << index; 0172 mController.clearOrganizer(); 0173 mController.clearAccountId(); 0174 } 0175 } 0176 private: 0177 EventController &mController; 0178 }; 0179 0180 class AttendeeCompleter : public Completer { 0181 public: 0182 AttendeeCompleter() : Completer(new RecipientAutocompletionModel) 0183 { 0184 } 0185 0186 void setSearchString(const QString &s) { 0187 static_cast<RecipientAutocompletionModel*>(model())->setFilter(s); 0188 Completer::setSearchString(s); 0189 } 0190 }; 0191 0192 class AttendeeController : public Kube::ListPropertyController 0193 { 0194 Q_OBJECT 0195 public: 0196 AttendeeController() : Kube::ListPropertyController{{"name", "status"}} 0197 { 0198 } 0199 }; 0200 0201 EventController::EventController() 0202 : Kube::Controller(), 0203 controller_attendees{new AttendeeController}, 0204 action_save{new Kube::ControllerAction{this, &EventController::save}}, 0205 mAttendeeCompleter{new AttendeeCompleter}, 0206 mIdentitySelector{new OrganizerSelector{*this}} 0207 { 0208 updateSaveAction(); 0209 } 0210 0211 Completer *EventController::attendeeCompleter() const 0212 { 0213 return mAttendeeCompleter.data(); 0214 } 0215 0216 Selector *EventController::identitySelector() const 0217 { 0218 return mIdentitySelector.data(); 0219 } 0220 0221 void EventController::save() 0222 { 0223 using namespace Sink; 0224 using namespace Sink::ApplicationDomain; 0225 0226 const auto calendar = getCalendar(); 0227 if (!calendar) { 0228 SinkWarning() << "No calendar selected"; 0229 return; 0230 } 0231 0232 const auto occurrenceVariant = getEventOccurrence(); 0233 if (occurrenceVariant.isValid()) { 0234 const auto occurrence = occurrenceVariant.value<EventOccurrenceModel::Occurrence>(); 0235 0236 Sink::ApplicationDomain::Event event = *occurrence.domainObject; 0237 0238 //Apply the changed properties on top of what's existing 0239 auto calcoreEvent = KCalendarCore::ICalFormat().readIncidence(event.getIcal()).dynamicCast<KCalendarCore::Event>(); 0240 if(!calcoreEvent) { 0241 SinkWarning() << "Invalid ICal to process, ignoring..."; 0242 return; 0243 } 0244 0245 saveToEvent(*calcoreEvent); 0246 0247 //Bump the sequence number 0248 calcoreEvent->setRevision(calcoreEvent->revision() + 1); 0249 0250 event.setIcal(KCalendarCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); 0251 event.setCalendar(*calendar); 0252 0253 //We ignore the case where we are not the organizer because we turn those read-only via the ourEvent property 0254 sendInvitation(getAccountId(), getOrganizer(), calcoreEvent, true); 0255 0256 auto job = Store::modify(event) 0257 .then([&] (const KAsync::Error &error) { 0258 if (error) { 0259 SinkWarning() << "Failed to save the event: " << error; 0260 } 0261 emit done(); 0262 }); 0263 0264 run(job); 0265 } else { 0266 Sink::ApplicationDomain::Event event(calendar->resourceInstanceIdentifier()); 0267 0268 auto calcoreEvent = QSharedPointer<KCalendarCore::Event>::create(); 0269 calcoreEvent->setUid(QUuid::createUuid().toString()); 0270 saveToEvent(*calcoreEvent); 0271 0272 event.setIcal(KCalendarCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); 0273 event.setCalendar(*calendar); 0274 0275 sendInvitation(getAccountId(), getOrganizer(), calcoreEvent); 0276 0277 auto job = Store::create(event) 0278 .then([&] (const KAsync::Error &error) { 0279 if (error) { 0280 SinkWarning() << "Failed to save the event: " << error; 0281 } 0282 emit done(); 0283 }); 0284 0285 run(job); 0286 } 0287 } 0288 0289 void EventController::updateSaveAction() 0290 { 0291 saveAction()->setEnabled(!getSummary().isEmpty()); 0292 } 0293 0294 static EventController::ParticipantStatus toStatus(KCalendarCore::Attendee::PartStat status) { 0295 switch(status) { 0296 case KCalendarCore::Attendee::Accepted: 0297 return EventController::Accepted; 0298 case KCalendarCore::Attendee::Declined: 0299 return EventController::Declined; 0300 case KCalendarCore::Attendee::NeedsAction: 0301 default: 0302 break; 0303 } 0304 return EventController::Unknown; 0305 } 0306 0307 static KCalendarCore::Attendee::PartStat fromStatus(EventController::ParticipantStatus status) { 0308 switch(status) { 0309 case EventController::Accepted: 0310 return KCalendarCore::Attendee::Accepted; 0311 case EventController::Declined: 0312 return KCalendarCore::Attendee::Declined; 0313 default: 0314 break; 0315 } 0316 return KCalendarCore::Attendee::NeedsAction; 0317 } 0318 0319 void EventController::populateFromEvent(const KCalendarCore::Event &event) 0320 { 0321 setSummary(event.summary()); 0322 setDescription(event.description()); 0323 setLocation(event.location()); 0324 setRecurring(event.recurs()); 0325 setAllDay(event.allDay()); 0326 setOurEvent(true); 0327 0328 setOrganizer(event.organizer().fullName()); 0329 for (const auto &attendee : event.attendees()) { 0330 attendeesController()->add({{"name", attendee.fullName()}, {"email", attendee.email()}, {"status", toStatus(attendee.status())}}); 0331 } 0332 } 0333 0334 void EventController::saveToEvent(KCalendarCore::Event &event) 0335 { 0336 event.setSummary(getSummary()); 0337 event.setDescription(getDescription()); 0338 event.setLocation(getLocation()); 0339 event.setDtStart(getStart()); 0340 event.setDtEnd(getEnd()); 0341 event.setAllDay(getAllDay()); 0342 event.setOrganizer(getOrganizer()); 0343 0344 event.clearAttendees(); 0345 KCalendarCore::Attendee::List attendees; 0346 attendeesController()->traverse([&] (const QVariantMap &map) { 0347 bool rsvp = true; 0348 KCalendarCore::Attendee::PartStat status = fromStatus(map["status"].value<ParticipantStatus>()); 0349 KCalendarCore::Attendee::Role role = KCalendarCore::Attendee::ReqParticipant; 0350 const auto [name, email] = parseEmailAddress(map["name"].toString()); 0351 event.addAttendee(KCalendarCore::Attendee(name, email, rsvp, status, role, QString{})); 0352 }); 0353 } 0354 0355 void EventController::init() 0356 { 0357 using namespace Sink; 0358 0359 const auto occurrenceVariant = getEventOccurrence(); 0360 if (occurrenceVariant.isValid()) { 0361 const auto occurrence = occurrenceVariant.value<EventOccurrenceModel::Occurrence>(); 0362 0363 Sink::ApplicationDomain::Event event = *occurrence.domainObject; 0364 0365 0366 setCalendar(ApplicationDomainType::Ptr::create(ApplicationDomainType::createEntity<ApplicationDomain::Calendar>(event.resourceInstanceIdentifier(), event.getCalendar()))); 0367 0368 auto icalEvent = KCalendarCore::ICalFormat().readIncidence(event.getIcal()).dynamicCast<KCalendarCore::Event>(); 0369 if(!icalEvent) { 0370 SinkWarning() << "Invalid ICal to process, ignoring..."; 0371 return; 0372 } 0373 populateFromEvent(*icalEvent); 0374 setStart(occurrence.start); 0375 setEnd(occurrence.end); 0376 } 0377 setModified(false); 0378 } 0379 0380 void EventController::reload() 0381 { 0382 init(); 0383 } 0384 0385 void EventController::remove() 0386 { 0387 const auto occurrenceVariant = getEventOccurrence(); 0388 if (occurrenceVariant.isValid()) { 0389 const auto occurrence = occurrenceVariant.value<EventOccurrenceModel::Occurrence>(); 0390 Sink::ApplicationDomain::Event event = *occurrence.domainObject; 0391 run(Sink::Store::remove(event)); 0392 } 0393 } 0394 0395 #include "eventcontroller.moc"