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"