text_calendar.cpp 59.1 KB
Newer Older
Laurent Montel's avatar
Laurent Montel committed
1
2
3
4
5
6
/*
  This file is part of kdepim.

  Copyright (c) 2004 Cornelius Schumacher <schumacher@kde.org>
  Copyright (c) 2007 Volker Krause <vkrause@kde.org>
  Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
Laurent Montel's avatar
Laurent Montel committed
7
  Copyright (C) 2017-2020 Laurent Montel <montel@kde.org>
Laurent Montel's avatar
Laurent Montel committed
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

  In addition, as a special exception, the copyright holders give
  permission to link the code of this program with any edition of
  the Qt library by Trolltech AS, Norway (or with modified versions
  of Qt that use the same license as Qt), and distribute linked
  combinations including the two.  You must obey the GNU General
  Public License in all respects for all of the code used other than
  Qt.  If you modify this file, you may extend this exception to
  your version of the file, but you are not obligated to do so.  If
  you do not wish to do so, delete this exception statement from
  your version.
*/
#include "attendeeselector.h"
#include "calendarinterface.h"
#include "delegateselector.h"
#include "memorycalendarmemento.h"
#include "syncitiphandler.h"
39
#include "reactiontoinvitationdialog.h"
Laurent Montel's avatar
Laurent Montel committed
40

Laurent Montel's avatar
Laurent Montel committed
41
42
#include <KIdentityManagement/IdentityManager>

Laurent Montel's avatar
Laurent Montel committed
43
#include <MessageViewer/BodyPartURLHandler>
44
#include <MessageViewer/MessagePartRendererBase>
45
#include <MessageViewer/MessagePartRenderPlugin>
Laurent Montel's avatar
Laurent Montel committed
46
47
#include <MessageViewer/MessageViewerSettings>
#include <MessageViewer/Viewer>
48
#include <MessageViewer/HtmlWriter>
Laurent Montel's avatar
Laurent Montel committed
49
#include <MimeTreeParser/BodyPart>
50
#include <MimeTreeParser/MessagePart>
Laurent Montel's avatar
Laurent Montel committed
51
52
using namespace MessageViewer;

53
54
#include <KCalendarCore/ICalFormat>
using namespace KCalendarCore;
Laurent Montel's avatar
Laurent Montel committed
55
56
57
58
59
60
61
62
63

#include <KCalUtils/IncidenceFormatter>

#include <KMime/Message>

#include <KIdentityManagement/Identity>

#include <KEmailAddress>

Laurent Montel's avatar
Laurent Montel committed
64
#include <MailTransportAkonadi/MessageQueueJob>
Laurent Montel's avatar
Laurent Montel committed
65
66
#include <MailTransport/TransportManager>

67
68
#include <KontactInterface/PimUniqueApplication>

Laurent Montel's avatar
Laurent Montel committed
69
70
71
#include "text_calendar_debug.h"

#include <KMessageBox>
72
73
#include <KIO/JobUiDelegate>
#include <KIO/OpenUrlJob>
Laurent Montel's avatar
Laurent Montel committed
74
75
76
77
78
79
#include <KIO/FileCopyJob>
#include <KIO/StatJob>
#include <KLocalizedString>

#include <QEventLoop>
#include <QDBusServiceWatcher>
80
#include <QMimeDatabase>
Laurent Montel's avatar
Laurent Montel committed
81
82
83
84
85
86
87
88
#include <QProcess>
#include <QUrl>
#include <QTemporaryFile>
#include <QIcon>
#include <QFileDialog>
#include <QDesktopServices>
#include <QInputDialog>
#include <QMenu>
89
#include <QPointer>
Laurent Montel's avatar
Laurent Montel committed
90
91
92

using namespace MailTransport;

Laurent Montel's avatar
Laurent Montel committed
93
namespace {
Laurent Montel's avatar
Laurent Montel committed
94
95
static bool hasMyWritableEventsFolders(const QString &family)
{
Laurent Montel's avatar
Laurent Montel committed
96
97
    Q_UNUSED(family)
#if 0 // TODO port to Akonadi
Laurent Montel's avatar
Laurent Montel committed
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
    QString myfamily = family;
    if (family.isEmpty()) {
        myfamily = QStringLiteral("calendar");
    }

#ifndef KDEPIM_NO_KRESOURCES
    CalendarResourceManager manager(myfamily);
    manager.readConfig();

    CalendarResourceManager::ActiveIterator it;
    for (it = manager.activeBegin(); it != manager.activeEnd(); ++it) {
        if ((*it)->readOnly()) {
            continue;
        }

        const QStringList subResources = (*it)->subresources();
        if (subResources.isEmpty()) {
            return true;
        }

        QStringList::ConstIterator subIt;
        for (subIt = subResources.begin(); subIt != subResources.end(); ++subIt) {
            if (!(*it)->subresourceActive((*subIt))) {
                continue;
            }
            if ((*it)->type() == "imap" || (*it)->type() == "kolab") {
Laurent Montel's avatar
Laurent Montel committed
124
125
126
                if ((*it)->subresourceType((*subIt)) == "todo"
                    || (*it)->subresourceType((*subIt)) == "journal"
                    || !(*subIt).contains("/.INBOX.directory/")) {
Laurent Montel's avatar
Laurent Montel committed
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
                    continue;
                }
            }
            return true;
        }
    }
    return false;
#endif
#else
    qCDebug(TEXT_CALENDAR_LOG) << "Disabled code, port to Akonadi";
    return true;
#endif
}

static bool occurredAlready(const Incidence::Ptr &incidence)
{
    Q_ASSERT(incidence);
144
    const QDateTime now = QDateTime::currentDateTime();
Laurent Montel's avatar
Laurent Montel committed
145
146
147
    const QDate today = now.date();

    if (incidence->recurs()) {
148
        const QDateTime nextDate = incidence->recurrence()->getNextDateTime(now);
Laurent Montel's avatar
Laurent Montel committed
149
150
151

        return !nextDate.isValid();
    } else {
152
        const QDateTime incidenceDate = incidence->dateTime(Incidence::RoleDisplayEnd);
Laurent Montel's avatar
Laurent Montel committed
153
        if (incidenceDate.isValid()) {
154
            return incidence->allDay() ? (incidenceDate.date() < today) : (incidenceDate < QDateTime::currentDateTime());
Laurent Montel's avatar
Laurent Montel committed
155
156
157
158
159
160
161
162
163
        }
    }

    return false;
}

class KMInvitationFormatterHelper : public KCalUtils::InvitationFormatterHelper
{
public:
164
    KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr &bodyPart, const KCalendarCore::MemoryCalendar::Ptr &calendar)
Laurent Montel's avatar
Laurent Montel committed
165
166
167
168
169
        : mBodyPart(bodyPart)
        , mCalendar(calendar)
    {
    }

170
    QString generateLinkURL(const QString &id) override
Laurent Montel's avatar
Laurent Montel committed
171
    {
Laurent Montel's avatar
Laurent Montel committed
172
173
174
        return mBodyPart->makeLink(id);
    }

175
    KCalendarCore::Calendar::Ptr calendar() const override
Laurent Montel's avatar
Laurent Montel committed
176
177
178
    {
        return mCalendar;
    }
Laurent Montel's avatar
Laurent Montel committed
179

Laurent Montel's avatar
Laurent Montel committed
180
private:
181
    MimeTreeParser::MessagePartPtr mBodyPart;
182
    KCalendarCore::MemoryCalendar::Ptr mCalendar;
Laurent Montel's avatar
Laurent Montel committed
183
184
};

