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

0001 /*
0002     SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "sendjob.h"
0008 
0009 #include "storeresultjob.h"
0010 
0011 #include "maildispatcher_debug.h"
0012 #include <Akonadi/AddressAttribute>
0013 #include <Akonadi/AgentInstance>
0014 #include <Akonadi/AgentManager>
0015 #include <Akonadi/Collection>
0016 #include <Akonadi/CollectionFetchJob>
0017 #include <Akonadi/ItemDeleteJob>
0018 #include <Akonadi/ItemModifyJob>
0019 #include <Akonadi/ItemMoveJob>
0020 #include <Akonadi/MessageParts>
0021 #include <Akonadi/SentBehaviourAttribute>
0022 #include <Akonadi/ServerManager>
0023 #include <Akonadi/SpecialMailCollections>
0024 #include <Akonadi/TransportAttribute>
0025 #include <Akonadi/TransportResourceBase>
0026 #include <KLocalizedString>
0027 #include <MailTransport/Transport>
0028 #include <MailTransport/TransportJob>
0029 #include <MailTransport/TransportManager>
0030 
0031 #include <KMime/Message>
0032 
0033 #include <QDBusConnection>
0034 #include <QDBusInterface>
0035 #include <QDBusReply>
0036 #include <QTimer>
0037 
0038 using namespace Akonadi;
0039 using namespace KMime;
0040 using namespace MailTransport;
0041 
0042 void SendJob::doTransport()
0043 {
0044     qCDebug(MAILDISPATCHER_LOG) << "Transporting message.";
0045 
0046     if (mAborting) {
0047         qCDebug(MAILDISPATCHER_LOG) << "Marking message as aborted.";
0048         setError(UserDefinedError);
0049         setErrorText(i18n("Message sending aborted."));
0050         storeResult(false, i18n("Message sending aborted."));
0051         return;
0052     }
0053 
0054     // Is it an Akonadi transport or a traditional one?
0055     const TransportAttribute *transportAttribute = mItem.attribute<TransportAttribute>();
0056     Q_ASSERT(transportAttribute);
0057     const auto transport = TransportManager::self()->transportById(transportAttribute->transportId(), false);
0058     if (!transport) {
0059         storeResult(false, i18n("Could not initiate message transport. Possibly invalid transport."));
0060         return;
0061     }
0062 
0063     const TransportType type = transport->transportType();
0064     if (!type.isValid()) {
0065         storeResult(false, i18n("Could not send message. Invalid transport."));
0066         return;
0067     }
0068 
0069     if (!filterItem(8)) { // BeforeOutbound
0070         return;
0071     }
0072 
0073     if (type.isAkonadiResource()) {
0074         // Send the item directly to the resource that will send it.
0075         mResourceId = transport->host();
0076         doAkonadiTransport();
0077     } else {
0078         // Use a traditional transport job.
0079         doTraditionalTransport();
0080     }
0081 }
0082 
0083 void SendJob::doAkonadiTransport()
0084 {
0085     Q_ASSERT(!mResourceId.isEmpty());
0086     Q_ASSERT(mInterface == nullptr);
0087 
0088     const auto service = Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, mResourceId);
0089 
0090     mInterface = new QDBusInterface(service,
0091                                     QStringLiteral("/Transport"),
0092                                     QStringLiteral("org.freedesktop.Akonadi.Resource.Transport"),
0093                                     QDBusConnection::sessionBus(),
0094                                     this);
0095 
0096     if (!mInterface->isValid()) {
0097         storeResult(false, i18n("Failed to get D-Bus interface of resource %1.", mResourceId));
0098         delete mInterface;
0099         mInterface = nullptr;
0100         return;
0101     }
0102 
0103     // Signals.
0104     QObject::connect(AgentManager::self(), &AgentManager::instanceProgressChanged, this, &SendJob::resourceProgress);
0105     QObject::connect(mInterface, SIGNAL(transportResult(qlonglong, int, QString)), this, SLOT(resourceResult(qlonglong, int, QString)));
0106 
0107     // Start sending.
0108     const QDBusReply<void> reply = mInterface->call(QStringLiteral("send"), mItem.id());
0109     if (!reply.isValid()) {
0110         storeResult(false, i18n("Invalid D-Bus reply from resource %1.", mResourceId));
0111         return;
0112     }
0113 }
0114 
0115 void SendJob::doTraditionalTransport()
0116 {
0117     const TransportAttribute *transportAttribute = mItem.attribute<TransportAttribute>();
0118     Q_ASSERT(transportAttribute);
0119     TransportJob *job = TransportManager::self()->createTransportJob(transportAttribute->transportId());
0120 
0121     Q_ASSERT(job);
0122     Q_ASSERT(mCurrentJob == nullptr);
0123 
0124     mCurrentJob = job;
0125 
0126     // Message.
0127     Q_ASSERT(mItem.hasPayload<Message::Ptr>());
0128     const auto message = mItem.payload<Message::Ptr>();
0129     bool needAssemble = false;
0130     if (message->removeHeader("Bcc")) {
0131         needAssemble = true;
0132     }
0133     if (message->removeHeader("X-KMail-Identity")) {
0134         needAssemble = true;
0135     }
0136     if (message->removeHeader("X-KMail-Dictionary")) {
0137         needAssemble = true;
0138     }
0139     if (message->removeHeader("X-KMail-Transport")) {
0140         needAssemble = true;
0141     }
0142     if (message->removeHeader("X-KMail-Fcc")) {
0143         needAssemble = true;
0144     }
0145     if (message->removeHeader("X-KMail-Identity-Name")) {
0146         needAssemble = true;
0147     }
0148     if (message->removeHeader("X-KMail-Transport-Name")) {
0149         needAssemble = true;
0150     }
0151 
0152     if (needAssemble) {
0153         message->assemble();
0154     }
0155     const QByteArray content = message->encodedContent(true) + "\r\n";
0156     Q_ASSERT(!content.isEmpty());
0157 
0158     // Addresses.
0159     const AddressAttribute *addressAttribute = mItem.attribute<AddressAttribute>();
0160     Q_ASSERT(addressAttribute);
0161 
0162     job->setData(content);
0163     job->setSender(addressAttribute->from());
0164     job->setTo(addressAttribute->to());
0165     job->setCc(addressAttribute->cc());
0166     job->setBcc(addressAttribute->bcc());
0167     job->setDeliveryStatusNotification(addressAttribute->deliveryStatusNotification());
0168 
0169     // Signals.
0170     connect(job, &TransportJob::result, this, &SendJob::transportResult);
0171     connect(job, &TransportJob::percentChanged, this, [this](KJob *job, ulong val) {
0172         transportPercent(job, val);
0173     });
0174     job->start();
0175 }
0176 
0177 void SendJob::transportPercent(KJob *job, unsigned long)
0178 {
0179     Q_ASSERT(mCurrentJob == job);
0180     qCDebug(MAILDISPATCHER_LOG) << "Processed amount" << job->processedAmount(KJob::Bytes) << "total amount" << job->totalAmount(KJob::Bytes);
0181 
0182     setTotalAmount(KJob::Bytes, job->totalAmount(KJob::Bytes)); // Is not set at the time of start().
0183     setProcessedAmount(KJob::Bytes, job->processedAmount(KJob::Bytes));
0184 }
0185 
0186 void SendJob::transportResult(KJob *job)
0187 {
0188     Q_ASSERT(mCurrentJob == job);
0189     mCurrentJob = nullptr;
0190     doPostJob(!job->error(), job->errorString());
0191 }
0192 
0193 void SendJob::resourceProgress(const AgentInstance &instance)
0194 {
0195     if (!mInterface) {
0196         // We might have gotten a very late signal.
0197         qCWarning(MAILDISPATCHER_LOG) << "called but no resource job running!";
0198         return;
0199     }
0200 
0201     if (instance.identifier() == mResourceId) {
0202         // This relies on the resource's progress representing the progress of
0203         // sending this item.
0204         setPercent(instance.progress());
0205     }
0206 }
0207 
0208 void SendJob::resourceResult(qlonglong itemId, int result, const QString &message)
0209 {
0210     Q_UNUSED(itemId)
0211     Q_ASSERT(mInterface);
0212     delete mInterface; // So that abort() knows the transport job is over.
0213     mInterface = nullptr;
0214 
0215     const auto transportResult = static_cast<TransportResourceBase::TransportResult>(result);
0216 
0217     const bool success = (transportResult == TransportResourceBase::TransportSucceeded);
0218 
0219     Q_ASSERT(itemId == mItem.id());
0220     doPostJob(success, message);
0221 }
0222 
0223 void SendJob::abortPostJob()
0224 {
0225     // We were unlucky and LocalFolders is recreating its stuff right now.
0226     // We will not wait for it.
0227     qCWarning(MAILDISPATCHER_LOG) << "Default sent mail collection unavailable, not moving the mail after sending.";
0228     setError(UserDefinedError);
0229     setErrorText(i18n("Default sent-mail folder unavailable. Keeping message in outbox."));
0230     storeResult(false, errorString());
0231 }
0232 
0233 void SendJob::doPostJob(bool transportSuccess, const QString &transportMessage)
0234 {
0235     qCDebug(MAILDISPATCHER_LOG) << "success" << transportSuccess << "message" << transportMessage;
0236 
0237     if (!transportSuccess) {
0238         qCDebug(MAILDISPATCHER_LOG) << "Error transporting.";
0239         setError(UserDefinedError);
0240 
0241         const QString error = mAborting ? i18n("Message transport aborted.") : i18n("Failed to transport message.");
0242 
0243         setErrorText(error + QLatin1Char(' ') + transportMessage);
0244         storeResult(false, errorString());
0245     } else {
0246         qCDebug(MAILDISPATCHER_LOG) << "Success transporting.";
0247 
0248         // Delete or move to sent-mail.
0249         const SentBehaviourAttribute *attribute = mItem.attribute<SentBehaviourAttribute>();
0250         Q_ASSERT(attribute);
0251 
0252         if (attribute->sentBehaviour() == SentBehaviourAttribute::Delete) {
0253             qCDebug(MAILDISPATCHER_LOG) << "Deleting item from outbox.";
0254             mCurrentJob = new ItemDeleteJob(mItem);
0255             QObject::connect(mCurrentJob, &ItemDeleteJob::result, this, &SendJob::postJobResult);
0256         } else {
0257             if (attribute->sentBehaviour() == SentBehaviourAttribute::MoveToDefaultSentCollection) {
0258                 if (SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::SentMail)) {
0259                     mCurrentJob = new ItemMoveJob(mItem, SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::SentMail), this);
0260                     QObject::connect(mCurrentJob, &ItemMoveJob::result, this, &SendJob::postJobResult);
0261                 } else {
0262                     abortPostJob();
0263                 }
0264             } else {
0265                 qCDebug(MAILDISPATCHER_LOG) << "sentBehaviour=" << attribute->sentBehaviour() << "using collection from attribute";
0266                 mCurrentJob = new CollectionFetchJob(attribute->moveToCollection(), Akonadi::CollectionFetchJob::Base);
0267                 QObject::connect(mCurrentJob, &CollectionFetchJob::result, this, &SendJob::slotSentMailCollectionFetched);
0268             }
0269         }
0270     }
0271 }
0272 
0273 bool SendJob::filterItem(int filterset)
0274 {
0275     const auto service = Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Agent, QStringLiteral("akonadi_mailfilter_agent"));
0276 
0277     QDBusInterface iface(service,
0278                          QStringLiteral("/MailFilterAgent"),
0279                          QStringLiteral("org.freedesktop.Akonadi.MailFilterAgent"),
0280                          QDBusConnection::sessionBus(),
0281                          this);
0282     if (!iface.isValid()) {
0283         storeResult(false, i18n("Failed to get D-Bus interface of mailfilteragent."));
0284         return false;
0285     }
0286 
0287     // Outbound = 0x2
0288     const QDBusReply<void> reply = iface.call(QStringLiteral("filterItem"), mItem.id(), filterset, QString());
0289     if (!reply.isValid()) {
0290         storeResult(false, i18n("Invalid D-Bus reply from mailfilteragent"));
0291         return false;
0292     }
0293 
0294     return true;
0295 }
0296 
0297 void SendJob::slotSentMailCollectionFetched(KJob *job)
0298 {
0299     Akonadi::Collection fetchCol;
0300     bool ok = false;
0301     if (!job->error()) {
0302         const CollectionFetchJob *const fetchJob = qobject_cast<CollectionFetchJob *>(job);
0303         if (!fetchJob->collections().isEmpty()) {
0304             fetchCol = fetchJob->collections().at(0);
0305             ok = true;
0306         }
0307     }
0308     if (!ok) {
0309         if (!SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::SentMail)) {
0310             abortPostJob();
0311             return;
0312         }
0313         fetchCol = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::SentMail);
0314     }
0315     mCurrentJob = new ItemMoveJob(mItem, fetchCol, this);
0316     QObject::connect(mCurrentJob, &ItemMoveJob::result, this, &SendJob::postJobResult);
0317 }
0318 
0319 void SendJob::postJobResult(KJob *job)
0320 {
0321     Q_ASSERT(mCurrentJob == job);
0322     mCurrentJob = nullptr;
0323     const SentBehaviourAttribute *attribute = mItem.attribute<SentBehaviourAttribute>();
0324     Q_ASSERT(attribute);
0325 
0326     if (job->error()) {
0327         qCDebug(MAILDISPATCHER_LOG) << "Error deleting or moving to sent-mail.";
0328 
0329         QString errorStr;
0330         switch (attribute->sentBehaviour()) {
0331         case SentBehaviourAttribute::Delete:
0332             errorStr = i18n("Sending succeeded, but failed to remove the message from the outbox.");
0333             break;
0334         default:
0335             errorStr = i18n("Sending succeeded, but failed to move the message to the sent-mail folder.");
0336             break;
0337         }
0338         setError(UserDefinedError);
0339         setErrorText(errorStr + QLatin1Char(' ') + job->errorString());
0340         storeResult(false, errorString());
0341     } else {
0342         qCDebug(MAILDISPATCHER_LOG) << "Success deleting or moving to sent-mail.";
0343         if (!filterItem(2)) { // Outbound
0344             return;
0345         }
0346         if (attribute->sentBehaviour() == SentBehaviourAttribute::Delete) {
0347             emitResult();
0348         } else {
0349             storeResult(true);
0350         }
0351     }
0352 }
0353 
0354 void SendJob::storeResult(bool success, const QString &message)
0355 {
0356     qCDebug(MAILDISPATCHER_LOG) << "success" << success << "message" << message;
0357 
0358     Q_ASSERT(mCurrentJob == nullptr);
0359     mCurrentJob = new StoreResultJob(mItem, success, message);
0360     connect(mCurrentJob, &StoreResultJob::result, this, &SendJob::doEmitResult);
0361 }
0362 
0363 void SendJob::doEmitResult(KJob *job)
0364 {
0365     Q_ASSERT(mCurrentJob == job);
0366     mCurrentJob = nullptr;
0367 
0368     if (job->error()) {
0369         qCWarning(MAILDISPATCHER_LOG) << "Error storing result.";
0370         setError(UserDefinedError);
0371         setErrorText(errorString() + QLatin1Char(' ') + i18n("Failed to store result in item.") + QLatin1Char(' ') + job->errorString());
0372     } else {
0373         qCDebug(MAILDISPATCHER_LOG) << "Success storing result.";
0374         // It is still possible that the transport failed.
0375         auto srJob = static_cast<StoreResultJob *>(job);
0376         if (!srJob->success()) {
0377             setError(UserDefinedError);
0378             setErrorText(srJob->message());
0379         }
0380     }
0381 
0382     emitResult();
0383 }
0384 
0385 SendJob::SendJob(const Item &item, QObject *parent)
0386     : KJob(parent)
0387     , mItem(item)
0388 {
0389 }
0390 
0391 SendJob::~SendJob() = default;
0392 
0393 void SendJob::start()
0394 {
0395     QTimer::singleShot(0, this, &SendJob::doTransport);
0396 }
0397 
0398 void SendJob::setMarkAborted()
0399 {
0400     Q_ASSERT(!mAborting);
0401     mAborting = true;
0402 }
0403 
0404 void SendJob::abort()
0405 {
0406     setMarkAborted();
0407 
0408     if (dynamic_cast<TransportJob *>(mCurrentJob)) {
0409         qCDebug(MAILDISPATCHER_LOG) << "Abort called, active transport job.";
0410         // Abort transport.
0411         mCurrentJob->kill(KJob::EmitResult);
0412     } else if (mInterface != nullptr) {
0413         qCDebug(MAILDISPATCHER_LOG) << "Abort called, propagating to resource.";
0414         // Abort resource doing transport.
0415         AgentInstance instance = AgentManager::self()->instance(mResourceId);
0416         instance.abortCurrentTask();
0417     } else {
0418         qCDebug(MAILDISPATCHER_LOG) << "Abort called, but no transport job is active.";
0419         // Either transport has not started, in which case doTransport will
0420         // mark the item as aborted, or the item has already been sent, in which
0421         // case there is nothing we can do.
0422     }
0423 }
0424 
0425 #include "moc_sendjob.cpp"