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