185
class Formatter : public MessageViewer::MessagePartRendererBase
Laurent Montel's avatar
Laurent Montel committed
186
187
{
public:
188
    bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override
Laurent Montel's avatar
Laurent Montel committed
189
    {
190
191
        QMimeDatabase db;
        auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower()));
Laurent Montel's avatar
Laurent Montel committed
192
        if (!mt.isValid() || mt.name() != QLatin1String("text/calendar")) {
193
            return false;
Laurent Montel's avatar
Laurent Montel committed
194
        }
195

196
        auto nodeHelper = msgPart->nodeHelper();
197
        if (!nodeHelper) {
198
            return false;
199
200
        }

201
        /** Formatting is async now because we need to fetch incidences from akonadi.
Laurent Montel's avatar
Laurent Montel committed
202
203
204
205
206
207
208
            Basically this method (format()) will be called twice. The first time
            it creates the memento that fetches incidences and returns.

            When the memento finishes, this is called a second time, and we can proceed.

            BodyPartMementos are documented in MessageViewer/ObjectTreeParser
        */
209
        MemoryCalendarMemento *memento = dynamic_cast<MemoryCalendarMemento *>(msgPart->memento());
Laurent Montel's avatar
Laurent Montel committed
210
211

        if (memento) {
212
            KMime::Message *const message = dynamic_cast<KMime::Message *>(msgPart->content()->topLevel());
Laurent Montel's avatar
Laurent Montel committed
213
214
            if (!message) {
                qCWarning(TEXT_CALENDAR_LOG) << "The top-level content is not a message. Cannot handle the invitation then.";
215
                return false;
Laurent Montel's avatar
Laurent Montel committed
216
217
218
            }

            if (memento->finished()) {
219
                KMInvitationFormatterHelper helper(msgPart, memento->calendar());
Laurent Montel's avatar
Laurent Montel committed
220
221
222
                QString source;
                // If the bodypart does not have a charset specified, we need to fall back to utf8,
                // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
223
224
                if (msgPart->content()->contentType()->parameter(QStringLiteral("charset")).isEmpty()) {
                    const QByteArray &ba = msgPart->content()->decodedContent();
Laurent Montel's avatar
Laurent Montel committed
225
226
                    source = QString::fromUtf8(ba);
                } else {
227
                    source = msgPart->text();
Laurent Montel's avatar
Laurent Montel committed
228
229
                }

230
                MemoryCalendar::Ptr cl(new MemoryCalendar(QTimeZone::systemTimeZone()));
Laurent Montel's avatar
Laurent Montel committed
231
232
                const QString html
                    = KCalUtils::IncidenceFormatter::formatICalInvitationNoHtml(
Laurent Montel's avatar
Laurent Montel committed
233
                          source, cl, &helper, message->sender()->asUnicodeString());
Laurent Montel's avatar
Laurent Montel committed
234
235

                if (html.isEmpty()) {
236
                    return false;
Laurent Montel's avatar
Laurent Montel committed
237
                }
238
                writer->write(html);
Laurent Montel's avatar
Laurent Montel committed
239
240
241
            }
        } else {
            MemoryCalendarMemento *memento = new MemoryCalendarMemento();
242
            msgPart->setMemento(memento);
243
244
            QObject::connect(memento, &MemoryCalendarMemento::update,
                             nodeHelper, &MimeTreeParser::NodeHelper::update);
Laurent Montel's avatar
Laurent Montel committed
245
246
        }

247
        return true;
Laurent Montel's avatar
Laurent Montel committed
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
    }
};

static QString directoryForStatus(Attendee::PartStat status)
{
    QString dir;
    switch (status) {
    case Attendee::Accepted:
        dir = QStringLiteral("accepted");
        break;
    case Attendee::Tentative:
        dir = QStringLiteral("tentative");
        break;
    case Attendee::Declined:
        dir = QStringLiteral("cancel");
        break;
    case Attendee::Delegated:
        dir = QStringLiteral("delegated");
        break;
    case Attendee::NeedsAction:
        dir = QStringLiteral("request");
        break;
    default:
        break;
    }
    return dir;
}

static Incidence::Ptr stringToIncidence(const QString &iCal)
{
278
    MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::systemTimeZone()));
Laurent Montel's avatar
Laurent Montel committed
279
280
281
282
283
284
285
286
287
288
289
    ICalFormat format;
    ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, iCal);
    if (!message) {
        //TODO: Error message?
        qCWarning(TEXT_CALENDAR_LOG) << "Can't parse this ical string: "  << iCal;
        return Incidence::Ptr();
    }

    return message->event().dynamicCast<Incidence>();
}

