File indexing completed on 2025-01-19 04:46:49
0001 /* 0002 This file is part of kdepim. 0003 0004 SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org> 0005 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org> 0006 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net> 0007 SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org> 0008 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include "attendeeselector.h" 0013 #include "calendarinterface.h" 0014 #include "delegateselector.h" 0015 #include "memorycalendarmemento.h" 0016 #include "reactiontoinvitationdialog.h" 0017 #include "syncitiphandler.h" 0018 0019 #include <KIdentityManagementCore/IdentityManager> 0020 0021 #include <MessageViewer/BodyPartURLHandler> 0022 #include <MessageViewer/HtmlWriter> 0023 #include <MessageViewer/MessagePartRenderPlugin> 0024 #include <MessageViewer/MessagePartRendererBase> 0025 #include <MessageViewer/MessageViewerSettings> 0026 #include <MessageViewer/Viewer> 0027 #include <MimeTreeParser/BodyPart> 0028 #include <MimeTreeParser/MessagePart> 0029 using namespace MessageViewer; 0030 0031 #include <KCalendarCore/ICalFormat> 0032 using namespace KCalendarCore; 0033 0034 #include <KCalUtils/IncidenceFormatter> 0035 0036 #include <KMime/Message> 0037 0038 #include <KIdentityManagementCore/Identity> 0039 0040 #include <KEmailAddress> 0041 0042 #include <Akonadi/MessageQueueJob> 0043 #include <MailTransport/TransportManager> 0044 0045 #include "text_calendar_debug.h" 0046 0047 #include <KIO/FileCopyJob> 0048 #include <KIO/JobUiDelegate> 0049 #include <KIO/OpenUrlJob> 0050 #include <KIO/StatJob> 0051 #include <KLocalizedString> 0052 #include <KMessageBox> 0053 0054 #include <QDBusServiceWatcher> 0055 #include <QDesktopServices> 0056 #include <QFileDialog> 0057 #include <QIcon> 0058 #include <QInputDialog> 0059 #include <QMenu> 0060 #include <QMimeDatabase> 0061 #include <QPointer> 0062 #include <QTemporaryFile> 0063 #include <QUrl> 0064 0065 using namespace MailTransport; 0066 0067 namespace 0068 { 0069 static bool hasMyWritableEventsFolders(const QString &family) 0070 { 0071 Q_UNUSED(family) 0072 #if 0 // TODO port to Akonadi 0073 QString myfamily = family; 0074 if (family.isEmpty()) { 0075 myfamily = QStringLiteral("calendar"); 0076 } 0077 0078 #ifndef KDEPIM_NO_KRESOURCES 0079 CalendarResourceManager manager(myfamily); 0080 manager.readConfig(); 0081 0082 CalendarResourceManager::ActiveIterator it; 0083 for (it = manager.activeBegin(); it != manager.activeEnd(); ++it) { 0084 if ((*it)->readOnly()) { 0085 continue; 0086 } 0087 0088 const QStringList subResources = (*it)->subresources(); 0089 if (subResources.isEmpty()) { 0090 return true; 0091 } 0092 0093 QStringList::ConstIterator subIt; 0094 for (subIt = subResources.begin(); subIt != subResources.end(); ++subIt) { 0095 if (!(*it)->subresourceActive((*subIt))) { 0096 continue; 0097 } 0098 if ((*it)->type() == "imap" || (*it)->type() == "kolab") { 0099 if ((*it)->subresourceType((*subIt)) == "todo" 0100 || (*it)->subresourceType((*subIt)) == "journal" 0101 || !(*subIt).contains("/.INBOX.directory/")) { 0102 continue; 0103 } 0104 } 0105 return true; 0106 } 0107 } 0108 return false; 0109 #endif 0110 #else 0111 qCDebug(TEXT_CALENDAR_LOG) << "Disabled code, port to Akonadi"; 0112 return true; 0113 #endif 0114 } 0115 0116 static bool occurredAlready(const Incidence::Ptr &incidence) 0117 { 0118 Q_ASSERT(incidence); 0119 const QDateTime now = QDateTime::currentDateTime(); 0120 const QDate today = now.date(); 0121 0122 if (incidence->recurs()) { 0123 const QDateTime nextDate = incidence->recurrence()->getNextDateTime(now); 0124 0125 return !nextDate.isValid(); 0126 } else { 0127 const QDateTime incidenceDate = incidence->dateTime(Incidence::RoleDisplayEnd); 0128 if (incidenceDate.isValid()) { 0129 return incidence->allDay() ? (incidenceDate.date() < today) : (incidenceDate < QDateTime::currentDateTime()); 0130 } 0131 } 0132 0133 return false; 0134 } 0135 0136 class KMInvitationFormatterHelper : public KCalUtils::InvitationFormatterHelper 0137 { 0138 public: 0139 KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr &bodyPart, const KCalendarCore::MemoryCalendar::Ptr &calendar) 0140 : mBodyPart(bodyPart) 0141 , mCalendar(calendar) 0142 { 0143 } 0144 0145 QString generateLinkURL(const QString &id) override 0146 { 0147 return mBodyPart->makeLink(id); 0148 } 0149 0150 [[nodiscard]] KCalendarCore::Calendar::Ptr calendar() const override 0151 { 0152 return mCalendar; 0153 } 0154 0155 private: 0156 const MimeTreeParser::MessagePartPtr mBodyPart; 0157 const KCalendarCore::MemoryCalendar::Ptr mCalendar; 0158 }; 0159 0160 static QString getSender(const MimeTreeParser::MessagePart *msgPart) 0161 { 0162 if (auto msg = dynamic_cast<KMime::Message *>(msgPart->content()->topLevel()); msg != nullptr) { 0163 return msg->sender()->asUnicodeString(); 0164 } 0165 return {}; 0166 } 0167 0168 class Formatter : public MessageViewer::MessagePartRendererBase 0169 { 0170 public: 0171 bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override 0172 { 0173 QMimeDatabase db; 0174 auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower())); 0175 if (!mt.isValid() || mt.name() != QLatin1StringView("text/calendar")) { 0176 return false; 0177 } 0178 0179 auto nodeHelper = msgPart->nodeHelper(); 0180 if (!nodeHelper) { 0181 return false; 0182 } 0183 0184 /** Formatting is async now because we need to fetch incidences from akonadi. 0185 Basically this method (format()) will be called twice. The first time 0186 it creates the memento that fetches incidences and returns. 0187 0188 When the memento finishes, this is called a second time, and we can proceed. 0189 0190 BodyPartMementos are documented in MessageViewer/ObjectTreeParser 0191 */ 0192 auto memento = dynamic_cast<MemoryCalendarMemento *>(msgPart->memento()); 0193 0194 if (memento) { 0195 if (memento->finished()) { 0196 KMInvitationFormatterHelper helper(msgPart, memento->calendar()); 0197 QString source; 0198 // If the bodypart does not have a charset specified, we need to fall back to utf8, 0199 // not the KMail fallback encoding, so get the contents as binary and decode explicitly. 0200 if (msgPart->content()->contentType()->parameter(QStringLiteral("charset")).isEmpty()) { 0201 const QByteArray &ba = msgPart->content()->decodedContent(); 0202 source = QString::fromUtf8(ba); 0203 } else { 0204 source = msgPart->text(); 0205 } 0206 0207 MemoryCalendar::Ptr cl(new MemoryCalendar(QTimeZone::systemTimeZone())); 0208 0209 const auto sender = getSender(msgPart.get()); 0210 const QString html = KCalUtils::IncidenceFormatter::formatICalInvitationNoHtml(source, cl, &helper, sender); 0211 0212 if (html.isEmpty()) { 0213 return false; 0214 } 0215 writer->write(html); 0216 } 0217 } else { 0218 auto memento = new MemoryCalendarMemento(); 0219 msgPart->setMemento(memento); 0220 QObject::connect(memento, &MemoryCalendarMemento::update, nodeHelper, &MimeTreeParser::NodeHelper::update); 0221 } 0222 0223 return true; 0224 } 0225 }; 0226 0227 static QString directoryForStatus(Attendee::PartStat status) 0228 { 0229 QString dir; 0230 switch (status) { 0231 case Attendee::Accepted: 0232 dir = QStringLiteral("accepted"); 0233 break; 0234 case Attendee::Tentative: 0235 dir = QStringLiteral("tentative"); 0236 break; 0237 case Attendee::Declined: 0238 dir = QStringLiteral("cancel"); 0239 break; 0240 case Attendee::Delegated: 0241 dir = QStringLiteral("delegated"); 0242 break; 0243 case Attendee::NeedsAction: 0244 dir = QStringLiteral("request"); 0245 break; 0246 default: 0247 break; 0248 } 0249 return dir; 0250 } 0251 0252 static Incidence::Ptr stringToIncidence(const QString &iCal) 0253 { 0254 MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::systemTimeZone())); 0255 ICalFormat format; 0256 ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, iCal); 0257 if (!message) { 0258 // TODO: Error message? 0259 qCWarning(TEXT_CALENDAR_LOG) << "Can't parse this ical string: " << iCal; 0260 return {}; 0261 } 0262 0263 return message->event().dynamicCast<Incidence>(); 0264 } 0265 0266 class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler 0267 { 0268 public: 0269 UrlHandler() 0270 { 0271 // qCDebug(TEXT_CALENDAR_LOG) << "UrlHandler() (iCalendar)"; 0272 } 0273 0274 [[nodiscard]] QString name() const override 0275 { 0276 return QStringLiteral("calendar handler"); 0277 } 0278 0279 [[nodiscard]] Attendee findMyself(const Incidence::Ptr &incidence, const QString &receiver) const 0280 { 0281 const Attendee::List attendees = incidence->attendees(); 0282 const auto idx = findMyself(attendees, receiver); 0283 if (idx >= 0) { 0284 return attendees.at(idx); 0285 } 0286 return {}; 0287 } 0288 0289 [[nodiscard]] int findMyself(const Attendee::List &attendees, const QString &receiver) const 0290 { 0291 // Find myself. There will always be all attendees listed, even if 0292 // only I need to answer it. 0293 for (int i = 0; i < attendees.size(); ++i) { 0294 // match only the email part, not the name 0295 if (KEmailAddress::compareEmail(attendees.at(i).email(), receiver, false)) { 0296 // We are the current one, and even the receiver, note 0297 // this and quit searching. 0298 return i; 0299 } 0300 } 0301 return -1; 0302 } 0303 0304 static bool heuristicalRSVP(const Incidence::Ptr &incidence) 0305 { 0306 bool rsvp = true; // better send superfluously than not at all 0307 const Attendee::List attendees = incidence->attendees(); 0308 Attendee::List::ConstIterator it; 0309 Attendee::List::ConstIterator end(attendees.constEnd()); 0310 for (it = attendees.constBegin(); it != end; ++it) { 0311 if (it == attendees.constBegin()) { 0312 rsvp = (*it).RSVP(); // use what the first one has 0313 } else { 0314 if ((*it).RSVP() != rsvp) { 0315 rsvp = true; // they differ, default 0316 break; 0317 } 0318 } 0319 } 0320 return rsvp; 0321 } 0322 0323 static Attendee::Role heuristicalRole(const Incidence::Ptr &incidence) 0324 { 0325 Attendee::Role role = Attendee::OptParticipant; 0326 const Attendee::List attendees = incidence->attendees(); 0327 Attendee::List::ConstIterator it; 0328 Attendee::List::ConstIterator end = attendees.constEnd(); 0329 0330 for (it = attendees.constBegin(); it != end; ++it) { 0331 if (it == attendees.constBegin()) { 0332 role = (*it).role(); // use what the first one has 0333 } else { 0334 if ((*it).role() != role) { 0335 role = Attendee::OptParticipant; // they differ, default 0336 break; 0337 } 0338 } 0339 } 0340 return role; 0341 } 0342 0343 static Attachment findAttachment(const QString &name, const QString &iCal) 0344 { 0345 Incidence::Ptr incidence = stringToIncidence(iCal); 0346 0347 // get the attachment by name from the incidence 0348 Attachment::List attachments = incidence->attachments(); 0349 Attachment attachment; 0350 const Attachment::List::ConstIterator end = attachments.constEnd(); 0351 for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) { 0352 if ((*it).label() == name) { 0353 attachment = *it; 0354 break; 0355 } 0356 } 0357 0358 if (attachment.isEmpty()) { 0359 KMessageBox::error(nullptr, i18n("No attachment named \"%1\" found in the invitation.", name)); 0360 return Attachment(); 0361 } 0362 0363 if (attachment.isUri()) { 0364 bool fileExists = false; 0365 QUrl attachmentUrl(attachment.uri()); 0366 if (attachmentUrl.isLocalFile()) { 0367 fileExists = QFile::exists(attachmentUrl.toLocalFile()); 0368 } else { 0369 auto job = KIO::stat(attachmentUrl, KIO::StatJob::SourceSide, KIO::StatBasic); 0370 fileExists = job->exec(); 0371 } 0372 if (!fileExists) { 0373 KMessageBox::information(nullptr, 0374 i18n("The invitation attachment \"%1\" is a web link that " 0375 "is inaccessible from this computer. Please ask the event " 0376 "organizer to resend the invitation with this attachment " 0377 "stored inline instead of a link.", 0378 attachmentUrl.toDisplayString())); 0379 return Attachment(); 0380 } 0381 } 0382 return attachment; 0383 } 0384 0385 static QString findReceiver(KMime::Content *node) 0386 { 0387 if (!node || !node->topLevel()) { 0388 return {}; 0389 } 0390 0391 QString receiver; 0392 KIdentityManagementCore::IdentityManager *im = KIdentityManagementCore::IdentityManager::self(); 0393 0394 KMime::Types::Mailbox::List addrs; 0395 if (auto header = node->topLevel()->header<KMime::Headers::To>()) { 0396 addrs = header->mailboxes(); 0397 } 0398 int found = 0; 0399 QList<KMime::Types::Mailbox>::const_iterator end = addrs.constEnd(); 0400 for (QList<KMime::Types::Mailbox>::const_iterator it = addrs.constBegin(); it != end; ++it) { 0401 if (im->identityForAddress(QLatin1StringView((*it).address())) != KIdentityManagementCore::Identity::null()) { 0402 // Ok, this could be us 0403 ++found; 0404 receiver = QLatin1StringView((*it).address()); 0405 } 0406 } 0407 0408 KMime::Types::Mailbox::List ccaddrs; 0409 if (auto header = node->topLevel()->header<KMime::Headers::Cc>()) { 0410 ccaddrs = header->mailboxes(); 0411 } 0412 end = ccaddrs.constEnd(); 0413 for (QList<KMime::Types::Mailbox>::const_iterator it = ccaddrs.constBegin(); it != end; ++it) { 0414 if (im->identityForAddress(QLatin1StringView((*it).address())) != KIdentityManagementCore::Identity::null()) { 0415 // Ok, this could be us 0416 ++found; 0417 receiver = QLatin1StringView((*it).address()); 0418 } 0419 } 0420 if (found != 1) { 0421 QStringList possibleAddrs; 0422 bool ok; 0423 QString selectMessage; 0424 if (found == 0) { 0425 selectMessage = i18n( 0426 "<qt>None of your identities match the receiver of this message,<br/>" 0427 "please choose which of the following addresses is yours,<br/> if any, " 0428 "or select one of your identities to use in the reply:</qt>"); 0429 possibleAddrs += im->allEmails(); 0430 } else { 0431 selectMessage = i18n( 0432 "<qt>Several of your identities match the receiver of this message,<br/>" 0433 "please choose which of the following addresses is yours:</qt>"); 0434 possibleAddrs.reserve(addrs.count() + ccaddrs.count()); 0435 for (const KMime::Types::Mailbox &mbx : std::as_const(addrs)) { 0436 possibleAddrs.append(QLatin1StringView(mbx.address())); 0437 } 0438 for (const KMime::Types::Mailbox &mbx : std::as_const(ccaddrs)) { 0439 possibleAddrs.append(QLatin1StringView(mbx.address())); 0440 } 0441 } 0442 0443 // select default identity by default 0444 const QString defaultAddr = im->defaultIdentity().primaryEmailAddress(); 0445 const int defaultIndex = qMax(0, possibleAddrs.indexOf(defaultAddr)); 0446 0447 receiver = QInputDialog::getItem(nullptr, i18n("Select Address"), selectMessage, possibleAddrs, defaultIndex, false, &ok); 0448 0449 if (!ok) { 0450 receiver.clear(); 0451 } 0452 } 0453 return receiver; 0454 } 0455 0456 [[nodiscard]] Attendee setStatusOnMyself(const Incidence::Ptr &incidence, const Attendee &myself, Attendee::PartStat status, const QString &receiver) const 0457 { 0458 QString name; 0459 QString email; 0460 KEmailAddress::extractEmailAddressAndName(receiver, email, name); 0461 if (name.isEmpty() && !myself.isNull()) { 0462 name = myself.name(); 0463 } 0464 if (email.isEmpty() && !myself.isNull()) { 0465 email = myself.email(); 0466 } 0467 Q_ASSERT(!email.isEmpty()); // delivery must be possible 0468 0469 Attendee newMyself(name, 0470 email, 0471 true, // RSVP, otherwise we would not be here 0472 status, 0473 !myself.isNull() ? myself.role() : heuristicalRole(incidence), 0474 myself.uid()); 0475 if (!myself.isNull()) { 0476 newMyself.setDelegate(myself.delegate()); 0477 newMyself.setDelegator(myself.delegator()); 0478 } 0479 0480 // Make sure only ourselves is in the event 0481 incidence->clearAttendees(); 0482 if (!newMyself.isNull()) { 0483 incidence->addAttendee(newMyself); 0484 } 0485 return newMyself; 0486 } 0487 0488 enum MailType { 0489 Answer, 0490 Delegation, 0491 Forward, 0492 DeclineCounter, 0493 }; 0494 0495 bool mailICal(const QString &receiver, 0496 const QString &to, 0497 const QString &iCal, 0498 const QString &subject, 0499 const QString &status, 0500 bool delMessage, 0501 Viewer *viewerInstance) const 0502 { 0503 qCDebug(TEXT_CALENDAR_LOG) << "Mailing message:" << iCal; 0504 0505 KMime::Message::Ptr msg(new KMime::Message); 0506 if (MessageViewer::MessageViewerSettings::self()->exchangeCompatibleInvitations()) { 0507 msg->subject()->fromUnicodeString(status, "utf-8"); 0508 QString tsubject = subject; 0509 tsubject.remove(i18n("Answer: ")); 0510 if (status == QLatin1StringView("cancel")) { 0511 msg->subject()->fromUnicodeString(i18nc("Not able to attend.", "Declined: %1", tsubject), "utf-8"); 0512 } else if (status == QLatin1StringView("tentative")) { 0513 msg->subject()->fromUnicodeString(i18nc("Unsure if it is possible to attend.", "Tentative: %1", tsubject), "utf-8"); 0514 } else if (status == QLatin1StringView("accepted")) { 0515 msg->subject()->fromUnicodeString(i18nc("Accepted the invitation.", "Accepted: %1", tsubject), "utf-8"); 0516 } else { 0517 msg->subject()->fromUnicodeString(subject, "utf-8"); 0518 } 0519 } else { 0520 msg->subject()->fromUnicodeString(subject, "utf-8"); 0521 } 0522 msg->to()->fromUnicodeString(to, "utf-8"); 0523 msg->from()->fromUnicodeString(receiver, "utf-8"); 0524 msg->date()->setDateTime(QDateTime::currentDateTime()); 0525 0526 if (MessageViewer::MessageViewerSettings::self()->legacyBodyInvites()) { 0527 auto ct = msg->contentType(); // create 0528 ct->setMimeType("text/calendar"); 0529 ct->setCharset("utf-8"); 0530 ct->setName(QStringLiteral("cal.ics"), "utf-8"); 0531 ct->setParameter(QStringLiteral("method"), QStringLiteral("reply")); 0532 0533 auto disposition = new KMime::Headers::ContentDisposition; 0534 disposition->setDisposition(KMime::Headers::CDinline); 0535 msg->setHeader(disposition); 0536 msg->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); 0537 const QString answer = i18n("Invitation answer attached."); 0538 msg->setBody(answer.toUtf8()); 0539 } else { 0540 // We need to set following 4 lines by hand else KMime::Content::addContent 0541 // will create a new Content instance for us to attach the main message 0542 // what we don't need cause we already have the main message instance where 0543 // 2 additional messages are attached. 0544 KMime::Headers::ContentType *ct = msg->contentType(); 0545 ct->setMimeType("multipart/mixed"); 0546 ct->setBoundary(KMime::multiPartBoundary()); 0547 0548 // Set the first multipart, the body message. 0549 auto bodyMessage = new KMime::Content; 0550 auto bodyDisposition = new KMime::Headers::ContentDisposition; 0551 bodyDisposition->setDisposition(KMime::Headers::CDinline); 0552 auto bodyMessageCt = bodyMessage->contentType(); 0553 bodyMessageCt->setMimeType("text/plain"); 0554 bodyMessageCt->setCharset("utf-8"); 0555 bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); 0556 const QString answer = i18n("Invitation answer attached."); 0557 bodyMessage->setBody(answer.toUtf8()); 0558 bodyMessage->setHeader(bodyDisposition); 0559 msg->appendContent(bodyMessage); 0560 0561 // Set the second multipart, the attachment. 0562 auto attachMessage = new KMime::Content; 0563 auto attachDisposition = new KMime::Headers::ContentDisposition; 0564 attachDisposition->setDisposition(KMime::Headers::CDattachment); 0565 auto attachCt = attachMessage->contentType(); 0566 attachCt->setMimeType("text/calendar"); 0567 attachCt->setCharset("utf-8"); 0568 attachCt->setName(QStringLiteral("cal.ics"), "utf-8"); 0569 attachCt->setParameter(QStringLiteral("method"), QStringLiteral("reply")); 0570 attachMessage->setHeader(attachDisposition); 0571 attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); 0572 attachMessage->setBody(KMime::CRLFtoLF(iCal.toUtf8())); 0573 msg->appendContent(attachMessage); 0574 } 0575 0576 // Try and match the receiver with an identity. 0577 // Setting the identity here is important, as that is used to select the correct 0578 // transport later 0579 KIdentityManagementCore::IdentityManager *im = KIdentityManagementCore::IdentityManager::self(); 0580 const KIdentityManagementCore::Identity identity = im->identityForAddress(findReceiver(viewerInstance->message().data())); 0581 0582 const bool nullIdentity = (identity == KIdentityManagementCore::Identity::null()); 0583 0584 if (!nullIdentity) { 0585 auto x_header = new KMime::Headers::Generic("X-KMail-Identity"); 0586 x_header->from7BitString(QByteArray::number(identity.uoid())); 0587 msg->setHeader(x_header); 0588 } 0589 0590 const bool identityHasTransport = !identity.transport().isEmpty(); 0591 int transportId = -1; 0592 if (!nullIdentity && identityHasTransport) { 0593 transportId = identity.transport().toInt(); 0594 } else { 0595 transportId = TransportManager::self()->defaultTransportId(); 0596 } 0597 if (transportId == -1) { 0598 if (!TransportManager::self()->showTransportCreationDialog(nullptr, TransportManager::IfNoTransportExists)) { 0599 return false; 0600 } 0601 transportId = TransportManager::self()->defaultTransportId(); 0602 } 0603 auto header = new KMime::Headers::Generic("X-KMail-Transport"); 0604 header->fromUnicodeString(QString::number(transportId), "utf-8"); 0605 msg->setHeader(header); 0606 0607 // Outlook will only understand the reply if the From: header is the 0608 // same as the To: header of the invitation message. 0609 if (!MessageViewer::MessageViewerSettings::self()->legacyMangleFromToHeaders()) { 0610 if (identity != KIdentityManagementCore::Identity::null()) { 0611 msg->from()->fromUnicodeString(identity.fullEmailAddr(), "utf-8"); 0612 } 0613 // Remove BCC from identity on ical invitations (kolab/issue474) 0614 msg->removeHeader<KMime::Headers::Bcc>(); 0615 } 0616 0617 msg->assemble(); 0618 MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(transportId); 0619 0620 auto job = new Akonadi::MessageQueueJob; 0621 0622 job->addressAttribute().setTo(QStringList() << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(to))); 0623 job->transportAttribute().setTransportId(transport->id()); 0624 0625 if (transport->specifySenderOverwriteAddress()) { 0626 job->addressAttribute().setFrom( 0627 KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress()))); 0628 } else { 0629 job->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(msg->from()->asUnicodeString()))); 0630 } 0631 0632 job->setMessage(msg); 0633 0634 if (!job->exec()) { 0635 qCWarning(TEXT_CALENDAR_LOG) << "Error queuing message in outbox:" << job->errorText(); 0636 return false; 0637 } 0638 // We are not notified when mail was sent, so assume it was sent when queued. 0639 if (delMessage && MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { 0640 viewerInstance->deleteMessage(); 0641 } 0642 return true; 0643 } 0644 0645 bool mail(Viewer *viewerInstance, 0646 const Incidence::Ptr &incidence, 0647 const QString &status, 0648 iTIPMethod method = iTIPReply, 0649 const QString &receiver = QString(), 0650 const QString &to = QString(), 0651 MailType type = Answer) const 0652 { 0653 // status is accepted/tentative/declined 0654 ICalFormat format; 0655 format.setTimeZone(QTimeZone::systemTimeZone()); 0656 QString msg = format.createScheduleMessage(incidence, method); 0657 QString summary = incidence->summary(); 0658 if (summary.isEmpty()) { 0659 summary = i18n("Incidence with no summary"); 0660 } 0661 QString subject; 0662 switch (type) { 0663 case Answer: 0664 subject = i18n("Answer: %1", summary); 0665 break; 0666 case Delegation: 0667 subject = i18n("Delegated: %1", summary); 0668 break; 0669 case Forward: 0670 subject = i18n("Forwarded: %1", summary); 0671 break; 0672 case DeclineCounter: 0673 subject = i18n("Declined Counter Proposal: %1", summary); 0674 break; 0675 } 0676 0677 // Set the organizer to the sender, if the ORGANIZER hasn't been set. 0678 if (incidence->organizer().isEmpty()) { 0679 QString tname; 0680 QString temail; 0681 KMime::Message::Ptr message = viewerInstance->message(); 0682 KEmailAddress::extractEmailAddressAndName(message->sender()->asUnicodeString(), temail, tname); 0683 incidence->setOrganizer(Person(tname, temail)); 0684 } 0685 0686 QString recv = to; 0687 if (recv.isEmpty()) { 0688 recv = incidence->organizer().fullName(); 0689 } 0690 return mailICal(receiver, recv, msg, subject, status, type != Forward, viewerInstance); 0691 } 0692 0693 bool saveFile(const QString &receiver, const QString &iCal, const QString &type, MimeTreeParser::Interface::BodyPart *bodyPart) const 0694 { 0695 auto memento = dynamic_cast<MemoryCalendarMemento *>(bodyPart->memento()); 0696 // This will block. There's no way to make it async without refactoring the memento mechanism 0697 0698 auto itipHandler = new SyncItipHandler(receiver, iCal, type, memento->calendar()); 0699 0700 // If result is ResultCancelled, then we don't show the message box and return false so kmail 0701 // doesn't delete the e-mail. 0702 qCDebug(TEXT_CALENDAR_LOG) << "ITIPHandler result was " << itipHandler->result(); 0703 const Akonadi::ITIPHandler::Result res = itipHandler->result(); 0704 if (res == Akonadi::ITIPHandler::ResultError) { 0705 const QString errorMessage = itipHandler->errorMessage(); 0706 if (!errorMessage.isEmpty()) { 0707 qCCritical(TEXT_CALENDAR_LOG) << "Error while processing invitation: " << errorMessage; 0708 KMessageBox::error(nullptr, errorMessage); 0709 } 0710 return false; 0711 } 0712 0713 return res; 0714 } 0715 0716 [[nodiscard]] bool cancelPastInvites(const Incidence::Ptr incidence, const QString &path) const 0717 { 0718 QString warnStr; 0719 QDateTime now = QDateTime::currentDateTime(); 0720 QDate today = now.date(); 0721 Incidence::IncidenceType type = Incidence::TypeUnknown; 0722 const bool occurred = occurredAlready(incidence); 0723 if (incidence->type() == Incidence::TypeEvent) { 0724 type = Incidence::TypeEvent; 0725 Event::Ptr event = incidence.staticCast<Event>(); 0726 if (!event->allDay()) { 0727 if (occurred) { 0728 warnStr = i18n("\"%1\" occurred already.", event->summary()); 0729 } else if (event->dtStart() <= now && now <= event->dtEnd()) { 0730 warnStr = i18n("\"%1\" is currently in-progress.", event->summary()); 0731 } 0732 } else { 0733 if (occurred) { 0734 warnStr = i18n("\"%1\" occurred already.", event->summary()); 0735 } else if (event->dtStart().date() <= today && today <= event->dtEnd().date()) { 0736 warnStr = i18n("\"%1\", happening all day today, is currently in-progress.", event->summary()); 0737 } 0738 } 0739 } else if (incidence->type() == Incidence::TypeTodo) { 0740 type = Incidence::TypeTodo; 0741 Todo::Ptr todo = incidence.staticCast<Todo>(); 0742 if (!todo->allDay()) { 0743 if (todo->hasDueDate()) { 0744 if (todo->dtDue() < now) { 0745 warnStr = i18n("\"%1\" is past due.", todo->summary()); 0746 } else if (todo->hasStartDate() && todo->dtStart() <= now && now <= todo->dtDue()) { 0747 warnStr = i18n("\"%1\" is currently in-progress.", todo->summary()); 0748 } 0749 } else if (todo->hasStartDate()) { 0750 if (todo->dtStart() < now) { 0751 warnStr = i18n("\"%1\" has already started.", todo->summary()); 0752 } 0753 } 0754 } else { 0755 if (todo->hasDueDate()) { 0756 if (todo->dtDue().date() < today) { 0757 warnStr = i18n("\"%1\" is past due.", todo->summary()); 0758 } else if (todo->hasStartDate() && todo->dtStart().date() <= today && today <= todo->dtDue().date()) { 0759 warnStr = i18n("\"%1\", happening all-day today, is currently in-progress.", todo->summary()); 0760 } 0761 } else if (todo->hasStartDate()) { 0762 if (todo->dtStart().date() < today) { 0763 warnStr = i18n("\"%1\", happening all day, has already started.", todo->summary()); 0764 } 0765 } 0766 } 0767 } 0768 0769 if (!warnStr.isEmpty()) { 0770 QString queryStr; 0771 KGuiItem yesItem; 0772 KGuiItem noItem; 0773 if (path == QLatin1StringView("accept")) { 0774 if (type == Incidence::TypeTodo) { 0775 queryStr = i18n("Do you still want to accept the task?"); 0776 } else { 0777 queryStr = i18n("Do you still want to accept the invitation?"); 0778 } 0779 yesItem.setText(i18nc("@action:button", "Accept")); 0780 yesItem.setIconName(QStringLiteral("dialog-ok")); 0781 } else if (path == QLatin1StringView("accept_conditionally")) { 0782 if (type == Incidence::TypeTodo) { 0783 queryStr = i18n("Do you still want to send conditional acceptance of the invitation?"); 0784 } else { 0785 queryStr = i18n("Do you still want to send conditional acceptance of the task?"); 0786 } 0787 yesItem.setText(i18nc("@action:button", "Send")); 0788 yesItem.setIconName(QStringLiteral("mail-send")); 0789 } else if (path == QLatin1StringView("accept_counter")) { 0790 queryStr = i18n("Do you still want to accept the counter proposal?"); 0791 yesItem.setText(i18nc("@action:button", "Accept")); 0792 yesItem.setIconName(QStringLiteral("dialog-ok")); 0793 } else if (path == QLatin1StringView("counter")) { 0794 queryStr = i18n("Do you still want to send a counter proposal?"); 0795 yesItem.setText(i18nc("@action:button", "Send")); 0796 yesItem.setIconName(QStringLiteral("mail-send")); 0797 } else if (path == QLatin1StringView("decline")) { 0798 queryStr = i18n("Do you still want to send a decline response?"); 0799 yesItem.setText(i18nc("@action:button", "Send")); 0800 yesItem.setIconName(QStringLiteral("mail-send")); 0801 } else if (path == QLatin1StringView("decline_counter")) { 0802 queryStr = i18n("Do you still want to decline the counter proposal?"); 0803 yesItem.setText(i18nc("@action:button", "Decline")); 0804 } else if (path == QLatin1StringView("reply")) { 0805 queryStr = i18n("Do you still want to record this response in your calendar?"); 0806 yesItem.setText(i18nc("@action:button", "Record")); 0807 } else if (path == QLatin1StringView("delegate")) { 0808 if (type == Incidence::TypeTodo) { 0809 queryStr = i18n("Do you still want to delegate this task?"); 0810 } else { 0811 queryStr = i18n("Do you still want to delegate this invitation?"); 0812 } 0813 yesItem.setText(i18nc("@action:button", "Delegate")); 0814 } else if (path == QLatin1StringView("forward")) { 0815 if (type == Incidence::TypeTodo) { 0816 queryStr = i18n("Do you still want to forward this task?"); 0817 } else { 0818 queryStr = i18n("Do you still want to forward this invitation?"); 0819 } 0820 yesItem.setText(i18nc("@action:button", "Forward")); 0821 yesItem.setIconName(QStringLiteral("mail-forward")); 0822 } else if (path == QLatin1StringView("cancel")) { 0823 if (type == Incidence::TypeTodo) { 0824 queryStr = i18n("Do you still want to cancel this task?"); 0825 yesItem.setText(i18nc("@action:button", "Cancel Task")); 0826 } else { 0827 queryStr = i18n("Do you still want to cancel this invitation?"); 0828 yesItem.setText(i18nc("@action:button", "Cancel Invitation")); 0829 } 0830 yesItem.setIconName(QStringLiteral("dialog-ok")); 0831 noItem.setText(i18nc("@action:button", "Do Not Cancel")); 0832 noItem.setIconName(QStringLiteral("dialog-cancel")); 0833 } else if (path == QLatin1StringView("check_calendar")) { 0834 queryStr = i18n("Do you still want to check your calendar?"); 0835 yesItem.setText(i18nc("@action:button", "Check")); 0836 } else if (path == QLatin1StringView("record")) { 0837 if (type == Incidence::TypeTodo) { 0838 queryStr = i18n("Do you still want to record this task in your calendar?"); 0839 } else { 0840 queryStr = i18n("Do you still want to record this invitation in your calendar?"); 0841 } 0842 yesItem.setText(i18nc("@action:button", "Record")); 0843 } else if (path == QLatin1StringView("cancel")) { 0844 if (type == Incidence::TypeTodo) { 0845 queryStr = i18n("Do you really want to cancel this task?"); 0846 yesItem.setText(i18nc("@action:button", "Cancel Task")); 0847 } else { 0848 queryStr = i18n("Do you really want to cancel this invitation?"); 0849 yesItem.setText(i18nc("@action:button", "Cancel Invitation")); 0850 } 0851 yesItem.setIconName(QStringLiteral("dialog-ok")); 0852 noItem.setText(i18nc("@action:button", "Do Not Cancel")); 0853 noItem.setIconName(QStringLiteral("dialog-cancel")); 0854 } else if (path.startsWith(QLatin1StringView("ATTACH:"))) { 0855 return false; 0856 } else { 0857 queryStr = i18n("%1?", path); 0858 yesItem = KStandardGuiItem::ok(); 0859 } 0860 0861 if (noItem.text().isEmpty()) { 0862 noItem = KStandardGuiItem::cancel(); 0863 } 0864 const int answer = KMessageBox::warningTwoActions(nullptr, i18n("%1\n%2", warnStr, queryStr), QString(), yesItem, noItem); 0865 if (answer == KMessageBox::ButtonCode::SecondaryAction) { 0866 return true; 0867 } 0868 } 0869 return false; 0870 } 0871 0872 bool handleInvitation(const QString &iCal, Attendee::PartStat status, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const 0873 { 0874 bool ok = true; 0875 const QString receiver = findReceiver(part->content()); 0876 qCDebug(TEXT_CALENDAR_LOG) << receiver; 0877 0878 if (receiver.isEmpty()) { 0879 // Must be some error. Still return true though, since we did handle it 0880 return true; 0881 } 0882 0883 Incidence::Ptr incidence = stringToIncidence(iCal); 0884 qCDebug(TEXT_CALENDAR_LOG) << "Handling invitation: uid is : " << incidence->uid() << "; schedulingId is:" << incidence->schedulingID() 0885 << "; Attendee::PartStat = " << status; 0886 0887 // get comment for tentative acceptance 0888 if (askForComment(status)) { 0889 QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr); 0890 dlg->setWindowTitle(i18nc("@title:window", "Reaction to Invitation")); 0891 QString comment; 0892 if (dlg->exec()) { 0893 comment = dlg->comment(); 0894 delete dlg; 0895 } else { 0896 delete dlg; 0897 return true; 0898 } 0899 0900 if (comment.trimmed().isEmpty()) { 0901 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks")); 0902 return true; 0903 } else { 0904 incidence->addComment(comment); 0905 } 0906 } 0907 0908 // First, save it for KOrganizer to handle 0909 const QString dir = directoryForStatus(status); 0910 if (dir.isEmpty()) { 0911 qCWarning(TEXT_CALENDAR_LOG) << "Impossible to understand status: " << status; 0912 return true; // unknown status 0913 } 0914 if (status != Attendee::Delegated) { 0915 // we do that below for delegated incidences 0916 if (!saveFile(receiver, iCal, dir, part)) { 0917 return false; 0918 } 0919 } 0920 0921 QString delegateString; 0922 bool delegatorRSVP = false; 0923 if (status == Attendee::Delegated) { 0924 DelegateSelector dlg; 0925 if (dlg.exec() == QDialog::Rejected) { 0926 return true; 0927 } 0928 delegateString = dlg.delegate(); 0929 delegatorRSVP = dlg.rsvp(); 0930 if (delegateString.isEmpty()) { 0931 return true; 0932 } 0933 if (KEmailAddress::compareEmail(delegateString, incidence->organizer().email(), false)) { 0934 KMessageBox::error(nullptr, i18n("Delegation to organizer is not possible.")); 0935 return true; 0936 } 0937 } 0938 0939 if (!incidence) { 0940 return false; 0941 } 0942 0943 const Attendee myself = findMyself(incidence, receiver); 0944 0945 // find our delegator, we need to inform him as well 0946 QString delegator; 0947 if (status != Attendee::NeedsAction && !myself.isNull() && !myself.delegator().isEmpty()) { 0948 const Attendee::List attendees = incidence->attendees(); 0949 Attendee::List::ConstIterator end = attendees.constEnd(); 0950 for (Attendee::List::ConstIterator it = attendees.constBegin(); it != end; ++it) { 0951 if (KEmailAddress::compareEmail((*it).fullName(), myself.delegator(), false) && (*it).status() == Attendee::Delegated) { 0952 delegator = (*it).fullName(); 0953 delegatorRSVP = (*it).RSVP(); 0954 break; 0955 } 0956 } 0957 } 0958 0959 if (status != Attendee::NeedsAction 0960 && ((!myself.isNull() && (myself.RSVP() || myself.status() == Attendee::NeedsAction)) || heuristicalRSVP(incidence))) { 0961 Attendee newMyself = setStatusOnMyself(incidence, myself, status, receiver); 0962 if (!newMyself.isNull() && status == Attendee::Delegated) { 0963 newMyself.setDelegate(delegateString); 0964 newMyself.setRSVP(delegatorRSVP); 0965 } 0966 ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver); 0967 0968 // check if we need to inform our delegator about this as well 0969 if (!newMyself.isNull() && (status == Attendee::Accepted || status == Attendee::Declined) && !delegator.isEmpty()) { 0970 if (delegatorRSVP || status == Attendee::Declined) { 0971 ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver, delegator); 0972 } 0973 } 0974 } else if (myself.isNull() && (status != Attendee::Declined && status != Attendee::NeedsAction)) { 0975 // forwarded invitation 0976 QString name; 0977 QString email; 0978 KEmailAddress::extractEmailAddressAndName(receiver, email, name); 0979 if (!email.isEmpty()) { 0980 Attendee newMyself(name, 0981 email, 0982 true, // RSVP, otherwise we would not be here 0983 status, 0984 heuristicalRole(incidence), 0985 QString()); 0986 incidence->clearAttendees(); 0987 incidence->addAttendee(newMyself); 0988 ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver); 0989 } 0990 } else { 0991 if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { 0992 viewerInstance->deleteMessage(); 0993 } 0994 } 0995 0996 // create invitation for the delegate (same as the original invitation 0997 // with the delegate as additional attendee), we also use that for updating 0998 // our calendar 0999 if (status == Attendee::Delegated) { 1000 incidence = stringToIncidence(iCal); 1001 auto attendees = incidence->attendees(); 1002 const int myselfIdx = findMyself(attendees, receiver); 1003 if (myselfIdx >= 0) { 1004 attendees[myselfIdx].setStatus(status); 1005 attendees[myselfIdx].setDelegate(delegateString); 1006 incidence->setAttendees(attendees); 1007 } 1008 QString name; 1009 QString email; 1010 KEmailAddress::extractEmailAddressAndName(delegateString, email, name); 1011 Attendee delegate(name, email, true); 1012 delegate.setDelegator(receiver); 1013 incidence->addAttendee(delegate); 1014 1015 ICalFormat format; 1016 format.setTimeZone(QTimeZone::systemTimeZone()); 1017 const QString iCal = format.createScheduleMessage(incidence, iTIPRequest); 1018 if (!saveFile(receiver, iCal, dir, part)) { 1019 return false; 1020 } 1021 1022 ok = mail(viewerInstance, incidence, dir, iTIPRequest, receiver, delegateString, Delegation); 1023 } 1024 return ok; 1025 } 1026 1027 void openAttachment(const QString &name, const QString &iCal) const 1028 { 1029 Attachment attachment(findAttachment(name, iCal)); 1030 if (attachment.isEmpty()) { 1031 return; 1032 } 1033 1034 if (attachment.isUri()) { 1035 QDesktopServices::openUrl(QUrl(attachment.uri())); 1036 } else { 1037 // put the attachment in a temporary file and launch it 1038 QTemporaryFile *file = nullptr; 1039 QMimeDatabase db; 1040 QStringList patterns = db.mimeTypeForName(attachment.mimeType()).globPatterns(); 1041 if (!patterns.empty()) { 1042 QString pattern = patterns.at(0); 1043 file = new QTemporaryFile(QDir::tempPath() + QLatin1StringView("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*'))); 1044 } else { 1045 file = new QTemporaryFile(); 1046 } 1047 file->setAutoRemove(false); 1048 file->open(); 1049 file->setPermissions(QFile::ReadUser); 1050 file->write(QByteArray::fromBase64(attachment.data())); 1051 file->close(); 1052 1053 auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file->fileName()), attachment.mimeType()); 1054 job->setDeleteTemporaryFile(true); 1055 job->start(); 1056 delete file; 1057 } 1058 } 1059 1060 [[nodiscard]] bool saveAsAttachment(const QString &name, const QString &iCal) const 1061 { 1062 Attachment a(findAttachment(name, iCal)); 1063 if (a.isEmpty()) { 1064 return false; 1065 } 1066 1067 // get the saveas file name 1068 const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18n("Save Invitation Attachment"), name, QString()); 1069 1070 if (saveAsFile.isEmpty()) { 1071 return false; 1072 } 1073 1074 bool stat = false; 1075 if (a.isUri()) { 1076 // save the attachment url 1077 auto job = KIO::file_copy(QUrl(a.uri()), QUrl::fromLocalFile(saveAsFile)); 1078 stat = job->exec(); 1079 } else { 1080 // put the attachment in a temporary file and save it 1081 QTemporaryFile *file{nullptr}; 1082 QMimeDatabase db; 1083 QStringList patterns = db.mimeTypeForName(a.mimeType()).globPatterns(); 1084 if (!patterns.empty()) { 1085 QString pattern = patterns.at(0); 1086 file = new QTemporaryFile(QDir::tempPath() + QLatin1StringView("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*'))); 1087 } else { 1088 file = new QTemporaryFile(); 1089 } 1090 file->setAutoRemove(false); 1091 file->open(); 1092 file->setPermissions(QFile::ReadUser); 1093 file->write(QByteArray::fromBase64(a.data())); 1094 file->close(); 1095 const QString filename = file->fileName(); 1096 delete file; 1097 1098 auto job = KIO::file_copy(QUrl::fromLocalFile(filename), QUrl::fromLocalFile(saveAsFile)); 1099 stat = job->exec(); 1100 } 1101 return stat; 1102 } 1103 1104 void showCalendar(QDate date) const 1105 { 1106 // Start or activate KOrganizer. When Kontact is running it will switch to KOrganizer view 1107 const auto korgaService = KService::serviceByDesktopName(QStringLiteral("org.kde.korganizer")); 1108 1109 if (!korgaService) { 1110 qCWarning(TEXT_CALENDAR_LOG) << "Could not find KOrganizer"; 1111 return; 1112 } 1113 1114 auto job = new KIO::ApplicationLauncherJob(korgaService); 1115 QObject::connect(job, &KJob::finished, job, [date](KJob *job) { 1116 if (job->error()) { 1117 qCWarning(TEXT_CALENDAR_LOG) << "failed to run korganizer" << job->errorString(); 1118 return; 1119 } 1120 1121 OrgKdeKorganizerCalendarInterface iface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QDBusConnection::sessionBus(), nullptr); 1122 if (!iface.isValid()) { 1123 qCDebug(TEXT_CALENDAR_LOG) << "Calendar interface is not valid! " << iface.lastError().message(); 1124 return; 1125 } 1126 iface.showEventView(); 1127 iface.showDate(date); 1128 }); 1129 1130 job->start(); 1131 } 1132 1133 bool handleIgnore(Viewer *viewerInstance) const 1134 { 1135 // simply move the message to trash 1136 viewerInstance->deleteMessage(); 1137 return true; 1138 } 1139 1140 bool handleDeclineCounter(const QString &iCal, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const 1141 { 1142 const QString receiver(findReceiver(part->content())); 1143 if (receiver.isEmpty()) { 1144 return true; 1145 } 1146 Incidence::Ptr incidence(stringToIncidence(iCal)); 1147 if (askForComment(Attendee::Declined)) { 1148 QPointer<ReactionToInvitationDialog> dlg = new ReactionToInvitationDialog(nullptr); 1149 dlg->setWindowTitle(i18nc("@title:window", "Decline Counter Proposal")); 1150 QString comment; 1151 if (dlg->exec()) { 1152 comment = dlg->comment(); 1153 delete dlg; 1154 } else { 1155 delete dlg; 1156 return true; 1157 } 1158 1159 if (comment.trimmed().isEmpty()) { 1160 KMessageBox::error(nullptr, i18n("You forgot to add proposal. Please add it. Thanks")); 1161 return true; 1162 } else { 1163 incidence->addComment(comment); 1164 } 1165 } 1166 return mail(viewerInstance, incidence, QStringLiteral("declinecounter"), KCalendarCore::iTIPDeclineCounter, receiver, QString(), DeclineCounter); 1167 } 1168 1169 bool counterProposal(const QString &iCal, MimeTreeParser::Interface::BodyPart *part) const 1170 { 1171 const QString receiver = findReceiver(part->content()); 1172 if (receiver.isEmpty()) { 1173 return true; 1174 } 1175 1176 // Don't delete the invitation here in any case, if the counter proposal 1177 // is declined you might need it again. 1178 return saveFile(receiver, iCal, QStringLiteral("counter"), part); 1179 } 1180 1181 bool handleClick(Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const override 1182 { 1183 // filter out known paths that don't belong to this type of urlmanager. 1184 // kolab/issue4054 msg27201 1185 if (path.contains(QLatin1StringView("addToAddressBook:")) || path.contains(QLatin1StringView("updateToAddressBook"))) { 1186 return false; 1187 } 1188 1189 if (!hasMyWritableEventsFolders(QStringLiteral("calendar"))) { 1190 KMessageBox::error(nullptr, 1191 i18n("You have no writable calendar folders for invitations, " 1192 "so storing or saving a response will not be possible.\n" 1193 "Please create at least 1 writable events calendar and re-sync.")); 1194 return false; 1195 } 1196 1197 // If the bodypart does not have a charset specified, we need to fall back to utf8, 1198 // not the KMail fallback encoding, so get the contents as binary and decode explicitly. 1199 QString iCal; 1200 if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) { 1201 const QByteArray &ba = part->content()->decodedContent(); 1202 iCal = QString::fromUtf8(ba); 1203 } else { 1204 iCal = part->content()->decodedText(); 1205 } 1206 1207 Incidence::Ptr incidence = stringToIncidence(iCal); 1208 if (!incidence) { 1209 KMessageBox::error(nullptr, 1210 i18n("The calendar invitation stored in this email message is broken in some way. " 1211 "Unable to continue.")); 1212 return false; 1213 } 1214 1215 bool result = false; 1216 if (cancelPastInvites(incidence, path)) { 1217 return result; 1218 } 1219 1220 if (path == QLatin1StringView("accept")) { 1221 result = handleInvitation(iCal, Attendee::Accepted, part, viewerInstance); 1222 } else if (path == QLatin1StringView("accept_conditionally")) { 1223 result = handleInvitation(iCal, Attendee::Tentative, part, viewerInstance); 1224 } else if (path == QLatin1StringView("counter")) { 1225 result = counterProposal(iCal, part); 1226 } else if (path == QLatin1StringView("ignore")) { 1227 result = handleIgnore(viewerInstance); 1228 } else if (path == QLatin1StringView("decline")) { 1229 result = handleInvitation(iCal, Attendee::Declined, part, viewerInstance); 1230 } else if (path == QLatin1StringView("decline_counter")) { 1231 result = handleDeclineCounter(iCal, part, viewerInstance); 1232 } else if (path == QLatin1StringView("postpone")) { 1233 result = handleInvitation(iCal, Attendee::NeedsAction, part, viewerInstance); 1234 } else if (path == QLatin1StringView("delegate")) { 1235 result = handleInvitation(iCal, Attendee::Delegated, part, viewerInstance); 1236 } else if (path == QLatin1StringView("forward")) { 1237 AttendeeSelector dlg; 1238 if (dlg.exec() == QDialog::Rejected) { 1239 return true; 1240 } 1241 QString fwdTo = dlg.attendees().join(QLatin1StringView(", ")); 1242 if (fwdTo.isEmpty()) { 1243 return true; 1244 } 1245 const QString receiver = findReceiver(part->content()); 1246 result = mail(viewerInstance, incidence, QStringLiteral("forward"), iTIPRequest, receiver, fwdTo, Forward); 1247 } else if (path == QLatin1StringView("check_calendar")) { 1248 incidence = stringToIncidence(iCal); 1249 showCalendar(incidence->dtStart().date()); 1250 return true; 1251 } else if (path == QLatin1StringView("reply") || path == QLatin1StringView("cancel") || path == QLatin1StringView("accept_counter")) { 1252 // These should just be saved with their type as the dir 1253 const QString p = (path == QLatin1StringView("accept_counter") ? QStringLiteral("reply") : path); 1254 if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, p, part)) { 1255 if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { 1256 viewerInstance->deleteMessage(); 1257 } 1258 result = true; 1259 } 1260 } else if (path == QLatin1StringView("record")) { 1261 incidence = stringToIncidence(iCal); 1262 QString summary; 1263 int response = KMessageBox::questionTwoActionsCancel(nullptr, 1264 i18nc("@info", 1265 "The organizer is not expecting a reply to this invitation " 1266 "but you can send them an email message if you desire.\n\n" 1267 "Would you like to send the organizer a message regarding this invitation?\n" 1268 "Press the [Cancel] button to cancel the recording operation."), 1269 i18nc("@title:window", "Send Email to Organizer"), 1270 KGuiItem(i18n("Do Not Send")), 1271 KGuiItem(i18n("Send EMail"))); 1272 1273 switch (response) { 1274 case KMessageBox::Cancel: 1275 break; 1276 case KMessageBox::ButtonCode::SecondaryAction: { // means "send email" 1277 summary = incidence->summary(); 1278 if (!summary.isEmpty()) { 1279 summary = i18n("Re: %1", summary); 1280 } 1281 1282 QUrlQuery query; 1283 query.addQueryItem(QStringLiteral("to"), incidence->organizer().email()); 1284 query.addQueryItem(QStringLiteral("subject"), summary); 1285 QUrl url; 1286 url.setScheme(QStringLiteral("mailto")); 1287 url.setQuery(query); 1288 QDesktopServices::openUrl(url); 1289 } 1290 // fall through 1291 case KMessageBox::ButtonCode::PrimaryAction: // means "do not send" 1292 if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, QStringLiteral("reply"), part)) { 1293 if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { 1294 viewerInstance->deleteMessage(); 1295 result = true; 1296 } 1297 } 1298 showCalendar(incidence->dtStart().date()); 1299 break; 1300 } 1301 } else if (path == QLatin1StringView("delete")) { 1302 viewerInstance->deleteMessage(); 1303 result = true; 1304 } 1305 1306 if (path.startsWith(QLatin1StringView("ATTACH:"))) { 1307 const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); 1308 openAttachment(name, iCal); 1309 } 1310 1311 if (result) { 1312 // do not close the secondary window if an attachment was opened (kolab/issue4317) 1313 if (!path.startsWith(QLatin1StringView("ATTACH:"))) { 1314 qCDebug(TEXT_CALENDAR_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO << "about closing if in a secondary window"; 1315 #if 0 // TODO port to Akonadi 1316 c.closeIfSecondaryWindow(); 1317 #endif 1318 } 1319 } 1320 return result; 1321 } 1322 1323 bool handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &point) const override 1324 { 1325 QString name = path; 1326 if (path.startsWith(QLatin1StringView("ATTACH:"))) { 1327 name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); 1328 } else { 1329 return false; // because it isn't an attachment invitation 1330 } 1331 1332 QString iCal; 1333 if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) { 1334 const QByteArray &ba = part->content()->decodedContent(); 1335 iCal = QString::fromUtf8(ba); 1336 } else { 1337 iCal = part->content()->decodedText(); 1338 } 1339 1340 auto menu = new QMenu(); 1341 QAction *open = menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Attachment")); 1342 QAction *saveas = menu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Attachment As...")); 1343 1344 QAction *a = menu->exec(point, nullptr); 1345 if (a == open) { 1346 openAttachment(name, iCal); 1347 } else if (a == saveas) { 1348 saveAsAttachment(name, iCal); 1349 } 1350 delete menu; 1351 return true; 1352 } 1353 1354 QString statusBarMessage(MimeTreeParser::Interface::BodyPart *, const QString &path) const override 1355 { 1356 if (!path.isEmpty()) { 1357 if (path == QLatin1StringView("accept")) { 1358 return i18n("Accept invitation"); 1359 } else if (path == QLatin1StringView("accept_conditionally")) { 1360 return i18n("Accept invitation conditionally"); 1361 } else if (path == QLatin1StringView("accept_counter")) { 1362 return i18n("Accept counter proposal"); 1363 } else if (path == QLatin1StringView("counter")) { 1364 return i18n("Create a counter proposal..."); 1365 } else if (path == QLatin1StringView("ignore")) { 1366 return i18n("Throw mail away"); 1367 } else if (path == QLatin1StringView("decline")) { 1368 return i18n("Decline invitation"); 1369 } else if (path == QLatin1StringView("postpone")) { 1370 return i18n("Postpone"); 1371 } else if (path == QLatin1StringView("decline_counter")) { 1372 return i18n("Decline counter proposal"); 1373 } else if (path == QLatin1StringView("check_calendar")) { 1374 return i18n("Check my calendar..."); 1375 } else if (path == QLatin1StringView("reply")) { 1376 return i18n("Record response into my calendar"); 1377 } else if (path == QLatin1StringView("record")) { 1378 return i18n("Record invitation into my calendar"); 1379 } else if (path == QLatin1StringView("delete")) { 1380 return i18n("Move this invitation to my trash folder"); 1381 } else if (path == QLatin1StringView("delegate")) { 1382 return i18n("Delegate invitation"); 1383 } else if (path == QLatin1StringView("forward")) { 1384 return i18n("Forward invitation"); 1385 } else if (path == QLatin1StringView("cancel")) { 1386 return i18n("Remove invitation from my calendar"); 1387 } else if (path.startsWith(QLatin1StringView("ATTACH:"))) { 1388 const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); 1389 return i18n("Open attachment \"%1\"", name); 1390 } 1391 } 1392 1393 return {}; 1394 } 1395 1396 [[nodiscard]] bool askForComment(Attendee::PartStat status) const 1397 { 1398 if (status != Attendee::NeedsAction 1399 && ((status != Attendee::Accepted 1400 && MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation() 1401 == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AskForAllButAcceptance) 1402 || (MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation() 1403 == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AlwaysAsk))) { 1404 return true; 1405 } 1406 return false; 1407 } 1408 }; 1409 1410 class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin 1411 { 1412 Q_OBJECT 1413 Q_INTERFACES(MessageViewer::MessagePartRenderPlugin) 1414 Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "text_calendar.json") 1415 public: 1416 MessageViewer::MessagePartRendererBase *renderer(int idx) override 1417 { 1418 if (idx < 2) { 1419 return new Formatter(); 1420 } else { 1421 return nullptr; 1422 } 1423 } 1424 1425 [[nodiscard]] const MessageViewer::Interface::BodyPartURLHandler *urlHandler(int idx) const override 1426 { 1427 if (idx == 0) { 1428 return new UrlHandler(); 1429 } else { 1430 return nullptr; 1431 } 1432 } 1433 }; 1434 } 1435 1436 #include "text_calendar.moc"