File indexing completed on 2024-05-26 05:27:50

0001 /*
0002  *   Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
0003  *
0004  *   This program is free software; you can redistribute it and/or modify
0005  *   it under the terms of the GNU General Public License as published by
0006  *   the Free Software Foundation; either version 2 of the License, or
0007  *   (at your option) any later version.
0008  *
0009  *   This program is distributed in the hope that it will be useful,
0010  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *   GNU General Public License for more details.
0013  *
0014  *   You should have received a copy of the GNU General Public License
0015  *   along with this program; if not, write to the
0016  *   Free Software Foundation, Inc.,
0017  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
0018  */
0019 
0020 #include "mailtransportresource.h"
0021 #include "facade.h"
0022 #include "facadefactory.h"
0023 #include "resourceconfig.h"
0024 #include "definitions.h"
0025 #include "inspector.h"
0026 #include "store.h"
0027 #include <QDir>
0028 #include <QFileInfo>
0029 #include <QSettings>
0030 #include <QUrl>
0031 #include <KMime/Message>
0032 
0033 #include "mailtransport.h"
0034 #include "inspection.h"
0035 #include <synchronizer.h>
0036 #include <log.h>
0037 #include <resourceconfig.h>
0038 #include <mailpreprocessor.h>
0039 #include <adaptorfactoryregistry.h>
0040 
0041 #define ENTITY_TYPE_MAIL "mail"
0042 
0043 using namespace Sink;
0044 
0045 struct Settings {
0046     QString server;
0047     QString username;
0048     QString cacert;
0049     bool testMode;
0050 };
0051 
0052 class MailtransportPreprocessor : public Sink::Preprocessor
0053 {
0054 public:
0055     MailtransportPreprocessor() : Sink::Preprocessor() {}
0056 
0057     QByteArray getTargetResource()
0058     {
0059         using namespace Sink::ApplicationDomain;
0060 
0061         auto resource = Store::readOne<ApplicationDomain::SinkResource>(Query{}.filter(resourceInstanceIdentifier()).request<ApplicationDomain::SinkResource::Account>());
0062         if (resource.identifier().isEmpty()) {
0063             SinkWarning() << "Failed to retrieve this resource: " << resourceInstanceIdentifier();
0064         }
0065         Query query;
0066         query.containsFilter<ApplicationDomain::SinkResource::Capabilities>(ApplicationDomain::ResourceCapabilities::Mail::sent);
0067         query.filter<ApplicationDomain::SinkResource::Account>(resource.getAccount());
0068         auto targetResource = Store::readOne<ApplicationDomain::SinkResource>(query);
0069         if (targetResource.identifier().isEmpty()) {
0070             SinkWarning() << "Failed to find target resource: " << targetResource.identifier();
0071         }
0072         return targetResource.identifier();
0073     }
0074 
0075     Result process(Type type, const ApplicationDomain::ApplicationDomainType &current, ApplicationDomain::ApplicationDomainType &diff) Q_DECL_OVERRIDE
0076     {
0077         if (type == Preprocessor::Modification) {
0078             using namespace Sink::ApplicationDomain;
0079             if (diff.changedProperties().contains(Mail::Trash::name)) {
0080                 //Move back to regular resource
0081                 diff.setResource(getTargetResource());
0082                 return {MoveToResource};
0083             } else if (diff.changedProperties().contains(Mail::Draft::name)) {
0084                 //Move back to regular resource
0085                 diff.setResource(getTargetResource());
0086                 return {MoveToResource};
0087             }
0088         }
0089         return {NoAction};
0090     }
0091 };
0092 
0093 class MailtransportSynchronizer : public Sink::Synchronizer {
0094 public:
0095     MailtransportSynchronizer(const Sink::ResourceContext &resourceContext)
0096         : Sink::Synchronizer(resourceContext),
0097         mResourceInstanceIdentifier(resourceContext.instanceId())
0098     {
0099 
0100     }
0101 
0102     KAsync::Job<void> send(const ApplicationDomain::Mail &mail, const Settings &settings)
0103     {
0104         return KAsync::start([=] {
0105             if (!syncStore().readValue(mail.identifier()).isEmpty()) {
0106                 SinkLog() << "Mail is already sent: " << mail.identifier();
0107                 return KAsync::null();
0108             }
0109             emitNotification(Notification::Info, ApplicationDomain::SyncInProgress, "Sending message.", {}, {mail.identifier()});
0110             const auto data = mail.getMimeMessage();
0111             auto msg = KMime::Message::Ptr::create();
0112             msg->setContent(KMime::CRLFtoLF(data));
0113             msg->parse();
0114             if (settings.testMode) {
0115                 auto subject = msg->subject(true)->asUnicodeString();
0116                 SinkLog() << "I would totally send that mail, but I'm in test mode." << mail.identifier() << subject;
0117                 if (!subject.contains("send")) {
0118                     return KAsync::error("Failed to send the message.");
0119                 }
0120                 auto path = resourceStorageLocation(mResourceInstanceIdentifier) + "/test/";
0121                 SinkTrace() << path;
0122                 QDir dir;
0123                 dir.mkpath(path);
0124                 QFile f(path+ mail.identifier());
0125                 f.open(QIODevice::ReadWrite);
0126                 f.write("foo");
0127                 f.close();
0128             } else {
0129                 MailTransport::Options options;
0130                 if (settings.server.contains("smtps")) {
0131                     if (settings.server.contains("465")) {
0132                         options |= MailTransport::UseTls;
0133                     } else {
0134                         options |= MailTransport::UseStarttls;
0135                     }
0136                 }
0137 
0138                 SinkLog() << "Sending message " << settings.server << settings.username << "CaCert: " << settings.cacert << "Using tls: " << bool(options & MailTransport::UseTls);
0139                 SinkTrace() << "Sending message " << msg;
0140                 auto result = MailTransport::sendMessage(msg, settings.server.toUtf8(), settings.username.toUtf8(), secret().toUtf8(), settings.cacert.toUtf8(), options);
0141                 if (!result.error) {
0142                     SinkWarning() << "Failed to send message: " << mail << "\n" << result.errorMessage;
0143                     const auto errorMessage = QString("Failed to send the message: %1").arg(result.errorMessage);
0144                     emitNotification(Notification::Warning, ApplicationDomain::SyncError, errorMessage, {}, {mail.identifier()});
0145                     emitNotification(Notification::Warning, ApplicationDomain::TransmissionError, errorMessage, {}, {mail.identifier()});
0146                     return KAsync::error(errorMessage.toUtf8().constData());
0147                 } else {
0148                     emitNotification(Notification::Info, ApplicationDomain::SyncSuccess, "Message successfully sent.", {}, {mail.identifier()});
0149                     emitNotification(Notification::Info, ApplicationDomain::TransmissionSuccess, "Message successfully sent.", {}, {mail.identifier()});
0150                 }
0151             }
0152             syncStore().writeValue(mail.identifier(), "sent");
0153 
0154             SinkLog() << "Sent mail, and triggering move to sent mail folder: " << mail.identifier();
0155             auto modifiedMail = ApplicationDomain::Mail(mResourceInstanceIdentifier, mail.identifier(), mail.revision(), QSharedPointer<Sink::ApplicationDomain::MemoryBufferAdaptor>::create());
0156             modifiedMail.setSent(true);
0157 
0158             auto resource = Store::readOne<ApplicationDomain::SinkResource>(Query{}.filter(mResourceInstanceIdentifier).request<ApplicationDomain::SinkResource::Account>());
0159             if (resource.identifier().isEmpty()) {
0160                 SinkWarning() << "Failed to retrieve target resource: " << mResourceInstanceIdentifier;
0161             }
0162             //Then copy the mail to the target resource
0163             Query query;
0164             query.containsFilter<ApplicationDomain::SinkResource::Capabilities>(ApplicationDomain::ResourceCapabilities::Mail::sent);
0165             query.filter<ApplicationDomain::SinkResource::Account>(resource.getAccount());
0166             return Store::fetchOne<ApplicationDomain::SinkResource>(query)
0167                 .then([this, modifiedMail](const ApplicationDomain::SinkResource &resource) {
0168                     //Modify the mail to have the sent property set to true, and move it to the new resource.
0169                     modify(modifiedMail, resource.identifier(), true);
0170                 });
0171         });
0172     }
0173 
0174     KAsync::Job<void> synchronizeWithSource(const Sink::QueryBase &query) Q_DECL_OVERRIDE
0175     {
0176         if (!QUrl{mSettings.server}.isValid()) {
0177             return KAsync::error(ApplicationDomain::ConfigurationError, "Invalid server url: " + mSettings.server);
0178         }
0179         return KAsync::start<void>([this]() {
0180             QList<ApplicationDomain::Mail> toSend;
0181             SinkLog() << "Looking for mails to send.";
0182             store().readAll<ApplicationDomain::Mail>([&](const ApplicationDomain::Mail &mail) {
0183                 if (!mail.getSent()) {
0184                     toSend << mail;
0185                 }
0186             });
0187             SinkLog() << "Found " << toSend.size() << " mails to send";
0188             auto job = KAsync::null<void>();
0189             for (const auto &m : toSend) {
0190                 job = job.then(send(m, mSettings));
0191             }
0192             return job;
0193         });
0194     }
0195 
0196     bool canReplay(const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE
0197     {
0198         return true;
0199     }
0200 
0201     KAsync::Job<QByteArray> replay(const ApplicationDomain::Mail &mail, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE
0202     {
0203         if (operation == Sink::Operation_Creation) {
0204             SinkTrace() << "Dispatching message.";
0205             return send(mail, mSettings)
0206                 .then(KAsync::value(QByteArray{}));
0207         } else if (operation == Sink::Operation_Removal) {
0208             syncStore().removeValue(mail.identifier(), "");
0209         } else if (operation == Sink::Operation_Modification) {
0210         }
0211         return KAsync::null<QByteArray>();
0212     }
0213 
0214 public:
0215     QByteArray mResourceInstanceIdentifier;
0216     Settings mSettings;
0217 };
0218 
0219 class MailtransportInspector : public Sink::Inspector {
0220 public:
0221     MailtransportInspector(const Sink::ResourceContext &resourceContext)
0222         : Sink::Inspector(resourceContext)
0223     {
0224 
0225     }
0226 
0227 protected:
0228     KAsync::Job<void> inspect(int inspectionType, const QByteArray &inspectionId, const QByteArray &domainType, const QByteArray &entityId, const QByteArray &property, const QVariant &expectedValue) Q_DECL_OVERRIDE
0229     {
0230         if (domainType == ENTITY_TYPE_MAIL) {
0231             if (inspectionType == Sink::ResourceControl::Inspection::ExistenceInspectionType) {
0232                 auto path = resourceStorageLocation(mResourceContext.instanceId()) + "/test/" + entityId;
0233                 if (QFileInfo::exists(path)) {
0234                     return KAsync::null<void>();
0235                 }
0236                 return KAsync::error<void>(1, "Couldn't find message: " + path);
0237             }
0238         }
0239         return KAsync::null<void>();
0240     }
0241 };
0242 
0243 
0244 MailtransportResource::MailtransportResource(const Sink::ResourceContext &resourceContext)
0245     : Sink::GenericResource(resourceContext)
0246 {
0247     auto config = ResourceConfig::getConfiguration(resourceContext.instanceId());
0248 
0249     auto synchronizer = QSharedPointer<MailtransportSynchronizer>::create(resourceContext);
0250     synchronizer->mSettings = {config.value("server").toString(),
0251                 config.value("username").toString(),
0252                 config.value("cacert").toString(),
0253                 config.value("testmode").toBool()
0254     };
0255     setupSynchronizer(synchronizer);
0256     setupInspector(QSharedPointer<MailtransportInspector>::create(resourceContext));
0257 
0258     setupPreprocessors(ENTITY_TYPE_MAIL, QVector<Sink::Preprocessor*>() << new MailPropertyExtractor << new MailtransportPreprocessor);
0259 }
0260 
0261 MailtransportResourceFactory::MailtransportResourceFactory(QObject *parent)
0262     : Sink::ResourceFactory(parent, {Sink::ApplicationDomain::ResourceCapabilities::Mail::mail,
0263             Sink::ApplicationDomain::ResourceCapabilities::Mail::transport})
0264 {
0265 
0266 }
0267 
0268 Sink::Resource *MailtransportResourceFactory::createResource(const Sink::ResourceContext &context)
0269 {
0270     return new MailtransportResource(context);
0271 }
0272 
0273 void MailtransportResourceFactory::registerFacades(const QByteArray &resourceName, Sink::FacadeFactory &factory)
0274 {
0275     factory.registerFacade<ApplicationDomain::Mail, DefaultFacade<ApplicationDomain::Mail>>(resourceName);
0276 }
0277 
0278 void MailtransportResourceFactory::registerAdaptorFactories(const QByteArray &resourceName, Sink::AdaptorFactoryRegistry &registry)
0279 {
0280     registry.registerFactory<Sink::ApplicationDomain::Mail, DomainTypeAdaptorFactory<ApplicationDomain::Mail>>(resourceName);
0281 }
0282 
0283 void MailtransportResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier)
0284 {
0285     MailtransportResource::removeFromDisk(instanceIdentifier);
0286 }