290
class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler
Laurent Montel's avatar
Laurent Montel committed
291
292
293
294
295
296
297
{
public:
    UrlHandler()
    {
        //qCDebug(TEXT_CALENDAR_LOG) << "UrlHandler() (iCalendar)";
    }

Laurent Montel's avatar
Laurent Montel committed
298
299
300
301
    QString name() const override
    {
        return QStringLiteral("calendar handler");
    }
302
303

    Attendee findMyself(const Incidence::Ptr &incidence, const QString &receiver) const
Laurent Montel's avatar
Laurent Montel committed
304
    {
Laurent Montel's avatar
Minor    
Laurent Montel committed
305
        const Attendee::List attendees = incidence->attendees();
306
307
308
309
310
311
312
313
314
        const auto idx = findMyself(attendees, receiver);
        if (idx >= 0) {
            return attendees.at(idx);
        }
        return {};
    }

    int findMyself(const Attendee::List &attendees, const QString &receiver) const
    {
Laurent Montel's avatar
Laurent Montel committed
315
316
        // Find myself. There will always be all attendees listed, even if
        // only I need to answer it.
317
        for (int i = 0; i < attendees.size(); ++i) {
Laurent Montel's avatar
Laurent Montel committed
318
            // match only the email part, not the name
319
            if (KEmailAddress::compareEmail(attendees.at(i).email(), receiver, false)) {
Laurent Montel's avatar
Laurent Montel committed
320
321
                // We are the current one, and even the receiver, note
                // this and quit searching.
322
                return i;
Laurent Montel's avatar
Laurent Montel committed
323
324
            }
        }
325
        return -1;
Laurent Montel's avatar
Laurent Montel committed
326
327
328
329
330
    }

    static bool heuristicalRSVP(const Incidence::Ptr &incidence)
    {
        bool rsvp = true; // better send superfluously than not at all
Laurent Montel's avatar
Minor    
Laurent Montel committed
331
        const Attendee::List attendees = incidence->attendees();
Laurent Montel's avatar
Laurent Montel committed
332
333
334
335
        Attendee::List::ConstIterator it;
        Attendee::List::ConstIterator end(attendees.constEnd());
        for (it = attendees.constBegin(); it != end; ++it) {
            if (it == attendees.constBegin()) {
336
                rsvp = (*it).RSVP(); // use what the first one has
Laurent Montel's avatar
Laurent Montel committed
337
            } else {
338
                if ((*it).RSVP() != rsvp) {
Laurent Montel's avatar
Laurent Montel committed
339
340
341
342
343
344
345
346
347
348
349
                    rsvp = true; // they differ, default
                    break;
                }
            }
        }
        return rsvp;
    }

    static Attendee::Role heuristicalRole(const Incidence::Ptr &incidence)
    {
        Attendee::Role role = Attendee::OptParticipant;
Laurent Montel's avatar
Minor    
Laurent Montel committed
350
        const Attendee::List attendees = incidence->attendees();
Laurent Montel's avatar
Laurent Montel committed
351
352
353
354
355
        Attendee::List::ConstIterator it;
        Attendee::List::ConstIterator end = attendees.constEnd();

        for (it = attendees.constBegin(); it != end; ++it) {
            if (it == attendees.constBegin()) {
356
                role = (*it).role(); // use what the first one has
Laurent Montel's avatar
Laurent Montel committed
357
            } else {
358
                if ((*it).role() != role) {
Laurent Montel's avatar
Laurent Montel committed
359
360
361
362
363
364
365
366
                    role = Attendee::OptParticipant; // they differ, default
                    break;
                }
            }
        }
        return role;
    }

367
    static Attachment findAttachment(const QString &name, const QString &iCal)
Laurent Montel's avatar
Laurent Montel committed
368
369
370
371
372
    {
        Incidence::Ptr incidence = stringToIncidence(iCal);

        // get the attachment by name from the incidence
        Attachment::List attachments = incidence->attachments();
373
374
375
376
377
378
        Attachment attachment;
        const Attachment::List::ConstIterator end = attachments.constEnd();
        for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) {
            if ((*it).label() == name) {
                attachment = *it;
                break;
Laurent Montel's avatar
Laurent Montel committed
379
380
381
            }
        }

382
        if (attachment.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
383
            KMessageBox::error(
Laurent Montel's avatar
Laurent Montel committed
384
                nullptr,
Laurent Montel's avatar
Laurent Montel committed
385
                i18n("No attachment named \"%1\" found in the invitation.", name));
386
            return Attachment();
Laurent Montel's avatar
Laurent Montel committed
387
388
        }

389
        if (attachment.isUri()) {
Laurent Montel's avatar
Laurent Montel committed
390
            bool fileExists = false;
391
            QUrl attachmentUrl(attachment.uri());
392
393
            if (attachmentUrl.isLocalFile()) {
                fileExists = QFile::exists(attachmentUrl.toLocalFile());
Laurent Montel's avatar
Laurent Montel committed
394
            } else {
Luca Beltrame's avatar
Luca Beltrame committed
395
                auto job = KIO::statDetails(attachmentUrl, KIO::StatJob::SourceSide, KIO::StatBasic);
Laurent Montel's avatar
Laurent Montel committed
396
397
                fileExists = job->exec();
            }
398
            if (!fileExists) {
Laurent Montel's avatar
Laurent Montel committed
399
                KMessageBox::information(
Laurent Montel's avatar
Laurent Montel committed
400
                    nullptr,
Laurent Montel's avatar
Laurent Montel committed
401
402
403
404
                    i18n("The invitation attachment \"%1\" is a web link that "
                         "is inaccessible from this computer. Please ask the event "
                         "organizer to resend the invitation with this attachment "
                         "stored inline instead of a link.",
405
                         attachmentUrl.toDisplayString()));
406
                return Attachment();
Laurent Montel's avatar
Laurent Montel committed
407
408
409
410
411
412
413
414
415
416
417
418
            }
        }
        return attachment;
    }

    static QString findReceiver(KMime::Content *node)
    {
        if (!node || !node->topLevel()) {
            return QString();
        }

        QString receiver;
419
        KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self();
Laurent Montel's avatar
Laurent Montel committed
420
421

        KMime::Types::Mailbox::List addrs;
Laurent Montel's avatar
Laurent Montel committed
422
423
        if (auto header = node->topLevel()->header<KMime::Headers::To>()) {
            addrs = header->mailboxes();
Laurent Montel's avatar
Laurent Montel committed
424
425
426
427
        }
        int found = 0;
        QVector< KMime::Types::Mailbox >::const_iterator end = addrs.constEnd();
        for (QVector< KMime::Types::Mailbox >::const_iterator it = addrs.constBegin();
Laurent Montel's avatar
Laurent Montel committed
428
             it != end; ++it) {
Laurent Montel's avatar
Laurent Montel committed
429
430
431
432
433
434
435
436
            if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) {
                // Ok, this could be us
                ++found;
                receiver = QLatin1String((*it).address());
            }
        }

        KMime::Types::Mailbox::List ccaddrs;
Laurent Montel's avatar
Laurent Montel committed
437
438
        if (auto header = node->topLevel()->header<KMime::Headers::Cc>()) {
            ccaddrs = header->mailboxes();
Laurent Montel's avatar
Laurent Montel committed
439
440
441
        }
        end = ccaddrs.constEnd();
        for (QVector<KMime::Types::Mailbox >::const_iterator it = ccaddrs.constBegin();
Laurent Montel's avatar
Laurent Montel committed
442
             it != end; ++it) {
Laurent Montel's avatar
Laurent Montel committed
443
444
445
446
447
448
449
450
451
452
453
            if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) {
                // Ok, this could be us
                ++found;
                receiver = QLatin1String((*it).address());
            }
        }
        if (found != 1) {
            QStringList possibleAddrs;
            bool ok;
            QString selectMessage;
            if (found == 0) {
Laurent Montel's avatar
Laurent Montel committed
454
455
                selectMessage
                    = i18n("<qt>None of your identities match the receiver of this message,<br/>"
Laurent Montel's avatar
Laurent Montel committed
456
                           "please choose which of the following addresses is yours,<br/> if any, "
Laurent Montel's avatar
Laurent Montel committed
457
                           "or select one of your identities to use in the reply:</qt>");
Laurent Montel's avatar
Laurent Montel committed
458
459
                possibleAddrs += im->allEmails();
            } else {
Laurent Montel's avatar
Laurent Montel committed
460
461
462
                selectMessage
                    = i18n("<qt>Several of your identities match the receiver of this message,<br/>"
                           "please choose which of the following addresses is yours:</qt>");
Laurent Montel's avatar
Laurent Montel committed
463
                possibleAddrs.reserve(addrs.count() + ccaddrs.count());
Laurent Montel's avatar
Laurent Montel committed
464
                for (const KMime::Types::Mailbox &mbx : qAsConst(addrs)) {
Laurent Montel's avatar
Laurent Montel committed
465
466
                    possibleAddrs.append(QLatin1String(mbx.address()));
                }
Laurent Montel's avatar
Laurent Montel committed
467
                for (const KMime::Types::Mailbox &mbx : qAsConst(ccaddrs)) {
Laurent Montel's avatar
Laurent Montel committed
468
469
470
471
472
473
474
475
                    possibleAddrs.append(QLatin1String(mbx.address()));
                }
            }

            // select default identity by default
            const QString defaultAddr = im->defaultIdentity().primaryEmailAddress();
            const int defaultIndex = qMax(0, possibleAddrs.indexOf(defaultAddr));

Laurent Montel's avatar
Laurent Montel committed
476
            receiver = QInputDialog::getItem(nullptr,
Laurent Montel's avatar
Laurent Montel committed
477
478
479
480
481
482
483
484
485
                                             i18n("Select Address"), selectMessage, possibleAddrs, defaultIndex, false, &ok);

            if (!ok) {
                receiver.clear();
            }
        }
        return receiver;
    }

486
    Attendee setStatusOnMyself(const Incidence::Ptr &incidence, const Attendee &myself, Attendee::PartStat status, const QString &receiver) const
