1 /*
2   This file is part of kdepim.
3 
4   SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org>
5   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
6   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
7   SPDX-FileCopyrightText: 2017-2021 Laurent Montel <montel@kde.org>
8 
9   SPDX-License-Identifier: GPL-2.0-or-later
10 */
11 
12 #include "attendeeselector.h"
13 #include "calendarinterface.h"
14 #include "delegateselector.h"
15 #include "memorycalendarmemento.h"
16 #include "reactiontoinvitationdialog.h"
17 #include "syncitiphandler.h"
18 
19 #include <KIdentityManagement/IdentityManager>
20 
21 #include <MessageViewer/BodyPartURLHandler>
22 #include <MessageViewer/HtmlWriter>
23 #include <MessageViewer/MessagePartRenderPlugin>
24 #include <MessageViewer/MessagePartRendererBase>
25 #include <MessageViewer/MessageViewerSettings>
26 #include <MessageViewer/Viewer>
27 #include <MimeTreeParser/BodyPart>
28 #include <MimeTreeParser/MessagePart>
29 using namespace MessageViewer;
30 
31 #include <KCalendarCore/ICalFormat>
32 using namespace KCalendarCore;
33 
34 #include <KCalUtils/IncidenceFormatter>
35 
36 #include <KMime/Message>
37 
38 #include <KIdentityManagement/Identity>
39 
40 #include <KEmailAddress>
41 
42 #include <MailTransport/TransportManager>
43 #include <MailTransportAkonadi/MessageQueueJob>
44 
45 #include <KontactInterface/PimUniqueApplication>
46 
47 #include "text_calendar_debug.h"
48 
49 #include <KIO/FileCopyJob>
50 #include <KIO/JobUiDelegate>
51 #include <KIO/OpenUrlJob>
52 #include <KIO/StatJob>
53 #include <KLocalizedString>
54 #include <KMessageBox>
55 
56 #include <QDBusServiceWatcher>
57 #include <QDesktopServices>
58 #include <QFileDialog>
59 #include <QIcon>
60 #include <QInputDialog>
61 #include <QMenu>
62 #include <QMimeDatabase>
63 #include <QPointer>
64 #include <QTemporaryFile>
65 #include <QUrl>
66 
67 using namespace MailTransport;
68 
69 namespace
70 {
hasMyWritableEventsFolders(const QString & family)71 static bool hasMyWritableEventsFolders(const QString &family)
72 {
73     Q_UNUSED(family)
74 #if 0 // TODO port to Akonadi
75     QString myfamily = family;
76     if (family.isEmpty()) {
77         myfamily = QStringLiteral("calendar");
78     }
79 
80 #ifndef KDEPIM_NO_KRESOURCES
81     CalendarResourceManager manager(myfamily);
82     manager.readConfig();
83 
84     CalendarResourceManager::ActiveIterator it;
85     for (it = manager.activeBegin(); it != manager.activeEnd(); ++it) {
86         if ((*it)->readOnly()) {
87             continue;
88         }
89 
90         const QStringList subResources = (*it)->subresources();
91         if (subResources.isEmpty()) {
92             return true;
93         }
94 
95         QStringList::ConstIterator subIt;
96         for (subIt = subResources.begin(); subIt != subResources.end(); ++subIt) {
97             if (!(*it)->subresourceActive((*subIt))) {
98                 continue;
99             }
100             if ((*it)->type() == "imap" || (*it)->type() == "kolab") {
101                 if ((*it)->subresourceType((*subIt)) == "todo"
102                     || (*it)->subresourceType((*subIt)) == "journal"
103                     || !(*subIt).contains("/.INBOX.directory/")) {
104                     continue;
105                 }
106             }
107             return true;
108         }
109     }
110     return false;
111 #endif
112 #else
113     qCDebug(TEXT_CALENDAR_LOG) << "Disabled code, port to Akonadi";
114     return true;
115 #endif
116 }
117 
occurredAlready(const Incidence::Ptr & incidence)118 static bool occurredAlready(const Incidence::Ptr &incidence)
119 {
120     Q_ASSERT(incidence);
121     const QDateTime now = QDateTime::currentDateTime();
122     const QDate today = now.date();
123 
124     if (incidence->recurs()) {
125         const QDateTime nextDate = incidence->recurrence()->getNextDateTime(now);
126 
127         return !nextDate.isValid();
128     } else {
129         const QDateTime incidenceDate = incidence->dateTime(Incidence::RoleDisplayEnd);
130         if (incidenceDate.isValid()) {
131             return incidence->allDay() ? (incidenceDate.date() < today) : (incidenceDate < QDateTime::currentDateTime());
132         }
133     }
134 
135     return false;
136 }
137 
138 class KMInvitationFormatterHelper : public KCalUtils::InvitationFormatterHelper
139 {
140 public:
KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr & bodyPart,const KCalendarCore::MemoryCalendar::Ptr & calendar)141     KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr &bodyPart, const KCalendarCore::MemoryCalendar::Ptr &calendar)
142         : mBodyPart(bodyPart)
143         , mCalendar(calendar)
144     {
145     }
146 
generateLinkURL(const QString & id)147     QString generateLinkURL(const QString &id) override
148     {
149         return mBodyPart->makeLink(id);
150     }
151 
calendar() const152     KCalendarCore::Calendar::Ptr calendar() const override
153     {
154         return mCalendar;
155     }
156 
157 private:
158     const MimeTreeParser::MessagePartPtr mBodyPart;
159     const KCalendarCore::MemoryCalendar::Ptr mCalendar;
160 };
161 
162 class Formatter : public MessageViewer::MessagePartRendererBase
163 {
164 public:
render(const MimeTreeParser::MessagePartPtr & msgPart,MessageViewer::HtmlWriter * writer,MessageViewer::RenderContext *) const165     bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override
166     {
167         QMimeDatabase db;
168         auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower()));
169         if (!mt.isValid() || mt.name() != QLatin1String("text/calendar")) {
170             return false;
171         }
172 
173         auto nodeHelper = msgPart->nodeHelper();
174         if (!nodeHelper) {
175             return false;
176         }
177 
178         /** Formatting is async now because we need to fetch incidences from akonadi.
179             Basically this method (format()) will be called twice. The first time
180             it creates the memento that fetches incidences and returns.
181 
182             When the memento finishes, this is called a second time, and we can proceed.
183 
184             BodyPartMementos are documented in MessageViewer/ObjectTreeParser
185         */
186         auto memento = dynamic_cast<MemoryCalendarMemento *>(msgPart->memento());
187 
188         if (memento) {
189             auto const message = dynamic_cast<KMime::Message *>(msgPart->content()->topLevel());
190             if (!message) {
191                 qCWarning(TEXT_CALENDAR_LOG) << "The top-level content is not a message. Cannot handle the invitation then.";
192                 return false;
193             }
194 
195             if (memento->finished()) {
196                 KMInvitationFormatterHelper helper(msgPart, memento->calendar());
197                 QString source;
198                 // If the bodypart does not have a charset specified, we need to fall back to utf8,
199                 // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
200                 if (msgPart->content()->contentType()->parameter(QStringLiteral("charset")).isEmpty()) {
201                     const QByteArray &ba = msgPart->content()->decodedContent();
202                     source = QString::fromUtf8(ba);
203                 } else {
204                     source = msgPart->text();
205                 }
206 
207                 MemoryCalendar::Ptr cl(new MemoryCalendar(QTimeZone::systemTimeZone()));
208                 const QString html = KCalUtils::IncidenceFormatter::formatICalInvitationNoHtml(source, cl, &helper, message->sender()->asUnicodeString());
209 
210                 if (html.isEmpty()) {
211                     return false;
212                 }
213                 writer->write(html);
214             }
215         } else {
216             auto memento = new MemoryCalendarMemento();
217             msgPart->setMemento(memento);
218             QObject::connect(memento, &MemoryCalendarMemento::update, nodeHelper, &MimeTreeParser::NodeHelper::update);
219         }
220 
221         return true;
222     }
223 };
224 
directoryForStatus(Attendee::PartStat status)225 static QString directoryForStatus(Attendee::PartStat status)
226 {
227     QString dir;
228     switch (status) {
229     case Attendee::Accepted:
230         dir = QStringLiteral("accepted");
231         break;
232     case Attendee::Tentative:
233         dir = QStringLiteral("tentative");
234         break;
235     case Attendee::Declined:
236         dir = QStringLiteral("cancel");
237         break;
238     case Attendee::Delegated:
239         dir = QStringLiteral("delegated");
240         break;
241     case Attendee::NeedsAction:
242         dir = QStringLiteral("request");
243         break;
244     default:
245         break;
246     }
247     return dir;
248 }
249 
stringToIncidence(const QString & iCal)250 static Incidence::Ptr stringToIncidence(const QString &iCal)
251 {
252     MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::systemTimeZone()));
253     ICalFormat format;
254     ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, iCal);
255     if (!message) {
256         // TODO: Error message?
257         qCWarning(TEXT_CALENDAR_LOG) << "Can't parse this ical string: " << iCal;
258         return Incidence::Ptr();
259     }
260 
261     return message->event().dynamicCast<Incidence>();
262 }
263 
264 class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler
265 {
266 public:
UrlHandler()267     UrlHandler()
268     {
269         // qCDebug(TEXT_CALENDAR_LOG) << "UrlHandler() (iCalendar)";
270     }
271 
name() const272     QString name() const override
273     {
274         return QStringLiteral("calendar handler");
275     }
276 
findMyself(const Incidence::Ptr & incidence,const QString & receiver) const277     Attendee findMyself(const Incidence::Ptr &incidence, const QString &receiver) const
278     {
279         const Attendee::List attendees = incidence->attendees();
280         const auto idx = findMyself(attendees, receiver);
281         if (idx >= 0) {
282             return attendees.at(idx);
283         }
284         return {};
285     }
286 
findMyself(const Attendee::List & attendees,const QString & receiver) const287     int findMyself(const Attendee::List &attendees, const QString &receiver) const
288     {
289         // Find myself. There will always be all attendees listed, even if
290         // only I need to answer it.
291         for (int i = 0; i < attendees.size(); ++i) {
292             // match only the email part, not the name
293             if (KEmailAddress::compareEmail(attendees.at(i).email(), receiver, false)) {
294                 // We are the current one, and even the receiver, note
295                 // this and quit searching.
296                 return i;
297             }
298         }
299         return -1;
300     }
301 
heuristicalRSVP(const Incidence::Ptr & incidence)302     static bool heuristicalRSVP(const Incidence::Ptr &incidence)
303     {
304         bool rsvp = true; // better send superfluously than not at all
305         const Attendee::List attendees = incidence->attendees();
306         Attendee::List::ConstIterator it;
307         Attendee::List::ConstIterator end(attendees.constEnd());
308         for (it = attendees.constBegin(); it != end; ++it) {
309             if (it == attendees.constBegin()) {
310                 rsvp = (*it).RSVP(); // use what the first one has
311             } else {
312                 if ((*it).RSVP() != rsvp) {
313                     rsvp = true; // they differ, default
314                     break;
315                 }
316             }
317         }
318         return rsvp;
319     }
320 
heuristicalRole(const Incidence::Ptr & incidence)321     static Attendee::Role heuristicalRole(const Incidence::Ptr &incidence)
322     {
323         Attendee::Role role = Attendee::OptParticipant;
324         const Attendee::List attendees = incidence->attendees();
325         Attendee::List::ConstIterator it;
326         Attendee::List::ConstIterator end = attendees.constEnd();
327 
328         for (it = attendees.constBegin(); it != end; ++it) {
329             if (it == attendees.constBegin()) {
330                 role = (*it).role(); // use what the first one has
331             } else {
332                 if ((*it).role() != role) {
333                     role = Attendee::OptParticipant; // they differ, default
334                     break;
335                 }
336             }
337         }
338         return role;
339     }
340 
findAttachment(const QString & name,const QString & iCal)341     static Attachment findAttachment(const QString &name, const QString &iCal)
342     {
343         Incidence::Ptr incidence = stringToIncidence(iCal);
344 
345         // get the attachment by name from the incidence
346         Attachment::List attachments = incidence->attachments();
347         Attachment attachment;
348         const Attachment::List::ConstIterator end = attachments.constEnd();
349         for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) {
350             if ((*it).label() == name) {
351                 attachment = *it;
352                 break;
353             }
354         }
355 
356         if (attachment.isEmpty()) {
357             KMessageBox::error(nullptr, i18n("No attachment named \"%1\" found in the invitation.", name));
358             return Attachment();
359         }
360 
361         if (attachment.isUri()) {
362             bool fileExists = false;
363             QUrl attachmentUrl(attachment.uri());
364             if (attachmentUrl.isLocalFile()) {
365                 fileExists = QFile::exists(attachmentUrl.toLocalFile());
366             } else {
367                 auto job = KIO::statDetails(attachmentUrl, KIO::StatJob::SourceSide, KIO::StatBasic);
368                 fileExists = job->exec();
369             }
370             if (!fileExists) {
371                 KMessageBox::information(nullptr,
372                                          i18n("The invitation attachment \"%1\" is a web link that "
373                                               "is inaccessible from this computer. Please ask the event "
374                                               "organizer to resend the invitation with this attachment "
375                                               "stored inline instead of a link.",
376                                               attachmentUrl.toDisplayString()));
377                 return Attachment();
378             }
379         }
380         return attachment;
381     }
382 
findReceiver(KMime::Content * node)383     static QString findReceiver(KMime::Content *node)
384     {
385         if (!node || !node->topLevel()) {
386             return QString();
387         }
388 
389         QString receiver;
390         KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self();
391 
392         KMime::Types::Mailbox::List addrs;
393         if (auto header = node->topLevel()->header<KMime::Headers::To>()) {
394             addrs = header->mailboxes();
395         }
396         int found = 0;
397         QVector<KMime::Types::Mailbox>::const_iterator end = addrs.constEnd();
398         for (QVector<KMime::Types::Mailbox>::const_iterator it = addrs.constBegin(); it != end; ++it) {
399             if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) {
400                 // Ok, this could be us
401                 ++found;
402                 receiver = QLatin1String((*it).address());
403             }
404         }
405 
406         KMime::Types::Mailbox::List ccaddrs;
407         if (auto header = node->topLevel()->header<KMime::Headers::Cc>()) {
408             ccaddrs = header->mailboxes();
409         }
410         end = ccaddrs.constEnd();
411         for (QVector<KMime::Types::Mailbox>::const_iterator it = ccaddrs.constBegin(); it != end; ++it) {
412             if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) {
413                 // Ok, this could be us
414                 ++found;
415                 receiver = QLatin1String((*it).address());
416             }
417         }
418         if (found != 1) {
419             QStringList possibleAddrs;
420             bool ok;
421             QString selectMessage;
422             if (found == 0) {
423                 selectMessage = i18n(
424                     "<qt>None of your identities match the receiver of this message,<br/>"
425                     "please choose which of the following addresses is yours,<br/> if any, "
426                     "or select one of your identities to use in the reply:</qt>");
427                 possibleAddrs += im->allEmails();
428             } else {
429                 selectMessage = i18n(
430                     "<qt>Several of your identities match the receiver of this message,<br/>"
431                     "please choose which of the following addresses is yours:</qt>");
432                 possibleAddrs.reserve(addrs.count() + ccaddrs.count());
433                 for (const KMime::Types::Mailbox &mbx : std::as_const(addrs)) {
434                     possibleAddrs.append(QLatin1String(mbx.address()));
435                 }
436                 for (const KMime::Types::Mailbox &mbx : std::as_const(ccaddrs)) {
437                     possibleAddrs.append(QLatin1String(mbx.address()));
438                 }
439             }
440 
441             // select default identity by default
442             const QString defaultAddr = im->defaultIdentity().primaryEmailAddress();
443             const int defaultIndex = qMax(0, possibleAddrs.indexOf(defaultAddr));
444 
445             receiver = QInputDialog::getItem(nullptr, i18n("Select Address"), selectMessage, possibleAddrs, defaultIndex, false, &ok);
446 
447             if (!ok) {
448                 receiver.clear();
449             }
450         }
451         return receiver;
452     }
453 
setStatusOnMyself(const Incidence::Ptr & incidence,const Attendee & myself,Attendee::PartStat status,const QString & receiver) const454     Attendee setStatusOnMyself(const Incidence::Ptr &incidence, const Attendee &myself, Attendee::PartStat status, const QString &receiver) const
455     {
456         QString name;
457         QString email;
458         KEmailAddress::extractEmailAddressAndName(receiver, email, name);
459         if (name.isEmpty() && !myself.isNull()) {
460             name = myself.name();
461         }
462         if (email.isEmpty() && !myself.isNull()) {
463             email = myself.email();
464         }
465         Q_ASSERT(!email.isEmpty()); // delivery must be possible
466 
467         Attendee newMyself(name,
468                            email,
469                            true, // RSVP, otherwise we would not be here
470                            status,
471                            !myself.isNull() ? myself.role() : heuristicalRole(incidence),
472                            myself.uid());
473         if (!myself.isNull()) {
474             newMyself.setDelegate(myself.delegate());
475             newMyself.setDelegator(myself.delegator());
476         }
477 
478         // Make sure only ourselves is in the event
479         incidence->clearAttendees();
480         if (!newMyself.isNull()) {
481             incidence->addAttendee(newMyself);
482         }
483         return newMyself;
484     }
485 
486     enum MailType {
487         Answer,
488         Delegation,
489         Forward,
490         DeclineCounter,
491     };
492 
mailICal(const QString & receiver,const QString & to,const QString & iCal,const QString & subject,const QString & status,bool delMessage,Viewer * viewerInstance) const493     bool mailICal(const QString &receiver,
494                   const QString &to,
495                   const QString &iCal,
496                   const QString &subject,
497                   const QString &status,
498                   bool delMessage,
499                   Viewer *viewerInstance) const
500     {
501         qCDebug(TEXT_CALENDAR_LOG) << "Mailing message:" << iCal;
502 
503         KMime::Message::Ptr msg(new KMime::Message);
504         if (MessageViewer::MessageViewerSettings::self()->exchangeCompatibleInvitations()) {
505             msg->subject()->fromUnicodeString(status, "utf-8");
506             QString tsubject = subject;
507             tsubject.remove(i18n("Answer: "));
508             if (status == QLatin1String("cancel")) {
509                 msg->subject()->fromUnicodeString(i18nc("Not able to attend.", "Declined: %1", tsubject), "utf-8");
510             } else if (status == QLatin1String("tentative")) {
511                 msg->subject()->fromUnicodeString(i18nc("Unsure if it is possible to attend.", "Tentative: %1", tsubject), "utf-8");
512             } else if (status == QLatin1String("accepted")) {
513                 msg->subject()->fromUnicodeString(i18nc("Accepted the invitation.", "Accepted: %1", tsubject), "utf-8");
514             } else {
515                 msg->subject()->fromUnicodeString(subject, "utf-8");
516             }
517         } else {
518             msg->subject()->fromUnicodeString(subject, "utf-8");
519         }
520         msg->to()->fromUnicodeString(to, "utf-8");
521         msg->from()->fromUnicodeString(receiver, "utf-8");
522         msg->date()->setDateTime(QDateTime::currentDateTime());
523 
524         if (MessageViewer::MessageViewerSettings::self()->legacyBodyInvites()) {
525             auto ct = msg->contentType(); // create
526             ct->setMimeType("text/calendar");
527             ct->setCharset("utf-8");
528             ct->setName(QStringLiteral("cal.ics"), "utf-8");
529             ct->setParameter(QStringLiteral("method"), QStringLiteral("reply"));
530 
531             auto disposition = new KMime::Headers::ContentDisposition;
532             disposition->setDisposition(KMime::Headers::CDinline);
533             msg->setHeader(disposition);
534             msg->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
535             const QString answer = i18n("Invitation answer attached.");
536             msg->setBody(answer.toUtf8());
537         } else {
538             // We need to set following 4 lines by hand else KMime::Content::addContent
539             // will create a new Content instance for us to attach the main message
540             // what we don't need cause we already have the main message instance where
541             // 2 additional messages are attached.
542             KMime::Headers::ContentType *ct = msg->contentType();
543             ct->setMimeType("multipart/mixed");
544             ct->setBoundary(KMime::multiPartBoundary());
545             ct->setCategory(KMime::Headers::CCcontainer);
546 
547             // Set the first multipart, the body message.
548             auto bodyMessage = new KMime::Content;
549             auto bodyDisposition = new KMime::Headers::ContentDisposition;
550             bodyDisposition->setDisposition(KMime::Headers::CDinline);
551             auto bodyMessageCt = bodyMessage->contentType();
552             bodyMessageCt->setMimeType("text/plain");
553             bodyMessageCt->setCharset("utf-8");
554             bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
555             const QString answer = i18n("Invitation answer attached.");
556             bodyMessage->setBody(answer.toUtf8());
557             bodyMessage->setHeader(bodyDisposition);
558             msg->addContent(bodyMessage);
559 
560             // Set the second multipart, the attachment.
561             auto attachMessage = new KMime::Content;
562             auto attachDisposition = new KMime::Headers::ContentDisposition;
563             attachDisposition->setDisposition(KMime::Headers::CDattachment);
564             auto attachCt = attachMessage->contentType();
565             attachCt->setMimeType("text/calendar");
566             attachCt->setCharset("utf-8");
567             attachCt->setName(QStringLiteral("cal.ics"), "utf-8");
568             attachCt->setParameter(QStringLiteral("method"), QStringLiteral("reply"));
569             attachMessage->setHeader(attachDisposition);
570             attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
571             attachMessage->setBody(KMime::CRLFtoLF(iCal.toUtf8()));
572             msg->addContent(attachMessage);
573         }
574 
575         // Try and match the receiver with an identity.
576         // Setting the identity here is important, as that is used to select the correct
577         // transport later
578         KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self();
579         const KIdentityManagement::Identity identity = im->identityForAddress(findReceiver(viewerInstance->message().data()));
580 
581         const bool nullIdentity = (identity == KIdentityManagement::Identity::null());
582 
583         if (!nullIdentity) {
584             auto x_header = new KMime::Headers::Generic("X-KMail-Identity");
585             x_header->from7BitString(QByteArray::number(identity.uoid()));
586             msg->setHeader(x_header);
587         }
588 
589         const bool identityHasTransport = !identity.transport().isEmpty();
590         int transportId = -1;
591         if (!nullIdentity && identityHasTransport) {
592             transportId = identity.transport().toInt();
593         } else {
594             transportId = TransportManager::self()->defaultTransportId();
595         }
596         if (transportId == -1) {
597             if (!TransportManager::self()->showTransportCreationDialog(nullptr, TransportManager::IfNoTransportExists)) {
598                 return false;
599             }
600             transportId = TransportManager::self()->defaultTransportId();
601         }
602         auto header = new KMime::Headers::Generic("X-KMail-Transport");
603         header->fromUnicodeString(QString::number(transportId), "utf-8");
604         msg->setHeader(header);
605 
606         // Outlook will only understand the reply if the From: header is the
607         // same as the To: header of the invitation message.
608         if (!MessageViewer::MessageViewerSettings::self()->legacyMangleFromToHeaders()) {
609             if (identity != KIdentityManagement::Identity::null()) {
610                 msg->from()->fromUnicodeString(identity.fullEmailAddr(), "utf-8");
611             }
612             // Remove BCC from identity on ical invitations (kolab/issue474)
613             msg->removeHeader<KMime::Headers::Bcc>();
614         }
615 
616         msg->assemble();
617         MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(transportId);
618 
619         auto job = new MailTransport::MessageQueueJob;
620 
621         job->addressAttribute().setTo(QStringList() << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(to)));
622         job->transportAttribute().setTransportId(transport->id());
623 
624         if (transport->specifySenderOverwriteAddress()) {
625             job->addressAttribute().setFrom(
626                 KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress())));
627         } else {
628             job->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(msg->from()->asUnicodeString())));
629         }
630 
631         job->setMessage(msg);
632 
633         if (!job->exec()) {
634             qCWarning(TEXT_CALENDAR_LOG) << "Error queuing message in outbox:" << job->errorText();
635             return false;
636         }
637         // We are not notified when mail was sent, so assume it was sent when queued.
638         if (delMessage && MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
639             viewerInstance->deleteMessage();
640         }
641         return true;
642     }
643 
mail(Viewer * viewerInstance,const Incidence::Ptr & incidence,const QString & status,iTIPMethod method=iTIPReply,const QString & receiver=QString (),const QString & to=QString (),MailType type=Answer) const644     bool mail(Viewer *viewerInstance,
645               const Incidence::Ptr &incidence,
646               const QString &status,
647               iTIPMethod method = iTIPReply,
648               const QString &receiver = QString(),
649               const QString &to = QString(),
650               MailType type = Answer) const
651     {
652         // status is accepted/tentative/declined
653         ICalFormat format;
654         format.setTimeZone(QTimeZone::systemTimeZone());
655         QString msg = format.createScheduleMessage(incidence, method);
656         QString summary = incidence->summary();
657         if (summary.isEmpty()) {
658             summary = i18n("Incidence with no summary");
659         }
660         QString subject;
661         switch (type) {
662         case Answer:
663             subject = i18n("Answer: %1", summary);
664             break;
665         case Delegation:
666             subject = i18n("Delegated: %1", summary);
667             break;
668         case Forward:
669             subject = i18n("Forwarded: %1", summary);
670             break;
671         case DeclineCounter:
672             subject = i18n("Declined Counter Proposal: %1", summary);
673             break;
674         }
675 
676         // Set the organizer to the sender, if the ORGANIZER hasn't been set.
677         if (incidence->organizer().isEmpty()) {
678             QString tname;
679             QString temail;
680             KMime::Message::Ptr message = viewerInstance->message();
681             KEmailAddress::extractEmailAddressAndName(message->sender()->asUnicodeString(), temail, tname);
682             incidence->setOrganizer(Person(tname, temail));
683         }
684 
685         QString recv = to;
686         if (recv.isEmpty()) {
687             recv = incidence->organizer().fullName();
688         }
689         return mailICal(receiver, recv, msg, subject, status, type != Forward, viewerInstance);
690     }
691 
saveFile(const QString & receiver,const QString & iCal,const QString & type,MimeTreeParser::Interface::BodyPart * bodyPart) const692     bool saveFile(const QString &receiver, const QString &iCal, const QString &type, MimeTreeParser::Interface::BodyPart *bodyPart) const
693     {
694         auto memento = dynamic_cast<MemoryCalendarMemento *>(bodyPart->memento());
695         // This will block. There's no way to make it async without refactoring the memento mechanism
696 
697         auto itipHandler = new SyncItipHandler(receiver, iCal, type, memento->calendar());
698 
699         // If result is ResultCancelled, then we don't show the message box and return false so kmail
700         // doesn't delete the e-mail.
701         qCDebug(TEXT_CALENDAR_LOG) << "ITIPHandler result was " << itipHandler->result();
702         const Akonadi::ITIPHandler::Result res = itipHandler->result();
703         if (res == Akonadi::ITIPHandler::ResultError) {
704             const QString errorMessage = itipHandler->errorMessage();
705             if (!errorMessage.isEmpty()) {
706                 qCCritical(TEXT_CALENDAR_LOG) << "Error while processing invitation: " << errorMessage;
707                 KMessageBox::error(nullptr, errorMessage);
708             }
709             return false;
710         }
711 
712         return res;
713     }
714 
cancelPastInvites(const Incidence::Ptr incidence,const QString & path) const715     bool cancelPastInvites(const Incidence::Ptr incidence, const QString &path) const
716     {
717         QString warnStr;
718         QDateTime now = QDateTime::currentDateTime();
719         QDate today = now.date();
720         Incidence::IncidenceType type = Incidence::TypeUnknown;
721         const bool occurred = occurredAlready(incidence);
722         if (incidence->type() == Incidence::TypeEvent) {
723             type = Incidence::TypeEvent;
724             Event::Ptr event = incidence.staticCast<Event>();
725             if (!event->allDay()) {
726                 if (occurred) {
727                     warnStr = i18n("\"%1\" occurred already.", event->summary());
728                 } else if (event->dtStart() <= now && now <= event->dtEnd()) {
729                     warnStr = i18n("\"%1\" is currently in-progress.", event->summary());
730                 }
731             } else {
732                 if (occurred) {
733                     warnStr = i18n("\"%1\" occurred already.", event->summary());
734                 } else if (event->dtStart().date() <= today && today <= event->dtEnd().date()) {
735                     warnStr = i18n("\"%1\", happening all day today, is currently in-progress.", event->summary());
736                 }
737             }
738         } else if (incidence->type() == Incidence::TypeTodo) {
739             type = Incidence::TypeTodo;
740             Todo::Ptr todo = incidence.staticCast<Todo>();
741             if (!todo->allDay()) {
742                 if (todo->hasDueDate()) {
743                     if (todo->dtDue() < now) {
744                         warnStr = i18n("\"%1\" is past due.", todo->summary());
745                     } else if (todo->hasStartDate() && todo->dtStart() <= now && now <= todo->dtDue()) {
746                         warnStr = i18n("\"%1\" is currently in-progress.", todo->summary());
747                     }
748                 } else if (todo->hasStartDate()) {
749                     if (todo->dtStart() < now) {
750                         warnStr = i18n("\"%1\" has already started.", todo->summary());
751                     }
752                 }
753             } else {
754                 if (todo->hasDueDate()) {
755                     if (todo->dtDue().date() < today) {
756                         warnStr = i18n("\"%1\" is past due.", todo->summary());
757                     } else if (todo->hasStartDate() && todo->dtStart().date() <= today && today <= todo->dtDue().date()) {
758                         warnStr = i18n("\"%1\", happening all-day today, is currently in-progress.", todo->summary());
759                     }
760                 } else if (todo->hasStartDate()) {
761                     if (todo->dtStart().date() < today) {
762                         warnStr = i18n("\"%1\", happening all day, has already started.", todo->summary());
763                     }
764                 }
765             }
766         }
767 
768         if (!warnStr.isEmpty()) {
769             QString queryStr;
770             KGuiItem yesItem;
771             KGuiItem noItem;
772             if (path == QLatin1String("accept")) {
773                 if (type == Incidence::TypeTodo) {
774                     queryStr = i18n("Do you still want to accept the task?");
775                 } else {
776                     queryStr = i18n("Do you still want to accept the invitation?");
777                 }
778                 yesItem.setText(i18nc("@action:button", "Accept"));
779                 yesItem.setIconName(QStringLiteral("dialog-ok"));
780             } else if (path == QLatin1String("accept_conditionally")) {
781                 if (type == Incidence::TypeTodo) {
782                     queryStr = i18n("Do you still want to send conditional acceptance of the invitation?");
783                 } else {
784                     queryStr = i18n("Do you still want to send conditional acceptance of the task?");
785                 }
786                 yesItem.setText(i18nc("@action:button", "Send"));
787                 yesItem.setIconName(QStringLiteral("mail-send"));
788             } else if (path == QLatin1String("accept_counter")) {
789                 queryStr = i18n("Do you still want to accept the counter proposal?");
790                 yesItem.setText(i18nc("@action:button", "Accept"));
791                 yesItem.setIconName(QStringLiteral("dialog-ok"));
792             } else if (path == QLatin1String("counter")) {
793                 queryStr = i18n("Do you still want to send a counter proposal?");
794                 yesItem.setText(i18nc("@action:button", "Send"));
795                 yesItem.setIconName(QStringLiteral("mail-send"));
796             } else if (path == QLatin1String("decline")) {
797                 queryStr = i18n("Do you still want to send a decline response?");
798                 yesItem.setText(i18nc("@action:button", "Send"));
799                 yesItem.setIconName(QStringLiteral("mail-send"));
800             } else if (path == QLatin1String("decline_counter")) {
801                 queryStr = i18n("Do you still want to decline the counter proposal?");
802                 yesItem.setText(i18nc("@action:button", "Decline"));
803             } else if (path == QLatin1String("reply")) {
804                 queryStr = i18n("Do you still want to record this response in your calendar?");
805                 yesItem.setText(i18nc("@action:button", "Record"));
806             } else if (path == QLatin1String("delegate")) {
807                 if (type == Incidence::TypeTodo) {
808                     queryStr = i18n("Do you still want to delegate this task?");
809                 } else {
810                     queryStr = i18n("Do you still want to delegate this invitation?");
811                 }
812                 yesItem.setText(i18nc("@action:button", "Delegate"));
813             } else if (path == QLatin1String("forward")) {
814                 if (type == Incidence::TypeTodo) {
815                     queryStr = i18n("Do you still want to forward this task?");
816                 } else {
817                     queryStr = i18n("Do you still want to forward this invitation?");
818                 }
819                 yesItem.setText(i18nc("@action:button", "Forward"));
820                 yesItem.setIconName(QStringLiteral("mail-forward"));
821             } else if (path == QLatin1String("cancel")) {
822                 if (type == Incidence::TypeTodo) {
823                     queryStr = i18n("Do you still want to cancel this task?");
824                     yesItem.setText(i18nc("@action:button", "Cancel Task"));
825                 } else {
826                     queryStr = i18n("Do you still want to cancel this invitation?");
827                     yesItem.setText(i18nc("@action:button", "Cancel Invitation"));
828                 }
829                 yesItem.setIconName(QStringLiteral("dialog-ok"));
830                 noItem.setText(i18nc("@action:button", "Do Not Cancel"));
831                 noItem.setIconName(QStringLiteral("dialog-cancel"));
832             } else if (path == QLatin1String("check_calendar")) {
833                 queryStr = i18n("Do you still want to check your calendar?");
834                 yesItem.setText(i18nc("@action:button", "Check"));
835             } else if (path == QLatin1String("record")) {
836                 if (type == Incidence::TypeTodo) {
837                     queryStr = i18n("Do you still want to record this task in your calendar?");
838                 } else {
839                     queryStr = i18n("Do you still want to record this invitation in your calendar?");
840                 }
841                 yesItem.setText(i18nc("@action:button", "Record"));
842             } else if (path == QLatin1String("cancel")) {
843                 if (type == Incidence::TypeTodo) {
844                     queryStr = i18n("Do you really want to cancel this task?");
845                     yesItem.setText(i18nc("@action:button", "Cancel Task"));
846                 } else {
847                     queryStr = i18n("Do you really want to cancel this invitation?");
848                     yesItem.setText(i18nc("@action:button", "Cancel Invitation"));
849                 }
850                 yesItem.setIconName(QStringLiteral("dialog-ok"));
851                 noItem.setText(i18nc("@action:button", "Do Not Cancel"));
852                 noItem.setIconName(QStringLiteral("dialog-cancel"));
853             } else if (path.startsWith(QLatin1String("ATTACH:"))) {
854                 return false;
855             } else {
856                 queryStr = i18n("%1?", path);
857                 yesItem = KStandardGuiItem::yes();
858             }
859 
860             if (noItem.text().isEmpty()) {
861                 noItem = KStandardGuiItem::cancel();
862             }
863             const int answer = KMessageBox::warningYesNo(nullptr, i18n("%1\n%2", warnStr, queryStr), QString(), yesItem, noItem);
864             if (answer == KMessageBox::No) {
865                 return true;
866             }
867         }
868         return false;
869     }
870 
handleInvitation(const QString & iCal,Attendee::PartStat status,MimeTreeParser::Interface::BodyPart * part,Viewer * viewerInstance) const871     bool handleInvitation(const QString &iCal, Attendee::PartStat status, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
872     {
873         bool ok = true;
874         const QString receiver = findReceiver(part->content());
875         qCDebug(TEXT_CALENDAR_LOG) << receiver;
876 
877         if (receiver.isEmpty()) {
878             // Must be some error. Still return true though, since we did handle it
879             return true;
880         }
881 
882         Incidence::Ptr incidence = stringToIncidence(iCal);
883         qCDebug(TEXT_CALENDAR_LOG) << "Handling invitation: uid is : " << incidence->uid() << "; schedulingId is:" << incidence->schedulingID()
884                                    << "; Attendee::PartStat = " << status;
885 
886         // get comment for tentative acceptance
887         if (askForComment(status)) {
888             QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
889             dlg->setWindowTitle(i18nc("@title:window", "Reaction to Invitation"));
890             QString comment;
891             if (dlg->exec()) {
892                 comment = dlg->comment();
893                 delete dlg;
894             } else {
895                 delete dlg;
896                 return true;
897             }
898 
899             if (comment.trimmed().isEmpty()) {
900                 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks"));
901                 return true;
902             } else {
903                 incidence->addComment(comment);
904             }
905         }
906 
907         // First, save it for KOrganizer to handle
908         const QString dir = directoryForStatus(status);
909         if (dir.isEmpty()) {
910             qCWarning(TEXT_CALENDAR_LOG) << "Impossible to understand status: " << status;
911             return true; // unknown status
912         }
913         if (status != Attendee::Delegated) {
914             // we do that below for delegated incidences
915             if (!saveFile(receiver, iCal, dir, part)) {
916                 return false;
917             }
918         }
919 
920         QString delegateString;
921         bool delegatorRSVP = false;
922         if (status == Attendee::Delegated) {
923             DelegateSelector dlg;
924             if (dlg.exec() == QDialog::Rejected) {
925                 return true;
926             }
927             delegateString = dlg.delegate();
928             delegatorRSVP = dlg.rsvp();
929             if (delegateString.isEmpty()) {
930                 return true;
931             }
932             if (KEmailAddress::compareEmail(delegateString, incidence->organizer().email(), false)) {
933                 KMessageBox::sorry(nullptr, i18n("Delegation to organizer is not possible."));
934                 return true;
935             }
936         }
937 
938         if (!incidence) {
939             return false;
940         }
941 
942         const Attendee myself = findMyself(incidence, receiver);
943 
944         // find our delegator, we need to inform him as well
945         QString delegator;
946         if (status != Attendee::NeedsAction && !myself.isNull() && !myself.delegator().isEmpty()) {
947             const Attendee::List attendees = incidence->attendees();
948             Attendee::List::ConstIterator end = attendees.constEnd();
949             for (Attendee::List::ConstIterator it = attendees.constBegin(); it != end; ++it) {
950                 if (KEmailAddress::compareEmail((*it).fullName(), myself.delegator(), false) && (*it).status() == Attendee::Delegated) {
951                     delegator = (*it).fullName();
952                     delegatorRSVP = (*it).RSVP();
953                     break;
954                 }
955             }
956         }
957 
958         if (status != Attendee::NeedsAction
959             && ((!myself.isNull() && (myself.RSVP() || myself.status() == Attendee::NeedsAction)) || heuristicalRSVP(incidence))) {
960             Attendee newMyself = setStatusOnMyself(incidence, myself, status, receiver);
961             if (!newMyself.isNull() && status == Attendee::Delegated) {
962                 newMyself.setDelegate(delegateString);
963                 newMyself.setRSVP(delegatorRSVP);
964             }
965             ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
966 
967             // check if we need to inform our delegator about this as well
968             if (!newMyself.isNull() && (status == Attendee::Accepted || status == Attendee::Declined) && !delegator.isEmpty()) {
969                 if (delegatorRSVP || status == Attendee::Declined) {
970                     ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver, delegator);
971                 }
972             }
973         } else if (myself.isNull() && (status != Attendee::Declined && status != Attendee::NeedsAction)) {
974             // forwarded invitation
975             QString name;
976             QString email;
977             KEmailAddress::extractEmailAddressAndName(receiver, email, name);
978             if (!email.isEmpty()) {
979                 Attendee newMyself(name,
980                                    email,
981                                    true, // RSVP, otherwise we would not be here
982                                    status,
983                                    heuristicalRole(incidence),
984                                    QString());
985                 incidence->clearAttendees();
986                 incidence->addAttendee(newMyself);
987                 ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
988             }
989         } else {
990             if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
991                 viewerInstance->deleteMessage();
992             }
993         }
994 
995         // create invitation for the delegate (same as the original invitation
996         // with the delegate as additional attendee), we also use that for updating
997         // our calendar
998         if (status == Attendee::Delegated) {
999             incidence = stringToIncidence(iCal);
1000             auto attendees = incidence->attendees();
1001             const int myselfIdx = findMyself(attendees, receiver);
1002             if (myselfIdx >= 0) {
1003                 attendees[myselfIdx].setStatus(status);
1004                 attendees[myselfIdx].setDelegate(delegateString);
1005                 incidence->setAttendees(attendees);
1006             }
1007             QString name;
1008             QString email;
1009             KEmailAddress::extractEmailAddressAndName(delegateString, email, name);
1010             Attendee delegate(name, email, true);
1011             delegate.setDelegator(receiver);
1012             incidence->addAttendee(delegate);
1013 
1014             ICalFormat format;
1015             format.setTimeZone(QTimeZone::systemTimeZone());
1016             const QString iCal = format.createScheduleMessage(incidence, iTIPRequest);
1017             if (!saveFile(receiver, iCal, dir, part)) {
1018                 return false;
1019             }
1020 
1021             ok = mail(viewerInstance, incidence, dir, iTIPRequest, receiver, delegateString, Delegation);
1022         }
1023         return ok;
1024     }
1025 
openAttachment(const QString & name,const QString & iCal) const1026     void openAttachment(const QString &name, const QString &iCal) const
1027     {
1028         Attachment attachment(findAttachment(name, iCal));
1029         if (attachment.isEmpty()) {
1030             return;
1031         }
1032 
1033         if (attachment.isUri()) {
1034             QDesktopServices::openUrl(QUrl(attachment.uri()));
1035         } else {
1036             // put the attachment in a temporary file and launch it
1037             QTemporaryFile *file = nullptr;
1038             QMimeDatabase db;
1039             QStringList patterns = db.mimeTypeForName(attachment.mimeType()).globPatterns();
1040             if (!patterns.empty()) {
1041                 QString pattern = patterns.at(0);
1042                 file = new QTemporaryFile(QDir::tempPath() + QLatin1String("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
1043             } else {
1044                 file = new QTemporaryFile();
1045             }
1046             file->setAutoRemove(false);
1047             file->open();
1048             file->setPermissions(QFile::ReadUser);
1049             file->write(QByteArray::fromBase64(attachment.data()));
1050             file->close();
1051 
1052             auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file->fileName()), attachment.mimeType());
1053             job->setDeleteTemporaryFile(true);
1054             job->start();
1055             delete file;
1056         }
1057     }
1058 
saveAsAttachment(const QString & name,const QString & iCal) const1059     bool saveAsAttachment(const QString &name, const QString &iCal) const
1060     {
1061         Attachment a(findAttachment(name, iCal));
1062         if (a.isEmpty()) {
1063             return false;
1064         }
1065 
1066         // get the saveas file name
1067         const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18n("Save Invitation Attachment"), name, QString());
1068 
1069         if (saveAsFile.isEmpty()) {
1070             return false;
1071         }
1072 
1073         bool stat = false;
1074         if (a.isUri()) {
1075             // save the attachment url
1076             auto job = KIO::file_copy(QUrl(a.uri()), QUrl::fromLocalFile(saveAsFile));
1077             stat = job->exec();
1078         } else {
1079             // put the attachment in a temporary file and save it
1080             QTemporaryFile *file{nullptr};
1081             QMimeDatabase db;
1082             QStringList patterns = db.mimeTypeForName(a.mimeType()).globPatterns();
1083             if (!patterns.empty()) {
1084                 QString pattern = patterns.at(0);
1085                 file = new QTemporaryFile(QDir::tempPath() + QLatin1String("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
1086             } else {
1087                 file = new QTemporaryFile();
1088             }
1089             file->setAutoRemove(false);
1090             file->open();
1091             file->setPermissions(QFile::ReadUser);
1092             file->write(QByteArray::fromBase64(a.data()));
1093             file->close();
1094             const QString filename = file->fileName();
1095             delete file;
1096 
1097             auto job = KIO::file_copy(QUrl::fromLocalFile(filename), QUrl::fromLocalFile(saveAsFile));
1098             stat = job->exec();
1099         }
1100         return stat;
1101     }
1102 
showCalendar(QDate date) const1103     void showCalendar(QDate date) const
1104     {
1105         // If korganizer or kontact is running, bring it to the front. Otherwise start korganizer.
1106         if (KontactInterface::PimUniqueApplication::activateApplication(QStringLiteral("korganizer"))) {
1107             OrgKdeKorganizerCalendarInterface iface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QDBusConnection::sessionBus(), nullptr);
1108             if (!iface.isValid()) {
1109                 qCDebug(TEXT_CALENDAR_LOG) << "Calendar interface is not valid! " << iface.lastError().message();
1110                 return;
1111             }
1112             iface.showEventView();
1113             iface.showDate(date);
1114         }
1115     }
1116 
handleIgnore(Viewer * viewerInstance) const1117     bool handleIgnore(Viewer *viewerInstance) const
1118     {
1119         // simply move the message to trash
1120         viewerInstance->deleteMessage();
1121         return true;
1122     }
1123 
handleDeclineCounter(const QString & iCal,MimeTreeParser::Interface::BodyPart * part,Viewer * viewerInstance) const1124     bool handleDeclineCounter(const QString &iCal, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
1125     {
1126         const QString receiver(findReceiver(part->content()));
1127         if (receiver.isEmpty()) {
1128             return true;
1129         }
1130         Incidence::Ptr incidence(stringToIncidence(iCal));
1131         if (askForComment(Attendee::Declined)) {
1132             QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
1133             dlg->setWindowTitle(i18nc("@title:window", "Decline Counter Proposal"));
1134             QString comment;
1135             if (dlg->exec()) {
1136                 comment = dlg->comment();
1137                 delete dlg;
1138             } else {
1139                 delete dlg;
1140                 return true;
1141             }
1142 
1143             if (comment.trimmed().isEmpty()) {
1144                 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks"));
1145                 return true;
1146             } else {
1147                 incidence->addComment(comment);
1148             }
1149         }
1150         return mail(viewerInstance, incidence, QStringLiteral("declinecounter"), KCalendarCore::iTIPDeclineCounter, receiver, QString(), DeclineCounter);
1151     }
1152 
counterProposal(const QString & iCal,MimeTreeParser::Interface::BodyPart * part) const1153     bool counterProposal(const QString &iCal, MimeTreeParser::Interface::BodyPart *part) const
1154     {
1155         const QString receiver = findReceiver(part->content());
1156         if (receiver.isEmpty()) {
1157             return true;
1158         }
1159 
1160         // Don't delete the invitation here in any case, if the counter proposal
1161         // is declined you might need it again.
1162         return saveFile(receiver, iCal, QStringLiteral("counter"), part);
1163     }
1164 
handleClick(Viewer * viewerInstance,MimeTreeParser::Interface::BodyPart * part,const QString & path) const1165     bool handleClick(Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const override
1166     {
1167         // filter out known paths that don't belong to this type of urlmanager.
1168         // kolab/issue4054 msg27201
1169         if (path.contains(QLatin1String("addToAddressBook:")) || path.contains(QLatin1String("updateToAddressBook"))) {
1170             return false;
1171         }
1172 
1173         if (!hasMyWritableEventsFolders(QStringLiteral("calendar"))) {
1174             KMessageBox::error(nullptr,
1175                                i18n("You have no writable calendar folders for invitations, "
1176                                     "so storing or saving a response will not be possible.\n"
1177                                     "Please create at least 1 writable events calendar and re-sync."));
1178             return false;
1179         }
1180 
1181         // If the bodypart does not have a charset specified, we need to fall back to utf8,
1182         // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
1183         QString iCal;
1184         if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) {
1185             const QByteArray &ba = part->content()->decodedContent();
1186             iCal = QString::fromUtf8(ba);
1187         } else {
1188             iCal = part->content()->decodedText();
1189         }
1190 
1191         Incidence::Ptr incidence = stringToIncidence(iCal);
1192         if (!incidence) {
1193             KMessageBox::sorry(nullptr,
1194                                i18n("The calendar invitation stored in this email message is broken in some way. "
1195                                     "Unable to continue."));
1196             return false;
1197         }
1198 
1199         bool result = false;
1200         if (cancelPastInvites(incidence, path)) {
1201             return result;
1202         }
1203 
1204         if (path == QLatin1String("accept")) {
1205             result = handleInvitation(iCal, Attendee::Accepted, part, viewerInstance);
1206         } else if (path == QLatin1String("accept_conditionally")) {
1207             result = handleInvitation(iCal, Attendee::Tentative, part, viewerInstance);
1208         } else if (path == QLatin1String("counter")) {
1209             result = counterProposal(iCal, part);
1210         } else if (path == QLatin1String("ignore")) {
1211             result = handleIgnore(viewerInstance);
1212         } else if (path == QLatin1String("decline")) {
1213             result = handleInvitation(iCal, Attendee::Declined, part, viewerInstance);
1214         } else if (path == QLatin1String("decline_counter")) {
1215             result = handleDeclineCounter(iCal, part, viewerInstance);
1216         } else if (path == QLatin1String("postpone")) {
1217             result = handleInvitation(iCal, Attendee::NeedsAction, part, viewerInstance);
1218         } else if (path == QLatin1String("delegate")) {
1219             result = handleInvitation(iCal, Attendee::Delegated, part, viewerInstance);
1220         } else if (path == QLatin1String("forward")) {
1221             AttendeeSelector dlg;
1222             if (dlg.exec() == QDialog::Rejected) {
1223                 return true;
1224             }
1225             QString fwdTo = dlg.attendees().join(QLatin1String(", "));
1226             if (fwdTo.isEmpty()) {
1227                 return true;
1228             }
1229             const QString receiver = findReceiver(part->content());
1230             result = mail(viewerInstance, incidence, QStringLiteral("forward"), iTIPRequest, receiver, fwdTo, Forward);
1231         } else if (path == QLatin1String("check_calendar")) {
1232             incidence = stringToIncidence(iCal);
1233             showCalendar(incidence->dtStart().date());
1234             return true;
1235         } else if (path == QLatin1String("reply") || path == QLatin1String("cancel") || path == QLatin1String("accept_counter")) {
1236             // These should just be saved with their type as the dir
1237             const QString p = (path == QLatin1String("accept_counter") ? QStringLiteral("reply") : path);
1238             if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, p, part)) {
1239                 if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
1240                     viewerInstance->deleteMessage();
1241                 }
1242                 result = true;
1243             }
1244         } else if (path == QLatin1String("record")) {
1245             incidence = stringToIncidence(iCal);
1246             QString summary;
1247             int response = KMessageBox::questionYesNoCancel(nullptr,
1248                                                             i18nc("@info",
1249                                                                   "The organizer is not expecting a reply to this invitation "
1250                                                                   "but you can send them an email message if you desire.\n\n"
1251                                                                   "Would you like to send the organizer a message regarding this invitation?\n"
1252                                                                   "Press the [Cancel] button to cancel the recording operation."),
1253                                                             i18nc("@title:window", "Send Email to Organizer"),
1254                                                             KGuiItem(i18n("Do Not Send")),
1255                                                             KGuiItem(i18n("Send EMail")));
1256 
1257             switch (response) {
1258             case KMessageBox::Cancel:
1259                 break;
1260             case KMessageBox::No: { // means "send email"
1261                 summary = incidence->summary();
1262                 if (!summary.isEmpty()) {
1263                     summary = i18n("Re: %1", summary);
1264                 }
1265 
1266                 QUrlQuery query;
1267                 query.addQueryItem(QStringLiteral("to"), incidence->organizer().email());
1268                 query.addQueryItem(QStringLiteral("subject"), summary);
1269                 QUrl url;
1270                 url.setScheme(QStringLiteral("mailto"));
1271                 url.setQuery(query);
1272                 QDesktopServices::openUrl(url);
1273             }
1274             // fall through
1275             case KMessageBox::Yes: // means "do not send"
1276                 if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, QStringLiteral("reply"), part)) {
1277                     if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
1278                         viewerInstance->deleteMessage();
1279                         result = true;
1280                     }
1281                 }
1282                 showCalendar(incidence->dtStart().date());
1283                 break;
1284             }
1285         } else if (path == QLatin1String("delete")) {
1286             viewerInstance->deleteMessage();
1287             result = true;
1288         }
1289 
1290         if (path.startsWith(QLatin1String("ATTACH:"))) {
1291             const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1292             openAttachment(name, iCal);
1293         }
1294 
1295         if (result) {
1296             // do not close the secondary window if an attachment was opened (kolab/issue4317)
1297             if (!path.startsWith(QLatin1String("ATTACH:"))) {
1298                 qCDebug(TEXT_CALENDAR_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO << "about closing if in a secondary window";
1299 #if 0 // TODO port to Akonadi
1300                 c.closeIfSecondaryWindow();
1301 #endif
1302             }
1303         }
1304         return result;
1305     }
1306 
handleContextMenuRequest(MimeTreeParser::Interface::BodyPart * part,const QString & path,const QPoint & point) const1307     bool handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &point) const override
1308     {
1309         QString name = path;
1310         if (path.startsWith(QLatin1String("ATTACH:"))) {
1311             name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1312         } else {
1313             return false; // because it isn't an attachment invitation
1314         }
1315 
1316         QString iCal;
1317         if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) {
1318             const QByteArray &ba = part->content()->decodedContent();
1319             iCal = QString::fromUtf8(ba);
1320         } else {
1321             iCal = part->content()->decodedText();
1322         }
1323 
1324         auto menu = new QMenu();
1325         QAction *open = menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Attachment"));
1326         QAction *saveas = menu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Attachment As..."));
1327 
1328         QAction *a = menu->exec(point, nullptr);
1329         if (a == open) {
1330             openAttachment(name, iCal);
1331         } else if (a == saveas) {
1332             saveAsAttachment(name, iCal);
1333         }
1334         delete menu;
1335         return true;
1336     }
1337 
statusBarMessage(MimeTreeParser::Interface::BodyPart *,const QString & path) const1338     QString statusBarMessage(MimeTreeParser::Interface::BodyPart *, const QString &path) const override
1339     {
1340         if (!path.isEmpty()) {
1341             if (path == QLatin1String("accept")) {
1342                 return i18n("Accept invitation");
1343             } else if (path == QLatin1String("accept_conditionally")) {
1344                 return i18n("Accept invitation conditionally");
1345             } else if (path == QLatin1String("accept_counter")) {
1346                 return i18n("Accept counter proposal");
1347             } else if (path == QLatin1String("counter")) {
1348                 return i18n("Create a counter proposal...");
1349             } else if (path == QLatin1String("ignore")) {
1350                 return i18n("Throw mail away");
1351             } else if (path == QLatin1String("decline")) {
1352                 return i18n("Decline invitation");
1353             } else if (path == QLatin1String("postpone")) {
1354                 return i18n("Postpone");
1355             } else if (path == QLatin1String("decline_counter")) {
1356                 return i18n("Decline counter proposal");
1357             } else if (path == QLatin1String("check_calendar")) {
1358                 return i18n("Check my calendar...");
1359             } else if (path == QLatin1String("reply")) {
1360                 return i18n("Record response into my calendar");
1361             } else if (path == QLatin1String("record")) {
1362                 return i18n("Record invitation into my calendar");
1363             } else if (path == QLatin1String("delete")) {
1364                 return i18n("Move this invitation to my trash folder");
1365             } else if (path == QLatin1String("delegate")) {
1366                 return i18n("Delegate invitation");
1367             } else if (path == QLatin1String("forward")) {
1368                 return i18n("Forward invitation");
1369             } else if (path == QLatin1String("cancel")) {
1370                 return i18n("Remove invitation from my calendar");
1371             } else if (path.startsWith(QLatin1String("ATTACH:"))) {
1372                 const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8()));
1373                 return i18n("Open attachment \"%1\"", name);
1374             }
1375         }
1376 
1377         return QString();
1378     }
1379 
askForComment(Attendee::PartStat status) const1380     bool askForComment(Attendee::PartStat status) const
1381     {
1382         if (status != Attendee::NeedsAction
1383             && ((status != Attendee::Accepted
1384                  && MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation()
1385                      == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AskForAllButAcceptance)
1386                 || (MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation()
1387                     == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AlwaysAsk))) {
1388             return true;
1389         }
1390         return false;
1391     }
1392 };
1393 
1394 class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin
1395 {
1396     Q_OBJECT
1397     Q_INTERFACES(MessageViewer::MessagePartRenderPlugin)
1398     Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "text_calendar.json")
1399 public:
renderer(int idx)1400     MessageViewer::MessagePartRendererBase *renderer(int idx) override
1401     {
1402         if (idx < 2) {
1403             return new Formatter();
1404         } else {
1405             return nullptr;
1406         }
1407     }
1408 
urlHandler(int idx) const1409     const MessageViewer::Interface::BodyPartURLHandler *urlHandler(int idx) const override
1410     {
1411         if (idx == 0) {
1412             return new UrlHandler();
1413         } else {
1414             return nullptr;
1415         }
1416     }
1417 };
1418 }
1419 
1420 #include "text_calendar.moc"
1421