File indexing completed on 2024-12-22 04:56:52

0001 /*
0002     SPDX-FileCopyrightText: 2013-2024 Laurent Montel <montel@kde.org>
0003 
0004     SPDX-FileCopyrightText: 2010 Volker Krause <vkrause@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "newmailnotifieragent.h"
0010 
0011 #include "newmailnotificationhistorydialog.h"
0012 #include "newmailnotifieradaptor.h"
0013 #include "newmailnotifieragentsettings.h"
0014 #include "specialnotifierjob.h"
0015 
0016 #include <KIdentityManagementCore/IdentityManager>
0017 
0018 #include <QDBusConnection>
0019 
0020 #include "newmailnotifier_debug.h"
0021 #include <Akonadi/AgentManager>
0022 #include <Akonadi/AttributeFactory>
0023 #include <Akonadi/ChangeRecorder>
0024 #include <Akonadi/CollectionFetchScope>
0025 #include <Akonadi/EntityDisplayAttribute>
0026 #include <Akonadi/EntityHiddenAttribute>
0027 #include <Akonadi/ItemFetchScope>
0028 #include <Akonadi/MessageStatus>
0029 #include <Akonadi/NewMailNotifierAttribute>
0030 #include <Akonadi/ServerManager>
0031 #include <Akonadi/Session>
0032 #include <Akonadi/SpecialMailCollections>
0033 #include <KLocalizedString>
0034 #include <KMime/Message>
0035 #include <KNotification>
0036 #if HAVE_TEXT_TO_SPEECH_SUPPORT
0037 #include <QTextToSpeech>
0038 #endif
0039 #include <KWindowSystem>
0040 using namespace std::chrono_literals;
0041 #include <chrono>
0042 
0043 using namespace Akonadi;
0044 
0045 NewMailNotifierAgent::NewMailNotifierAgent(const QString &id)
0046     : AgentBase(id)
0047 {
0048     connect(this, &Akonadi::AgentBase::reloadConfiguration, this, &NewMailNotifierAgent::slotReloadConfiguration);
0049     KLocalizedString::setApplicationDomain(QByteArrayLiteral("akonadi_newmailnotifier_agent"));
0050     Akonadi::AttributeFactory::registerAttribute<Akonadi::NewMailNotifierAttribute>();
0051     new NewMailNotifierAdaptor(this);
0052 
0053     NewMailNotifierAgentSettings::instance(KSharedConfig::openConfig());
0054     mIdentityManager = KIdentityManagementCore::IdentityManager::self();
0055     connect(mIdentityManager, qOverload<>(&KIdentityManagementCore::IdentityManager::changed), this, &NewMailNotifierAgent::slotIdentitiesChanged);
0056     slotIdentitiesChanged();
0057     mDefaultIconName = QStringLiteral("kmail");
0058 
0059     QDBusConnection::sessionBus().registerObject(QStringLiteral("/NewMailNotifierAgent"), this, QDBusConnection::ExportAdaptors);
0060 
0061     QString service = QStringLiteral("org.freedesktop.Akonadi.NewMailNotifierAgent");
0062     if (Akonadi::ServerManager::hasInstanceIdentifier()) {
0063         service += QLatin1Char('.') + Akonadi::ServerManager::instanceIdentifier();
0064     }
0065     QDBusConnection::sessionBus().registerService(service);
0066 
0067     connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceStatusChanged, this, &NewMailNotifierAgent::slotInstanceStatusChanged);
0068     connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceRemoved, this, &NewMailNotifierAgent::slotInstanceRemoved);
0069     connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceAdded, this, &NewMailNotifierAgent::slotInstanceAdded);
0070     connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceNameChanged, this, &NewMailNotifierAgent::slotInstanceNameChanged);
0071 
0072     changeRecorder()->setMimeTypeMonitored(KMime::Message::mimeType());
0073     changeRecorder()->itemFetchScope().setCacheOnly(true);
0074     changeRecorder()->itemFetchScope().setFetchModificationTime(false);
0075     changeRecorder()->fetchCollection(true);
0076     changeRecorder()->setChangeRecordingEnabled(false);
0077     changeRecorder()->ignoreSession(Akonadi::Session::defaultSession());
0078     changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
0079     changeRecorder()->setCollectionMonitored(Collection::root(), true);
0080     mTimer.setInterval(5s); // 5secondes
0081     connect(&mTimer, &QTimer::timeout, this, &NewMailNotifierAgent::slotShowNotifications);
0082 
0083     if (isActive()) {
0084         mTimer.setSingleShot(true);
0085     }
0086 }
0087 
0088 NewMailNotifierAgent::~NewMailNotifierAgent()
0089 {
0090     delete mHistoryNotificationDialog;
0091 }
0092 
0093 void NewMailNotifierAgent::slotReloadConfiguration()
0094 {
0095     NewMailNotifierAgentSettings::self()->load();
0096 }
0097 
0098 void NewMailNotifierAgent::slotIdentitiesChanged()
0099 {
0100     mListEmails = mIdentityManager->allEmails();
0101 }
0102 
0103 void NewMailNotifierAgent::doSetOnline(bool online)
0104 {
0105     if (!online) {
0106         clearAll();
0107     }
0108 }
0109 
0110 void NewMailNotifierAgent::setEnableAgent(bool enabled)
0111 {
0112     NewMailNotifierAgentSettings::setEnabled(enabled);
0113     NewMailNotifierAgentSettings::self()->save();
0114     if (!enabled) {
0115         clearAll();
0116     }
0117 }
0118 
0119 bool NewMailNotifierAgent::enabledAgent() const
0120 {
0121     return NewMailNotifierAgentSettings::enabled();
0122 }
0123 
0124 void NewMailNotifierAgent::clearAll()
0125 {
0126     mNewMails.clear();
0127     mInstanceNameInProgress.clear();
0128 }
0129 
0130 bool NewMailNotifierAgent::excludeSpecialCollection(const Akonadi::Collection &collection) const
0131 {
0132     if (collection.hasAttribute<Akonadi::EntityHiddenAttribute>()) {
0133         return true;
0134     }
0135 
0136     if (collection.hasAttribute<Akonadi::NewMailNotifierAttribute>()) {
0137         if (collection.attribute<Akonadi::NewMailNotifierAttribute>()->ignoreNewMail()) {
0138             return true;
0139         }
0140     }
0141 
0142     if (!collection.contentMimeTypes().contains(KMime::Message::mimeType())) {
0143         return true;
0144     }
0145 
0146     SpecialMailCollections::Type type = SpecialMailCollections::self()->specialCollectionType(collection);
0147     switch (type) {
0148     case SpecialMailCollections::Invalid: // Not a special collection
0149     case SpecialMailCollections::Inbox:
0150         return false;
0151     default:
0152         return true;
0153     }
0154 }
0155 
0156 void NewMailNotifierAgent::itemsRemoved(const Item::List &items)
0157 {
0158     if (!isActive()) {
0159         return;
0160     }
0161 
0162     const QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::iterator end(mNewMails.end());
0163     for (QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::iterator it = mNewMails.begin(); it != end; ++it) {
0164         QList<Akonadi::Item::Id> idList = it.value();
0165         bool itemFound = false;
0166         for (const Item &item : items) {
0167             const int numberOfItemsRemoved = idList.removeAll(item.id());
0168             if (numberOfItemsRemoved > 0) {
0169                 itemFound = true;
0170             }
0171         }
0172         if (itemFound) {
0173             if (mNewMails[it.key()].isEmpty()) {
0174                 mNewMails.remove(it.key());
0175             } else {
0176                 mNewMails[it.key()] = idList;
0177             }
0178         }
0179     }
0180 }
0181 
0182 void NewMailNotifierAgent::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet<QByteArray> &addedFlags, const QSet<QByteArray> &removedFlags)
0183 {
0184     Q_UNUSED(removedFlags)
0185 
0186     if (!isActive()) {
0187         return;
0188     }
0189     for (const Akonadi::Item &item : items) {
0190         const QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::iterator end(mNewMails.end());
0191         for (QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::iterator it = mNewMails.begin(); it != end; ++it) {
0192             QList<Akonadi::Item::Id> idList = it.value();
0193             if (idList.contains(item.id()) && addedFlags.contains("\\SEEN")) {
0194                 idList.removeAll(item.id());
0195                 if (idList.isEmpty()) {
0196                     mNewMails.remove(it.key());
0197                     break;
0198                 } else {
0199                     (*it) = idList;
0200                 }
0201             }
0202         }
0203     }
0204 }
0205 
0206 void NewMailNotifierAgent::itemsMoved(const Akonadi::Item::List &items,
0207                                       const Akonadi::Collection &collectionSource,
0208                                       const Akonadi::Collection &collectionDestination)
0209 {
0210     if (!isActive()) {
0211         return;
0212     }
0213 
0214     for (const Akonadi::Item &item : items) {
0215         if (ignoreStatusMail(item)) {
0216             continue;
0217         }
0218 
0219         if (excludeSpecialCollection(collectionSource)) {
0220             continue; // outbox, sent-mail, trash, drafts or templates.
0221         }
0222 
0223         if (mNewMails.contains(collectionSource)) {
0224             QList<Akonadi::Item::Id> idListFrom = mNewMails[collectionSource];
0225             const int removeItems = idListFrom.removeAll(item.id());
0226             if (removeItems > 0) {
0227                 if (idListFrom.isEmpty()) {
0228                     mNewMails.remove(collectionSource);
0229                 } else {
0230                     mNewMails[collectionSource] = idListFrom;
0231                 }
0232                 if (!excludeSpecialCollection(collectionDestination)) {
0233                     QList<Akonadi::Item::Id> idListTo = mNewMails[collectionDestination];
0234                     idListTo.append(item.id());
0235                     mNewMails[collectionDestination] = idListTo;
0236                 }
0237             }
0238         }
0239     }
0240 }
0241 
0242 bool NewMailNotifierAgent::ignoreStatusMail(const Akonadi::Item &item)
0243 {
0244     Akonadi::MessageStatus status;
0245     status.setStatusFromFlags(item.flags());
0246     if (status.isRead() || status.isSpam() || status.isIgnored()) {
0247         return true;
0248     }
0249     return false;
0250 }
0251 
0252 void NewMailNotifierAgent::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
0253 {
0254     if (!isActive()) {
0255         return;
0256     }
0257 
0258     if (excludeSpecialCollection(collection)) {
0259         return; // outbox, sent-mail, trash, drafts or templates.
0260     }
0261 
0262     if (ignoreStatusMail(item)) {
0263         return;
0264     }
0265 
0266     if (!mTimer.isActive()) {
0267         mTimer.start();
0268     }
0269     mNewMails[collection].append(item.id());
0270 }
0271 
0272 void NewMailNotifierAgent::showNotNotificationHistoryDialog(qlonglong windowId)
0273 {
0274     if (!mHistoryNotificationDialog) {
0275         mHistoryNotificationDialog = new NewMailNotificationHistoryDialog(nullptr);
0276         mHistoryNotificationDialog->setAttribute(Qt::WA_NativeWindow, true);
0277     }
0278     KWindowSystem::setMainWindow(mHistoryNotificationDialog->windowHandle(), windowId);
0279     mHistoryNotificationDialog->show();
0280     mHistoryNotificationDialog->raise();
0281     mHistoryNotificationDialog->activateWindow();
0282     mHistoryNotificationDialog->setModal(false);
0283 }
0284 
0285 void NewMailNotifierAgent::setVerboseMailNotification(bool b)
0286 {
0287     NewMailNotifierAgentSettings::setVerboseNotification(b);
0288     NewMailNotifierAgentSettings::self()->save();
0289 }
0290 
0291 bool NewMailNotifierAgent::verboseMailNotification() const
0292 {
0293     return NewMailNotifierAgentSettings::verboseNotification();
0294 }
0295 
0296 void NewMailNotifierAgent::slotShowNotifications()
0297 {
0298     if (mNewMails.isEmpty()) {
0299         return;
0300     }
0301 
0302     if (!isActive()) {
0303         return;
0304     }
0305 
0306     if (!mInstanceNameInProgress.isEmpty()) {
0307         // Restart timer until all is done.
0308         mTimer.start();
0309         return;
0310     }
0311 
0312     QList<NewMailNotificationHistoryManager::HistoryFolderInfo> infos;
0313     QString message;
0314     if (NewMailNotifierAgentSettings::verboseNotification()) {
0315         bool hasUniqMessage = true;
0316         Akonadi::Item::Id itemId = -1;
0317         QString currentPath;
0318         QStringList texts;
0319         const int numberOfCollection(mNewMails.count());
0320         if (numberOfCollection > 1) {
0321             hasUniqMessage = false;
0322         }
0323 
0324         QString resourceName;
0325         const QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::const_iterator end(mNewMails.constEnd());
0326         for (QHash<Akonadi::Collection, QList<Akonadi::Item::Id>>::const_iterator it = mNewMails.constBegin(); it != end; ++it) {
0327             const auto attr = it.key().attribute<Akonadi::EntityDisplayAttribute>();
0328             QString displayName;
0329             if (attr && !attr->displayName().isEmpty()) {
0330                 displayName = attr->displayName();
0331             } else {
0332                 displayName = it.key().name();
0333             }
0334 
0335             const QString resource = it.key().resource();
0336             resourceName = mCacheResourceName.value(resource);
0337             if (resourceName.isEmpty()) {
0338                 const Akonadi::AgentInstance::List lst = Akonadi::AgentManager::self()->instances();
0339                 for (const Akonadi::AgentInstance &instance : lst) {
0340                     if (instance.identifier() == resource) {
0341                         mCacheResourceName.insert(instance.identifier(), instance.name());
0342                         resourceName = instance.name();
0343                         break;
0344                     }
0345                 }
0346             }
0347 
0348             if (hasUniqMessage) {
0349                 const int numberOfValue(it.value().count());
0350                 if (numberOfValue == 0) {
0351                     // You can have an unique folder with 0 message
0352                     return;
0353                 } else if (numberOfValue == 1) {
0354                     itemId = it.value().at(0);
0355                     currentPath = displayName;
0356                     break;
0357                 } else {
0358                     hasUniqMessage = false;
0359                 }
0360             }
0361 
0362             const int numberOfEmails(it.value().count());
0363             if (numberOfEmails > 0) {
0364                 const QString text = i18ncp("%2 = name of mail folder; %3 = name of Akonadi POP3/IMAP/etc resource (as user named it)",
0365                                             "One new email in %2 from \"%3\"",
0366                                             "%1 new emails in %2 from \"%3\"",
0367                                             numberOfEmails,
0368                                             displayName,
0369                                             resourceName);
0370                 texts.append(text);
0371                 if (!hasUniqMessage) {
0372                     NewMailNotificationHistoryManager::HistoryFolderInfo info;
0373                     info.message = text;
0374                     info.identifier = it.key().id();
0375                     infos.append(info);
0376                 }
0377             }
0378         }
0379         if (hasUniqMessage) {
0380             SpecialNotifierJob::SpecialNotificationInfo info;
0381             info.itemId = itemId;
0382             info.path = currentPath;
0383             info.listEmails = mListEmails;
0384             info.defaultIconName = mDefaultIconName;
0385             info.resourceName = resourceName;
0386             auto job = new SpecialNotifierJob(std::move(info), this);
0387             connect(job, &SpecialNotifierJob::displayNotification, this, [this, itemId](const QPixmap &pixmap, const QString &message) {
0388                 NewMailNotificationHistoryManager::HistoryMailInfo info;
0389                 info.message = message;
0390                 info.identifier = itemId;
0391                 addEmailInfoNotificationHistory(pixmap, message, std::move(info));
0392             });
0393 #if HAVE_TEXT_TO_SPEECH_SUPPORT
0394             connect(job, &SpecialNotifierJob::say, this, &NewMailNotifierAgent::slotSay);
0395 #endif
0396             mNewMails.clear();
0397             return;
0398         } else {
0399             message = texts.join(QLatin1StringView("<br>"));
0400         }
0401     } else {
0402         message = i18n("New mail arrived");
0403     }
0404 
0405     qCDebug(NEWMAILNOTIFIER_LOG) << message;
0406 
0407     addFoldersInfoNotificationHistory(message, std::move(infos));
0408 
0409     mNewMails.clear();
0410 }
0411 
0412 void NewMailNotifierAgent::addEmailInfoNotificationHistory(const QPixmap &pixmap,
0413                                                            const QString &message,
0414                                                            const NewMailNotificationHistoryManager::HistoryMailInfo &info)
0415 {
0416     slotDisplayNotification(pixmap, message);
0417     if (NewMailNotifierAgentSettings::enableNotificationHistory()) {
0418         NewMailNotificationHistoryManager::self()->addEmailInfoNotificationHistory(info);
0419     }
0420 }
0421 
0422 void NewMailNotifierAgent::addFoldersInfoNotificationHistory(const QString &message, const QList<NewMailNotificationHistoryManager::HistoryFolderInfo> &infos)
0423 {
0424     slotDisplayNotification(QPixmap(), message);
0425     if (NewMailNotifierAgentSettings::enableNotificationHistory()) {
0426         NewMailNotificationHistoryManager::self()->addFoldersInfoNotificationHistory(infos);
0427     }
0428 }
0429 
0430 void NewMailNotifierAgent::slotDisplayNotification(const QPixmap &pixmap, const QString &message)
0431 {
0432     if (pixmap.isNull()) {
0433         KNotification::event(QStringLiteral("new-email"),
0434                              QString(),
0435                              message,
0436                              mDefaultIconName,
0437                              NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping
0438                                                                                         : KNotification::CloseOnTimeout,
0439                              QStringLiteral("akonadi_newmailnotifier_agent"));
0440     } else {
0441         KNotification::event(QStringLiteral("new-email"),
0442                              message,
0443                              pixmap,
0444                              NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping
0445                                                                                         : KNotification::CloseOnTimeout,
0446                              QStringLiteral("akonadi_newmailnotifier_agent"));
0447     }
0448 }
0449 
0450 void NewMailNotifierAgent::slotInstanceNameChanged(const Akonadi::AgentInstance &instance)
0451 {
0452     if (!isActive()) {
0453         return;
0454     }
0455 
0456     const QString identifier(instance.identifier());
0457     int resourceNameRemoved = mCacheResourceName.remove(identifier);
0458     if (resourceNameRemoved > 0) {
0459         mCacheResourceName.insert(identifier, instance.name());
0460     }
0461 }
0462 
0463 void NewMailNotifierAgent::slotInstanceStatusChanged(const Akonadi::AgentInstance &instance)
0464 {
0465     if (!isActive()) {
0466         return;
0467     }
0468 
0469     const QString identifier(instance.identifier());
0470     switch (instance.status()) {
0471     case Akonadi::AgentInstance::Broken:
0472     case Akonadi::AgentInstance::Idle:
0473         mInstanceNameInProgress.removeAll(identifier);
0474         break;
0475     case Akonadi::AgentInstance::Running:
0476         if (!excludeAgentType(instance)) {
0477             if (!mInstanceNameInProgress.contains(identifier)) {
0478                 mInstanceNameInProgress.append(identifier);
0479             }
0480         }
0481         break;
0482     case Akonadi::AgentInstance::NotConfigured:
0483         // Nothing
0484         break;
0485     }
0486 }
0487 
0488 bool NewMailNotifierAgent::excludeAgentType(const Akonadi::AgentInstance &instance)
0489 {
0490     if (instance.type().mimeTypes().contains(KMime::Message::mimeType())) {
0491         const QStringList capabilities(instance.type().capabilities());
0492         if (capabilities.contains(QLatin1StringView("Resource")) && !capabilities.contains(QLatin1StringView("Virtual"))
0493             && !capabilities.contains(QLatin1StringView("MailTransport"))) {
0494             return false;
0495         } else {
0496             return true;
0497         }
0498     }
0499     return true;
0500 }
0501 
0502 void NewMailNotifierAgent::slotInstanceRemoved(const Akonadi::AgentInstance &instance)
0503 {
0504     if (!isActive()) {
0505         return;
0506     }
0507 
0508     const QString identifier(instance.identifier());
0509     mInstanceNameInProgress.removeAll(identifier);
0510 }
0511 
0512 void NewMailNotifierAgent::slotInstanceAdded(const Akonadi::AgentInstance &instance)
0513 {
0514     mCacheResourceName.insert(instance.identifier(), instance.name());
0515 }
0516 
0517 void NewMailNotifierAgent::printDebug()
0518 {
0519     qCDebug(NEWMAILNOTIFIER_LOG) << "instance in progress: " << mInstanceNameInProgress << "\n notifier enabled : " << NewMailNotifierAgentSettings::enabled()
0520                                  << "\n check in progress : " << !mInstanceNameInProgress.isEmpty();
0521 }
0522 
0523 bool NewMailNotifierAgent::isActive() const
0524 {
0525     return isOnline() && NewMailNotifierAgentSettings::enabled();
0526 }
0527 
0528 void NewMailNotifierAgent::slotSay(const QString &message)
0529 {
0530 #if HAVE_TEXT_TO_SPEECH_SUPPORT
0531     if (!mTextToSpeech) {
0532         mTextToSpeech = new QTextToSpeech(this);
0533     }
0534     if (mTextToSpeech->availableEngines().isEmpty()) {
0535         qCWarning(NEWMAILNOTIFIER_LOG) << "No texttospeech engine available";
0536     } else {
0537         mTextToSpeech->say(message);
0538     }
0539 #endif
0540 }
0541 
0542 AKONADI_AGENT_MAIN(NewMailNotifierAgent)
0543 
0544 #include "moc_newmailnotifieragent.cpp"