Laurent Montel's avatar
Laurent Montel committed
487
488
489
490
    {
        QString name;
        QString email;
        KEmailAddress::extractEmailAddressAndName(receiver, email, name);
491
492
        if (name.isEmpty() && !myself.isNull()) {
            name = myself.name();
Laurent Montel's avatar
Laurent Montel committed
493
        }
494
495
        if (email.isEmpty() && !myself.isNull()) {
            email = myself.email();
Laurent Montel's avatar
Laurent Montel committed
496
497
498
        }
        Q_ASSERT(!email.isEmpty());   // delivery must be possible

499
        Attendee newMyself(name, email, true,  // RSVP, otherwise we would not be here
Laurent Montel's avatar
Laurent Montel committed
500
501
502
                           status,
                           !myself.isNull() ? myself.role() : heuristicalRole(incidence),
                           myself.uid());
503
504
505
        if (!myself.isNull()) {
            newMyself.setDelegate(myself.delegate());
            newMyself.setDelegator(myself.delegator());
Laurent Montel's avatar
Laurent Montel committed
506
507
508
509
        }

        // Make sure only ourselves is in the event
        incidence->clearAttendees();
510
        if (!newMyself.isNull()) {
Laurent Montel's avatar
Laurent Montel committed
511
512
513
514
515
516
517
518
519
520
521
522
            incidence->addAttendee(newMyself);
        }
        return newMyself;
    }

    enum MailType {
        Answer,
        Delegation,
        Forward,
        DeclineCounter
    };

Laurent Montel's avatar
Laurent Montel committed
523
    bool mailICal(const QString &receiver, const QString &to, const QString &iCal, const QString &subject, const QString &status, bool delMessage, Viewer *viewerInstance) const
Laurent Montel's avatar
Laurent Montel committed
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
    {
        qCDebug(TEXT_CALENDAR_LOG) << "Mailing message:" << iCal;

        KMime::Message::Ptr msg(new KMime::Message);
        if (MessageViewer::MessageViewerSettings::self()->exchangeCompatibleInvitations()) {
            msg->subject()->fromUnicodeString(status, "utf-8");
            QString tsubject = subject;
            tsubject.remove(i18n("Answer: "));
            if (status == QLatin1String("cancel")) {
                msg->subject()->fromUnicodeString(
                    i18nc("Not able to attend.", "Declined: %1", tsubject), "utf-8");
            } else if (status == QLatin1String("tentative")) {
                msg->subject()->fromUnicodeString(
                    i18nc("Unsure if it is possible to attend.", "Tentative: %1", tsubject), "utf-8");
            } else if (status == QLatin1String("accepted")) {
                msg->subject()->fromUnicodeString(
                    i18nc("Accepted the invitation.", "Accepted: %1", tsubject), "utf-8");
            } else {
                msg->subject()->fromUnicodeString(subject, "utf-8");
            }
        } else {
            msg->subject()->fromUnicodeString(subject, "utf-8");
        }
        msg->to()->fromUnicodeString(to, "utf-8");
        msg->from()->fromUnicodeString(receiver, "utf-8");
        msg->date()->setDateTime(QDateTime::currentDateTime());

Laurent Montel's avatar
Laurent Montel committed
551
        if (MessageViewer::MessageViewerSettings::self()->legacyBodyInvites()) {
Laurent Montel's avatar
Laurent Montel committed
552
553
554
555
556
            auto ct = msg->contentType(); //create
            ct->setMimeType("text/calendar");
            ct->setCharset("utf-8");
            ct->setName(QStringLiteral("cal.ics"), "utf-8");
            ct->setParameter(QStringLiteral("method"), QStringLiteral("reply"));
Laurent Montel's avatar
Laurent Montel committed
557
558
559
560

            KMime::Headers::ContentDisposition *disposition = new KMime::Headers::ContentDisposition;
            disposition->setDisposition(KMime::Headers::CDinline);
            msg->setHeader(disposition);
Laurent Montel's avatar
Laurent Montel committed
561
            msg->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
562
563
            const QString answer = i18n("Invitation answer attached.");
            msg->setBody(answer.toUtf8());
Laurent Montel's avatar
Laurent Montel committed
564
        } else {
Laurent Montel's avatar
Laurent Montel committed
565
566
567
568
569
570
571
572
573
574
575
576
577
            // We need to set following 4 lines by hand else KMime::Content::addContent
            // will create a new Content instance for us to attach the main message
            // what we don't need cause we already have the main message instance where
            // 2 additional messages are attached.
            KMime::Headers::ContentType *ct = msg->contentType();
            ct->setMimeType("multipart/mixed");
            ct->setBoundary(KMime::multiPartBoundary());
            ct->setCategory(KMime::Headers::CCcontainer);

            // Set the first multipart, the body message.
            KMime::Content *bodyMessage = new KMime::Content;
            KMime::Headers::ContentDisposition *bodyDisposition = new KMime::Headers::ContentDisposition;
            bodyDisposition->setDisposition(KMime::Headers::CDinline);
Laurent Montel's avatar
Laurent Montel committed
578
579
580
            auto bodyMessageCt = bodyMessage->contentType();
            bodyMessageCt->setMimeType("text/plain");
            bodyMessageCt->setCharset("utf-8");
Laurent Montel's avatar
Laurent Montel committed
581
            bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
582
583
            const QString answer = i18n("Invitation answer attached.");
            bodyMessage->setBody(answer.toUtf8());
Laurent Montel's avatar
Laurent Montel committed
584
585
586
587
588
589
590
            bodyMessage->setHeader(bodyDisposition);
            msg->addContent(bodyMessage);

            // Set the second multipart, the attachment.
            KMime::Content *attachMessage = new KMime::Content;
            KMime::Headers::ContentDisposition *attachDisposition = new KMime::Headers::ContentDisposition;
            attachDisposition->setDisposition(KMime::Headers::CDattachment);
Laurent Montel's avatar
Laurent Montel committed
591
592
593
594
595
            auto attachCt = attachMessage->contentType();
            attachCt->setMimeType("text/calendar");
            attachCt->setCharset("utf-8");
            attachCt->setName(QStringLiteral("cal.ics"), "utf-8");
            attachCt->setParameter(QStringLiteral("method"),
Laurent Montel's avatar
Laurent Montel committed
596
                                                       QStringLiteral("reply"));
Laurent Montel's avatar
Laurent Montel committed
597
598
599
600
            attachMessage->setHeader(attachDisposition);
            attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
            attachMessage->setBody(KMime::CRLFtoLF(iCal.toUtf8()));
            msg->addContent(attachMessage);
Laurent Montel's avatar
Laurent Montel committed
601
602
603
604
605
        }

        // Try and match the receiver with an identity.
        // Setting the identity here is important, as that is used to select the correct
        // transport later
Laurent Montel's avatar
Laurent Montel committed
606
        KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self();
Laurent Montel's avatar
Laurent Montel committed
607
608
        const KIdentityManagement::Identity identity
            = im->identityForAddress(
Laurent Montel's avatar
Laurent Montel committed
609
                  findReceiver(viewerInstance->message().data()));
Laurent Montel's avatar
Laurent Montel committed
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626

        const bool nullIdentity = (identity == KIdentityManagement::Identity::null());

        if (!nullIdentity) {
            KMime::Headers::Generic *x_header = new KMime::Headers::Generic("X-KMail-Identity");
            x_header->from7BitString(QByteArray::number(identity.uoid()));
            msg->setHeader(x_header);
        }

        const bool identityHasTransport = !identity.transport().isEmpty();
        int transportId = -1;
        if (!nullIdentity && identityHasTransport) {
            transportId = identity.transport().toInt();
        } else {
            transportId = TransportManager::self()->defaultTransportId();
        }
        if (transportId == -1) {
Laurent Montel's avatar
Laurent Montel committed
627
            if (!TransportManager::self()->showTransportCreationDialog(nullptr, TransportManager::IfNoTransportExists)) {
Laurent Montel's avatar
Laurent Montel committed
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
                return false;
            }
            transportId = TransportManager::self()->defaultTransportId();
        }
        auto header = new KMime::Headers::Generic("X-KMail-Transport");
        header->fromUnicodeString(QString::number(transportId), "utf-8");
        msg->setHeader(header);

        // Outlook will only understand the reply if the From: header is the
        // same as the To: header of the invitation message.
        if (!MessageViewer::MessageViewerSettings::self()->legacyMangleFromToHeaders()) {
            if (identity != KIdentityManagement::Identity::null()) {
                msg->from()->fromUnicodeString(identity.fullEmailAddr(), "utf-8");
            }
            // Remove BCC from identity on ical invitations (kolab/issue474)
            msg->removeHeader<KMime::Headers::Bcc>();
        }

        msg->assemble();
        MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(transportId);

        MailTransport::MessageQueueJob *job = new MailTransport::MessageQueueJob;

        job->addressAttribute().setTo(QStringList() << KEmailAddress::extractEmailAddress(
                                          KEmailAddress::normalizeAddressesAndEncodeIdn(to)));
        job->transportAttribute().setTransportId(transport->id());

        if (transport->specifySenderOverwriteAddress()) {
            job->addressAttribute().setFrom(
                KEmailAddress::extractEmailAddress(
                    KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress())));
        } else {
            job->addressAttribute().setFrom(
                KEmailAddress::extractEmailAddress(
                    KEmailAddress::normalizeAddressesAndEncodeIdn(msg->from()->asUnicodeString())));
        }

        job->setMessage(msg);

