File indexing completed on 2025-01-05 04:58:39

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 #pragma once
0021 #include <QDeadlineTimer>
0022 
0023 #include <KAsync/Async>
0024 
0025 #include <KMime/KMimeMessage>
0026 #include <KIMAP2/ListJob>
0027 #include <KIMAP2/Session>
0028 #include <KIMAP2/FetchJob>
0029 #include <KIMAP2/SearchJob>
0030 #include <KIMAP2/LoginJob>
0031 
0032 namespace Imap {
0033 
0034 enum ErrorCode {
0035     NoError,
0036     LoginFailed,
0037     HostNotFoundError,
0038     CouldNotConnectError,
0039     SslHandshakeError,
0040     ConnectionLost,
0041     MissingCredentialsError,
0042     CommandFailed,
0043     UnknownError
0044 };
0045 
0046 namespace Flags
0047 {
0048     /// The flag for a message being seen (i.e. opened by user).
0049     extern const char* Seen;
0050     /// The flag for a message being deleted by the user.
0051     extern const char* Deleted;
0052     /// The flag for a message being replied to by the user.
0053     extern const char* Answered;
0054     /// The flag for a message being marked as flagged.
0055     extern const char* Flagged;
0056 }
0057 
0058 namespace FolderFlags
0059 {
0060     extern const char* Noinferiors;
0061     extern const char* Noselect;
0062     extern const char* Marked;
0063     extern const char* Unmarked;
0064     extern const char* Subscribed;
0065     extern const char* Sent;
0066     extern const char* Trash;
0067     extern const char* Archive;
0068     extern const char* Junk;
0069     extern const char* Flagged;
0070     extern const char* All;
0071     extern const char* Drafts;
0072 }
0073 
0074 namespace Capabilities
0075 {
0076     extern const char* Condstore;
0077     extern const char* Uidplus;
0078     extern const char* Namespace;
0079 }
0080 
0081 struct Message {
0082     qint64 uid;
0083     qint64 size;
0084     KIMAP2::MessageAttributes attributes;
0085     KIMAP2::MessageFlags flags;
0086     KMime::Message::Ptr msg;
0087     bool fullPayload;
0088 };
0089 
0090 bool flagsContain(const QByteArray &f, const QByteArrayList &flags);
0091 
0092 struct Folder {
0093     Folder() = default;
0094     Folder(const QString &path, const QString &ns, const QChar &separator, bool noselect_, bool subscribed_, const QByteArrayList &flags_)
0095         : noselect(noselect_),
0096         subscribed(subscribed_),
0097         flags(flags_),
0098         mPath(path),
0099         mNamespace(ns),
0100         mSeparator(separator)
0101     {
0102     }
0103 
0104     Folder(const QString &path_)
0105         : mPath(path_)
0106     {
0107     }
0108 
0109     QString path() const
0110     {
0111         Q_ASSERT(!mPath.isEmpty());
0112         return mPath;
0113     }
0114 
0115     QString parentPath() const
0116     {
0117         Q_ASSERT(!mSeparator.isNull());
0118         auto parts = mPath.split(mSeparator);
0119         parts.removeLast();
0120         auto parentPath = parts.join(mSeparator);
0121         //Don't return the namespace for root folders as parent folder
0122         if (mNamespace.startsWith(parentPath)) {
0123             return QString{};
0124         }
0125         return parentPath;
0126     }
0127 
0128     Folder parentFolder() const
0129     {
0130         Folder parent;
0131         parent.mPath = parentPath();
0132         parent.mNamespace = mNamespace;
0133         parent.mSeparator = mSeparator;
0134         return parent;
0135     }
0136 
0137     QString name() const
0138     {
0139         auto pathParts = mPath.split(mSeparator);
0140         Q_ASSERT(!pathParts.isEmpty());
0141         return pathParts.last();
0142     }
0143 
0144     bool noselect = false;
0145     bool subscribed = false;
0146     QByteArrayList flags;
0147 
0148 private:
0149     QString mPath;
0150     QString mNamespace;
0151     QChar mSeparator;
0152 };
0153 
0154 struct SelectResult {
0155     qint64 uidValidity;
0156     qint64 uidNext;
0157     quint64 highestModSequence;
0158 };
0159 
0160 class Namespaces {
0161 public:
0162     QList<KIMAP2::MailBoxDescriptor> personal;
0163     QList<KIMAP2::MailBoxDescriptor> shared;
0164     QList<KIMAP2::MailBoxDescriptor> user;
0165 
0166     KIMAP2::MailBoxDescriptor getDefaultNamespace()
0167     {
0168         return personal.isEmpty() ? KIMAP2::MailBoxDescriptor{} : personal.first();
0169     }
0170 
0171     KIMAP2::MailBoxDescriptor getNamespace(const QString &mailbox)
0172     {
0173         for (const auto &ns : personal) {
0174             if (mailbox.startsWith(ns.name)) {
0175                 return ns;
0176             }
0177         }
0178         for (const auto &ns : shared) {
0179             if (mailbox.startsWith(ns.name)) {
0180                 return ns;
0181             }
0182         }
0183         for (const auto &ns : user) {
0184             if (mailbox.startsWith(ns.name)) {
0185                 return ns;
0186             }
0187         }
0188         return KIMAP2::MailBoxDescriptor{};
0189     }
0190 };
0191 
0192 using namespace std::chrono_literals;
0193 class CachedSession {
0194 public:
0195 
0196     CachedSession() = default;
0197     CachedSession(KIMAP2::Session *session, const QStringList &cap, const Namespaces &ns) : mSession(session), mCapabilities(cap), mNamespaces(ns), mTimer{30s}
0198     {
0199     }
0200 
0201     bool operator==(const CachedSession &other) const
0202     {
0203         return mSession && (mSession == other.mSession);
0204     }
0205 
0206     bool isConnected() const
0207     {
0208         return (mSession->state() == KIMAP2::Session::State::Authenticated || mSession->state() == KIMAP2::Session::State::Selected) ;
0209     }
0210 
0211     bool isExpired()
0212     {
0213         // Don't touch sessions that have been cached for over 5min
0214         // This is useful e.g. after a sleep, so we don't use a stale session,
0215         // that will then fail anyways.
0216         return mTimer.hasExpired();
0217     }
0218 
0219     bool isValid()
0220     {
0221         return mSession;
0222     }
0223 
0224     KIMAP2::Session *mSession = nullptr;
0225     QStringList mCapabilities;
0226     Namespaces mNamespaces;
0227 
0228 private:
0229     QDeadlineTimer mTimer;
0230 };
0231 
0232 class SessionCache : public QObject {
0233     Q_OBJECT
0234 public:
0235     void recycleSession(const CachedSession &session)
0236     {
0237         if (!session.isConnected()) {
0238             return;
0239         }
0240         QObject::connect(session.mSession, &KIMAP2::Session::stateChanged, this, [this, session](KIMAP2::Session::State newState, KIMAP2::Session::State oldState) {
0241             if (newState == KIMAP2::Session::Disconnected) {
0242                 mSessions.removeOne(session);
0243             }
0244         });
0245         mSessions << session;
0246     }
0247 
0248     CachedSession getSession()
0249     {
0250         while (!mSessions.isEmpty()) {
0251             auto session = mSessions.takeLast();
0252             if (session.isConnected() && !session.isExpired()) {
0253                 return session;
0254             }
0255         }
0256         return {};
0257     }
0258 
0259     bool isEmpty() const
0260     {
0261         return mSessions.isEmpty();
0262     }
0263 
0264     int size() const
0265     {
0266         return mSessions.size();
0267     }
0268 private:
0269     QList<CachedSession> mSessions;
0270 };
0271 
0272 enum EncryptionMode {
0273     NoEncryption,
0274     Tls,
0275     Starttls
0276 };
0277 
0278 using AuthenticationMode = KIMAP2::LoginJob::AuthenticationMode;
0279 
0280 AuthenticationMode fromAuthString(const QString &s);
0281 
0282 class ImapServerProxy {
0283 public:
0284     ImapServerProxy(const QString &serverUrl, int port, EncryptionMode encryption, AuthenticationMode authentication = AuthenticationMode::Plain, SessionCache *sessionCache = nullptr);
0285 
0286     //Standard IMAP calls
0287     KAsync::Job<void> login(const QString &username, const QString &password);
0288     KAsync::Job<void> logout();
0289     KAsync::Job<SelectResult> select(const QString &mailbox);
0290     KAsync::Job<SelectResult> select(const Folder &mailbox);
0291     KAsync::Job<SelectResult> examine(const QString &mailbox);
0292     KAsync::Job<SelectResult> examine(const Folder &mailbox);
0293     KAsync::Job<qint64> append(const QString &mailbox, const QByteArray &content, const QList<QByteArray> &flags = QList<QByteArray>(), const QDateTime &internalDate = QDateTime());
0294     KAsync::Job<void> store(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags);
0295     KAsync::Job<void> storeFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags);
0296     KAsync::Job<void> addFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags);
0297     KAsync::Job<void> removeFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags);
0298     KAsync::Job<void> create(const QString &mailbox);
0299     KAsync::Job<void> rename(const QString &mailbox, const QString &newMailbox);
0300     KAsync::Job<void> remove(const QString &mailbox);
0301     KAsync::Job<void> subscribe(const QString &mailbox);
0302     KAsync::Job<void> expunge();
0303     KAsync::Job<void> expunge(const KIMAP2::ImapSet &set);
0304     KAsync::Job<void> copy(const KIMAP2::ImapSet &set, const QString &newMailbox);
0305     KAsync::Job<QVector<qint64>> search(const KIMAP2::ImapSet &set);
0306     KAsync::Job<QVector<qint64>> search(const KIMAP2::Term &term);
0307 
0308     typedef std::function<void(const KIMAP2::FetchJob::Result &)> FetchCallback;
0309 
0310     KAsync::Job<void> fetch(const KIMAP2::ImapSet &set, KIMAP2::FetchJob::FetchScope scope, FetchCallback callback);
0311     KAsync::Job<void> fetch(const KIMAP2::ImapSet &set, KIMAP2::FetchJob::FetchScope scope, const std::function<void(const Message &)> &callback);
0312     KAsync::Job<void> list(KIMAP2::ListJob::Option option, const std::function<void(const KIMAP2::MailBoxDescriptor &mailboxes,const QList<QByteArray> &flags)> &callback);
0313 
0314     QStringList getCapabilities() const;
0315 
0316     //Composed calls that do login etc.
0317     KAsync::Job<QVector<qint64>> fetchHeaders(const QString &mailbox, qint64 minUid = 1);
0318     KAsync::Job<void> remove(const QString &mailbox, const KIMAP2::ImapSet &set);
0319     KAsync::Job<void> remove(const QString &mailbox, const QByteArray &imapSet);
0320     KAsync::Job<void> move(const QString &mailbox, const KIMAP2::ImapSet &set, const QString &newMailbox);
0321     KAsync::Job<QString> createSubfolder(const QString &parentMailbox, const QString &folderName);
0322     KAsync::Job<QString> renameSubfolder(const QString &mailbox, const QString &newName);
0323     KAsync::Job<QVector<qint64>> fetchUids();
0324     KAsync::Job<QVector<qint64>> fetchUidsSince(const QDate &since);
0325     KAsync::Job<QVector<qint64>> fetchUidsSince(const QDate &since, qint64 lowerBound);
0326 
0327     QString mailboxFromFolder(const Folder &) const;
0328 
0329     KAsync::Job<void> fetchFolders(std::function<void(const Folder &)> callback);
0330     KAsync::Job<void> fetchMessages(const Folder &folder, std::function<void(const Message &)> callback, std::function<void(int, int)> progress = std::function<void(int, int)>());
0331     KAsync::Job<void> fetchMessages(const Folder &folder, qint64 uidNext, std::function<void(const Message &)> callback, std::function<void(int, int)> progress = std::function<void(int, int)>());
0332     KAsync::Job<void> fetchMessages(const Folder &folder, const QVector<qint64> &uidsToFetch, bool headersOnly, std::function<void(const Message &)> callback, std::function<void(int, int)> progress);
0333     KAsync::Job<void> fetchFlags(const KIMAP2::ImapSet &set, qint64 changedsince, std::function<void(const Message &)> callback);
0334     KAsync::Job<QVector<qint64>> fetchUids(const Folder &folder);
0335 
0336 private:
0337     KAsync::Job<void> getMetaData(std::function<void(const QHash<QString, QMap<QByteArray, QByteArray> > &metadata)> callback);
0338     bool isGmail() const;
0339 
0340     QString getNamespace(const QString &name);
0341     QObject mGuard;
0342     SessionCache *mSessionCache;
0343     KIMAP2::Session *mSession;
0344     QStringList mCapabilities;
0345     Namespaces mNamespaces;
0346     EncryptionMode mEncryptionMode;
0347     AuthenticationMode mAuthenticationMode;
0348     const QString mServerUrl;
0349     const int mPort;
0350 };
0351 
0352 }