Laurent Montel's avatar
Laurent Montel committed
667
        if (!job->exec()) {
Laurent Montel's avatar
Laurent Montel committed
668
669
670
671
            qCWarning(TEXT_CALENDAR_LOG) << "Error queuing message in outbox:" << job->errorText();
            return false;
        }
        // We are not notified when mail was sent, so assume it was sent when queued.
Laurent Montel's avatar
Laurent Montel committed
672
673
        if (delMessage
            && MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
Laurent Montel's avatar
Laurent Montel committed
674
675
676
677
678
            viewerInstance->deleteMessage();
        }
        return true;
    }

Laurent Montel's avatar
Laurent Montel committed
679
    bool mail(Viewer *viewerInstance, const Incidence::Ptr &incidence, const QString &status, iTIPMethod method = iTIPReply, const QString &receiver = QString(), const QString &to = QString(), MailType type = Answer) const
Laurent Montel's avatar
Laurent Montel committed
680
681
682
    {
        //status is accepted/tentative/declined
        ICalFormat format;
683
        format.setTimeZone(QTimeZone::systemTimeZone());
Laurent Montel's avatar
Laurent Montel committed
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
        QString msg = format.createScheduleMessage(incidence, method);
        QString summary = incidence->summary();
        if (summary.isEmpty()) {
            summary = i18n("Incidence with no summary");
        }
        QString subject;
        switch (type) {
        case Answer:
            subject = i18n("Answer: %1", summary);
            break;
        case Delegation:
            subject = i18n("Delegated: %1", summary);
            break;
        case Forward:
            subject = i18n("Forwarded: %1", summary);
            break;
        case DeclineCounter:
            subject = i18n("Declined Counter Proposal: %1", summary);
            break;
        }

        // Set the organizer to the sender, if the ORGANIZER hasn't been set.
706
        if (incidence->organizer().isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
707
708
709
            QString tname, temail;
            KMime::Message::Ptr message = viewerInstance->message();
            KEmailAddress::extractEmailAddressAndName(message->sender()->asUnicodeString(),
Laurent Montel's avatar
Laurent Montel committed
710
                                                      temail, tname);
711
            incidence->setOrganizer(Person(tname, temail));
Laurent Montel's avatar
Laurent Montel committed
712
713
714
715
        }

        QString recv = to;
        if (recv.isEmpty()) {
716
            recv = incidence->organizer().fullName();
Laurent Montel's avatar
Laurent Montel committed
717
718
719
720
        }
        return mailICal(receiver, recv, msg, subject, status, type != Forward, viewerInstance);
    }

721
    bool saveFile(const QString &receiver, const QString &iCal, const QString &type, MimeTreeParser::Interface::BodyPart *bodyPart) const
Laurent Montel's avatar
Laurent Montel committed
722
    {
723
        MemoryCalendarMemento *memento = dynamic_cast<MemoryCalendarMemento *>(bodyPart->memento());
Laurent Montel's avatar
Laurent Montel committed
724
        // This will block. There's no way to make it async without refactoring the memento mechanism
725
726

        SyncItipHandler *itipHandler = new SyncItipHandler(receiver, iCal, type, memento->calendar());
Laurent Montel's avatar
Laurent Montel committed
727
728
729
730

        // If result is ResultCancelled, then we don't show the message box and return false so kmail
        // doesn't delete the e-mail.
        qCDebug(TEXT_CALENDAR_LOG) << "ITIPHandler result was " << itipHandler->result();
Laurent Montel's avatar
Minor    
Laurent Montel committed
731
732
        const Akonadi::ITIPHandler::Result res = itipHandler->result();
        if (res == Akonadi::ITIPHandler::ResultError) {
Laurent Montel's avatar
Laurent Montel committed
733
734
735
            const QString errorMessage = itipHandler->errorMessage();
            if (!errorMessage.isEmpty()) {
                qCCritical(TEXT_CALENDAR_LOG) << "Error while processing invitation: " << errorMessage;
Laurent Montel's avatar
Laurent Montel committed
736
                KMessageBox::error(nullptr, errorMessage);
Laurent Montel's avatar
Laurent Montel committed
737
            }
Laurent Montel's avatar
Minor    
Laurent Montel committed
738
            return false;
Laurent Montel's avatar
Laurent Montel committed
739
740
        }

Laurent Montel's avatar
Minor    
Laurent Montel committed
741
        return res;
Laurent Montel's avatar
Laurent Montel committed
742
743
744
745
746
    }

    bool cancelPastInvites(const Incidence::Ptr incidence, const QString &path) const
    {
        QString warnStr;
747
        QDateTime now = QDateTime::currentDateTime();
Laurent Montel's avatar
Laurent Montel committed
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        QDate today = now.date();
        Incidence::IncidenceType type = Incidence::TypeUnknown;
        const bool occurred = occurredAlready(incidence);
        if (incidence->type() == Incidence::TypeEvent) {
            type = Incidence::TypeEvent;
            Event::Ptr event = incidence.staticCast<Event>();
            if (!event->allDay()) {
                if (occurred) {
                    warnStr = i18n("\"%1\" occurred already.", event->summary());
                } else if (event->dtStart() <= now && now <= event->dtEnd()) {
                    warnStr = i18n("\"%1\" is currently in-progress.", event->summary());
                }
            } else {
                if (occurred) {
                    warnStr = i18n("\"%1\" occurred already.", event->summary());
                } else if (event->dtStart().date() <= today && today <= event->dtEnd().date()) {
                    warnStr = i18n("\"%1\", happening all day today, is currently in-progress.",
                                   event->summary());
                }
            }
        } else if (incidence->type() == Incidence::TypeTodo) {
            type = Incidence::TypeTodo;
            Todo::Ptr todo = incidence.staticCast<Todo>();
            if (!todo->allDay()) {
                if (todo->hasDueDate()) {
                    if (todo->dtDue() < now) {
                        warnStr = i18n("\"%1\" is past due.", todo->summary());
                    } else if (todo->hasStartDate() && todo->dtStart() <= now && now <= todo->dtDue()) {
                        warnStr = i18n("\"%1\" is currently in-progress.", todo->summary());
                    }
                } else if (todo->hasStartDate()) {
                    if (todo->dtStart() < now) {
                        warnStr = i18n("\"%1\" has already started.", todo->summary());
                    }
                }
            } else {
                if (todo->hasDueDate()) {
                    if (todo->dtDue().date() < today) {
                        warnStr = i18n("\"%1\" is past due.", todo->summary());
Laurent Montel's avatar
Laurent Montel committed
787
788
                    } else if (todo->hasStartDate()
                               && todo->dtStart().date() <= today && today <= todo->dtDue().date()) {
Laurent Montel's avatar
Laurent Montel committed
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
                        warnStr = i18n("\"%1\", happening all-day today, is currently in-progress.",
                                       todo->summary());
                    }
                } else if (todo->hasStartDate()) {
                    if (todo->dtStart().date() < today) {
                        warnStr = i18n("\"%1\", happening all day, has already started.", todo->summary());
                    }
                }
            }
        }

        if (!warnStr.isEmpty()) {
            QString queryStr;
            if (path == QLatin1String("accept")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you still want to accept the task?");
                } else {
                    queryStr = i18n("Do you still want to accept the invitation?");
                }
            } else if (path == QLatin1String("accept_conditionally")) {
                if (type == Incidence::TypeTodo) {
Laurent Montel's avatar
Laurent Montel committed
810
811
                    queryStr
                        = i18n("Do you still want to send conditional acceptance of the invitation?");
Laurent Montel's avatar
Laurent Montel committed
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
                } else {
                    queryStr = i18n("Do you still want to send conditional acceptance of the task?");
                }
            } else if (path == QLatin1String("accept_counter")) {
                queryStr = i18n("Do you still want to accept the counter proposal?");
            } else if (path == QLatin1String("counter")) {
                queryStr = i18n("Do you still want to send a counter proposal?");
            } else if (path == QLatin1String("decline")) {
                queryStr = i18n("Do you still want to send a decline response?");
            } else if (path == QLatin1String("decline_counter")) {
                queryStr = i18n("Do you still want to decline the counter proposal?");
            } else if (path == QLatin1String("reply")) {
                queryStr = i18n("Do you still want to record this response in your calendar?");
            } else if (path == QLatin1String("delegate")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you still want to delegate this task?");
                } else {
                    queryStr = i18n("Do you still want to delegate this invitation?");
                }
            } else if (path == QLatin1String("forward")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you still want to forward this task?");
                } else {
                    queryStr = i18n("Do you still want to forward this invitation?");
                }
            } else if (path == QLatin1String("cancel")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you still want to cancel this task?");
                } else {
                    queryStr = i18n("Do you still want to cancel this invitation?");
                }
            } else if (path == QLatin1String("check_calendar")) {
                queryStr = i18n("Do you still want to check your calendar?");
            } else if (path == QLatin1String("record")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you still want to record this task in your calendar?");
                } else {
                    queryStr = i18n("Do you still want to record this invitation in your calendar?");
                }
            } else if (path == QLatin1String("cancel")) {
                if (type == Incidence::TypeTodo) {
                    queryStr = i18n("Do you really want to cancel this task?");
                } else {
                    queryStr = i18n("Do you really want to cancel this invitation?");
                }
Laurent Montel's avatar
Laurent Montel committed
857
            } else if (path.startsWith(QLatin1String("ATTACH:"))) {
Laurent Montel's avatar
Laurent Montel committed
858
859
860
861
862
863
                return false;
            } else {
                queryStr = i18n("%1?", path);
            }

            if (KMessageBox::warningYesNo(
Laurent Montel's avatar
Laurent Montel committed
864
865
                    nullptr,
                    i18n("%1\n%2", warnStr, queryStr)) == KMessageBox::No) {
Laurent Montel's avatar
Laurent Montel committed
866
867
868
869
870
871
                return true;
            }
        }
        return false;
    }

Laurent Montel's avatar
Laurent Montel committed
872
    bool handleInvitation(const QString &iCal, Attendee::PartStat status, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
Laurent Montel's avatar
Laurent Montel committed
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
    {
        bool ok = true;
        const QString receiver = findReceiver(part->content());
        qCDebug(TEXT_CALENDAR_LOG) << receiver;

        if (receiver.isEmpty()) {
            // Must be some error. Still return true though, since we did handle it
            return true;
        }

        Incidence::Ptr incidence = stringToIncidence(iCal);
        qCDebug(TEXT_CALENDAR_LOG) << "Handling invitation: uid is : " << incidence->uid()
                                   << "; schedulingId is:" << incidence->schedulingID()
                                   << "; Attendee::PartStat = " << status;

        // get comment for tentative acceptance
        if (askForComment(status)) {
Laurent Montel's avatar
Laurent Montel committed
890
            QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
Laurent Montel's avatar
Laurent Montel committed
891
            dlg->setWindowTitle(i18nc("@title:window", "Reaction to Invitation"));
892
893
894
            QString comment;
            if (dlg->exec()) {
                comment = dlg->comment();
Laurent Montel's avatar
Laurent Montel committed
895
                delete dlg;
896
897
            } else {
                delete dlg;
Laurent Montel's avatar
Laurent Montel committed
898
899
                return true;
            }
900

901
            if (comment.trimmed().isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
902
                KMessageBox::error(
Laurent Montel's avatar
Laurent Montel committed
903
                    nullptr,
Laurent Montel's avatar
Laurent Montel committed
904
905
906
                    i18n("You forgot to add proposal. Please add it. Thanks"));
                return true;
            } else {
Laurent Montel's avatar
Laurent Montel committed
907
                incidence->addComment(comment);
Laurent Montel's avatar
Laurent Montel committed
908
909
910
911
912
913
            }
        }

        // First, save it for KOrganizer to handle
        const QString dir = directoryForStatus(status);
        if (dir.isEmpty()) {
914
            qCWarning(TEXT_CALENDAR_LOG) << "Impossible to understand status: " << status;
Laurent Montel's avatar
Laurent Montel committed
915
916
917
918
            return true; // unknown status
        }
        if (status != Attendee::Delegated) {
            // we do that below for delegated incidences
919
            if (!saveFile(receiver, iCal, dir, part)) {
Laurent Montel's avatar
Laurent Montel committed
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
                return false;
            }
        }

        QString delegateString;
        bool delegatorRSVP = false;
        if (status == Attendee::Delegated) {
            DelegateSelector dlg;
            if (dlg.exec() == QDialog::Rejected) {
                return true;
            }
            delegateString = dlg.delegate();
            delegatorRSVP = dlg.rsvp();
            if (delegateString.isEmpty()) {
                return true;
            }
936
            if (KEmailAddress::compareEmail(delegateString, incidence->organizer().email(), false)) {
Laurent Montel's avatar
Laurent Montel committed
937
                KMessageBox::sorry(nullptr, i18n("Delegation to organizer is not possible."));
Laurent Montel's avatar
Laurent Montel committed
938
939
940
941
942
943
944
945
                return true;
            }
        }

        if (!incidence) {
            return false;
        }

946
        const Attendee myself = findMyself(incidence, receiver);
Laurent Montel's avatar
Laurent Montel committed
947
948
949

        // find our delegator, we need to inform him as well
        QString delegator;
950
        if (status != Attendee::NeedsAction && !myself.isNull() && !myself.delegator().isEmpty()) {
Laurent Montel's avatar
Minor    
Laurent Montel committed
951
            const Attendee::List attendees = incidence->attendees();
Laurent Montel's avatar
Laurent Montel committed
952
953
            Attendee::List::ConstIterator end = attendees.constEnd();
            for (Attendee::List::ConstIterator it = attendees.constBegin();
Laurent Montel's avatar
Laurent Montel committed
954
                 it != end; ++it) {
955
956
957
958
                if (KEmailAddress::compareEmail((*it).fullName(), myself.delegator(), false)
                    && (*it).status() == Attendee::Delegated) {
                    delegator = (*it).fullName();
                    delegatorRSVP = (*it).RSVP();
Laurent Montel's avatar
Laurent Montel committed
959
960
961
962
963
                    break;
                }
            }
        }

964
965
966
967
968
        if (status != Attendee::NeedsAction && ((!myself.isNull() && (myself.RSVP() || myself.status() == Attendee::NeedsAction)) || heuristicalRSVP(incidence))) {
            Attendee newMyself = setStatusOnMyself(incidence, myself, status, receiver);
            if (!newMyself.isNull() && status == Attendee::Delegated) {
                newMyself.setDelegate(delegateString);
                newMyself.setRSVP(delegatorRSVP);
Laurent Montel's avatar
Laurent Montel committed
969
            }
Laurent Montel's avatar
Laurent Montel committed
970
            ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
Laurent Montel's avatar
Laurent Montel committed
971
972

            // check if we need to inform our delegator about this as well
973
            if (!newMyself.isNull()
Laurent Montel's avatar
Laurent Montel committed
974
975
                && (status == Attendee::Accepted || status == Attendee::Declined)
                && !delegator.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
976
977
978
979
                if (delegatorRSVP || status == Attendee::Declined) {
                    ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver, delegator);
                }
            }
980
        } else if (myself.isNull() && (status != Attendee::Declined && status != Attendee::NeedsAction)) {
Laurent Montel's avatar
Laurent Montel committed
981
982
983
984
985
            // forwarded invitation
            QString name;
            QString email;
            KEmailAddress::extractEmailAddressAndName(receiver, email, name);
            if (!email.isEmpty()) {
986
                Attendee newMyself(name, email, true,  // RSVP, otherwise we would not be here
Laurent Montel's avatar
Laurent Montel committed
987
988
989
                                   status,
                                   heuristicalRole(incidence),
                                   QString());
Laurent Montel's avatar
Laurent Montel committed
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
                incidence->clearAttendees();
                incidence->addAttendee(newMyself);
                ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver);
            }
        } else {
            if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) {
                viewerInstance->deleteMessage();
            }
        }

        // create invitation for the delegate (same as the original invitation
        // with the delegate as additional attendee), we also use that for updating
        // our calendar
        if (status == Attendee::Delegated) {
            incidence = stringToIncidence(iCal);
1005
1006
1007
            auto attendees = incidence->attendees();
            const int myselfIdx = findMyself(attendees, receiver);
            if (myselfIdx >= 0) {
1008
1009
                attendees[myselfIdx].setStatus(status);
                attendees[myselfIdx].setDelegate(delegateString);
1010
                incidence->setAttendees(attendees);
Laurent Montel's avatar
Laurent Montel committed
1011
1012
1013
            }
            QString name, email;
            KEmailAddress::extractEmailAddressAndName(delegateString, email, name);
1014
1015
            Attendee delegate(name, email, true);
            delegate.setDelegator(receiver);
Laurent Montel's avatar
Laurent Montel committed
1016
1017
1018
            incidence->addAttendee(delegate);

            ICalFormat format;
1019
            format.setTimeZone(QTimeZone::systemTimeZone());
Laurent Montel's avatar
Laurent Montel committed
1020
            const QString iCal = format.createScheduleMessage(incidence, iTIPRequest);
1021
            if (!saveFile(receiver, iCal, dir, part)) {
Laurent Montel's avatar
Laurent Montel committed
1022
1023
1024
1025
1026
1027
1028
1029
                return false;
            }

            ok = mail(viewerInstance, incidence, dir, iTIPRequest, receiver, delegateString, Delegation);
        }
        return ok;
    }

1030
    void openAttachment(const QString &name, const QString &iCal) const
Laurent Montel's avatar
Laurent Montel committed
1031
    {
1032
1033
        Attachment attachment(findAttachment(name, iCal));
        if (attachment.isEmpty()) {
1034
            return;
Laurent Montel's avatar
Laurent Montel committed
1035
1036
        }

1037
1038
        if (attachment.isUri()) {
            QDesktopServices::openUrl(QUrl(attachment.uri()));
Laurent Montel's avatar
Laurent Montel committed
1039
1040
        } else {
            // put the attachment in a temporary file and launch it
Laurent Montel's avatar
Minor    
Laurent Montel committed
1041
            QTemporaryFile *file = nullptr;
Laurent Montel's avatar
Laurent Montel committed
1042
            QMimeDatabase db;
1043
            QStringList patterns = db.mimeTypeForName(attachment.mimeType()).globPatterns();
Laurent Montel's avatar
Laurent Montel committed
1044
1045
            if (!patterns.empty()) {
                QString pattern = patterns.at(0);
1046
                file = new QTemporaryFile(QDir::tempPath() + QLatin1String("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
Laurent Montel's avatar
Laurent Montel committed
1047
1048
1049
1050
1051
1052
            } else {
                file = new QTemporaryFile();
            }
            file->setAutoRemove(false);
            file->open();
            file->setPermissions(QFile::ReadUser);
1053
            file->write(QByteArray::fromBase64(attachment.data()));
Laurent Montel's avatar
Laurent Montel committed
1054
1055
            file->close();

1056
1057
1058
            KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file->fileName()), attachment.mimeType());
            job->setDeleteTemporaryFile(true);
            job->start();
Laurent Montel's avatar
Laurent Montel committed
1059
1060
1061
1062
1063
1064
            delete file;
        }
    }

    bool saveAsAttachment(const QString &name, const QString &iCal) const
    {
1065
1066
        Attachment a(findAttachment(name, iCal));
        if (a.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
1067
1068
1069
1070
            return false;
        }

        // get the saveas file name
Laurent Montel's avatar
Laurent Montel committed
1071
        const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18n("Save Invitation Attachment"), name, QString());
Laurent Montel's avatar
Laurent Montel committed
1072

1073
        if (saveAsFile.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
1074
1075
1076
1077
            return false;
        }

        bool stat = false;
1078
        if (a.isUri()) {
Laurent Montel's avatar
Laurent Montel committed
1079
            // save the attachment url
1080
            auto job = KIO::file_copy(QUrl(a.uri()), QUrl::fromLocalFile(saveAsFile));
Laurent Montel's avatar
Laurent Montel committed
1081
1082
1083
            stat = job->exec();
        } else {
            // put the attachment in a temporary file and save it
Laurent Montel's avatar
Laurent Montel committed
1084
1085
1086
            QTemporaryFile *file{
                nullptr
            };
Laurent Montel's avatar
Laurent Montel committed
1087
            QMimeDatabase db;
1088
            QStringList patterns = db.mimeTypeForName(a.mimeType()).globPatterns();
Laurent Montel's avatar
Laurent Montel committed
1089
1090
            if (!patterns.empty()) {
                QString pattern = patterns.at(0);
1091
                file = new QTemporaryFile(QDir::tempPath() + QLatin1String("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*')));
Laurent Montel's avatar
Laurent Montel committed
1092
1093
1094
1095
1096
1097
            } else {
                file = new QTemporaryFile();
            }
            file->setAutoRemove(false);
            file->open();
            file->setPermissions(QFile::ReadUser);
1098
            file->write(QByteArray::fromBase64(a.data()));
Laurent Montel's avatar
Laurent Montel committed
1099
            file->close();
Laurent Montel's avatar
Laurent Montel committed
1100
1101
            const QString filename = file->fileName();
            delete file;
Laurent Montel's avatar
Laurent Montel committed
1102

Laurent Montel's avatar
Laurent Montel committed
1103
            auto job = KIO::file_copy(QUrl::fromLocalFile(filename), QUrl::fromLocalFile(saveAsFile));
Laurent Montel's avatar
Laurent Montel committed
1104
1105
1106
1107
1108
1109
1110
            stat = job->exec();
        }
        return stat;
    }

    void showCalendar(const QDate &date) const
    {
1111
        // If korganizer or kontact is running, bring it to the front. Otherwise start korganizer.
Laurent Montel's avatar
Laurent Montel committed
1112
        if (KontactInterface::PimUniqueApplication::activateApplication(QStringLiteral("korganizer"))) {
Laurent Montel's avatar
Laurent Montel committed
1113
1114
            OrgKdeKorganizerCalendarInterface iface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"),
                                                    QDBusConnection::sessionBus(), nullptr);
1115
1116
            if (!iface.isValid()) {
                qCDebug(TEXT_CALENDAR_LOG) << "Calendar interface is not valid! " << iface.lastError().message();
Laurent Montel's avatar
Laurent Montel committed
1117
1118
                return;
            }
1119
1120
            iface.showEventView();
            iface.showDate(date);
Laurent Montel's avatar
Laurent Montel committed
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
        }
    }

    bool handleIgnore(Viewer *viewerInstance) const
    {
        // simply move the message to trash
        viewerInstance->deleteMessage();
        return true;
    }

Laurent Montel's avatar
Laurent Montel committed
1131
    bool handleDeclineCounter(const QString &iCal, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const
Laurent Montel's avatar
Laurent Montel committed
1132
1133
1134
1135
1136
1137
1138
    {
        const QString receiver(findReceiver(part->content()));
        if (receiver.isEmpty()) {
            return true;
        }
        Incidence::Ptr incidence(stringToIncidence(iCal));
        if (askForComment(Attendee::Declined)) {
Laurent Montel's avatar
Laurent Montel committed
1139
            QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr);
Laurent Montel's avatar
Laurent Montel committed
1140
            dlg->setWindowTitle(i18nc("@title:window", "Decline Counter Proposal"));
1141
1142
1143
            QString comment;
            if (dlg->exec()) {
                comment = dlg->comment();
Laurent Montel's avatar
Laurent Montel committed
1144
                delete dlg;
1145
1146
            } else {
                delete dlg;
Laurent Montel's avatar
Laurent Montel committed
1147
1148
                return true;
            }
1149

Laurent Montel's avatar
Laurent Montel committed
1150
            if (comment.trimmed().isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
1151
                KMessageBox::error(
Laurent Montel's avatar
Laurent Montel committed
1152
                    nullptr,
Laurent Montel's avatar
Laurent Montel committed
1153
1154
1155
                    i18n("You forgot to add proposal. Please add it. Thanks"));
                return true;
            } else {
Laurent Montel's avatar
Laurent Montel committed
1156
                incidence->addComment(comment);
Laurent Montel's avatar
Laurent Montel committed
1157
1158
            }
        }
1159
        return mail(viewerInstance, incidence, QStringLiteral("declinecounter"), KCalendarCore::iTIPDeclineCounter,
Laurent Montel's avatar
Laurent Montel committed
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
                    receiver, QString(), DeclineCounter);
    }

    bool counterProposal(const QString &iCal, MimeTreeParser::Interface::BodyPart *part) const
    {
        const QString receiver = findReceiver(part->content());
        if (receiver.isEmpty()) {
            return true;
        }

        // Don't delete the invitation here in any case, if the counter proposal
        // is declined you might need it again.
1172
        return saveFile(receiver, iCal, QStringLiteral("counter"), part);
Laurent Montel's avatar
Laurent Montel committed
1173
1174
    }

1175
    bool handleClick(Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const override
Laurent Montel's avatar
Laurent Montel committed
1176
1177
1178
    {
        // filter out known paths that don't belong to this type of urlmanager.
        // kolab/issue4054 msg27201
Laurent Montel's avatar
Laurent Montel committed
1179
        if (path.contains(QLatin1String("addToAddressBook:")) || path.contains(QLatin1String("updateToAddressBook"))) {
Laurent Montel's avatar
Laurent Montel committed
1180
1181
1182
1183
1184
            return false;
        }

        if (!hasMyWritableEventsFolders(QStringLiteral("calendar"))) {
            KMessageBox::error(
Laurent Montel's avatar
Laurent Montel committed
1185
                nullptr,
Laurent Montel's avatar
Laurent Montel committed
1186
1187
1188
1189
1190
1191
1192
1193
1194
                i18n("You have no writable calendar folders for invitations, "
                     "so storing or saving a response will not be possible.\n"
                     "Please create at least 1 writable events calendar and re-sync."));
            return false;
        }

        // If the bodypart does not have a charset specified, we need to fall back to utf8,
        // not the KMail fallback encoding, so get the contents as binary and decode explicitly.
        QString iCal;
Laurent Montel's avatar
Laurent Montel committed
1195
        if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) {
1196
            const QByteArray &ba = part->content()->decodedContent();
Laurent Montel's avatar
Laurent Montel committed
1197
1198
            iCal = QString::fromUtf8(ba);
        } else {
1199
            iCal = part->content()->decodedText();
Laurent Montel's avatar
Laurent Montel committed
1200
1201
1202
1203
1204
        }

        Incidence::Ptr incidence = stringToIncidence(iCal);
        if (!incidence) {
            KMessageBox::sorry(
Laurent Montel's avatar
Laurent Montel committed
1205
                nullptr,
Laurent Montel's avatar
Laurent Montel committed
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
                i18n("The calendar invitation stored in this email message is broken in some way. "
                     "Unable to continue."));
            return false;
        }

        bool result = false;
        if (cancelPastInvites(incidence, path)) {
            return result;
        }

        if (path == QLatin1String("accept")) {
            result = handleInvitation(iCal, Attendee::Accepted, part, viewerInstance);
        } else if (path == QLatin1String