File indexing completed on 2025-01-05 04:58:38
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 #include "imapserverproxy.h" 0020 0021 #include <KIMAP2/LoginJob> 0022 #include <KIMAP2/LogoutJob> 0023 #include <KIMAP2/NamespaceJob> 0024 #include <KIMAP2/SelectJob> 0025 #include <KIMAP2/AppendJob> 0026 #include <KIMAP2/CreateJob> 0027 #include <KIMAP2/CopyJob> 0028 #include <KIMAP2/RenameJob> 0029 #include <KIMAP2/DeleteJob> 0030 #include <KIMAP2/StoreJob> 0031 #include <KIMAP2/ExpungeJob> 0032 #include <KIMAP2/CapabilitiesJob> 0033 #include <KIMAP2/SearchJob> 0034 #include <KIMAP2/GetMetaDataJob> 0035 #include <KIMAP2/SubscribeJob> 0036 0037 #include <KCoreAddons/KJob> 0038 #include <QHostInfo> 0039 0040 #include "log.h" 0041 #include "test.h" 0042 0043 using namespace Imap; 0044 0045 const char* Imap::Flags::Seen = "\\Seen"; 0046 const char* Imap::Flags::Deleted = "\\Deleted"; 0047 const char* Imap::Flags::Answered = "\\Answered"; 0048 const char* Imap::Flags::Flagged = "\\Flagged"; 0049 0050 const char* Imap::FolderFlags::Noselect = "\\Noselect"; 0051 const char* Imap::FolderFlags::Noinferiors = "\\Noinferiors"; 0052 const char* Imap::FolderFlags::Marked = "\\Marked"; 0053 const char* Imap::FolderFlags::Unmarked = "\\Unmarked"; 0054 const char* Imap::FolderFlags::Subscribed = "\\Subscribed"; 0055 //Special use 0056 const char* Imap::FolderFlags::Sent = "\\Sent"; 0057 const char* Imap::FolderFlags::Trash = "\\Trash"; 0058 const char* Imap::FolderFlags::Archive = "\\Archive"; 0059 const char* Imap::FolderFlags::Junk = "\\Junk"; 0060 const char* Imap::FolderFlags::Flagged = "\\Flagged"; 0061 const char* Imap::FolderFlags::Drafts = "\\Drafts"; 0062 0063 const char* Imap::Capabilities::Namespace = "NAMESPACE"; 0064 const char* Imap::Capabilities::Uidplus = "UIDPLUS"; 0065 const char* Imap::Capabilities::Condstore = "CONDSTORE"; 0066 0067 static int translateImapError(KJob *job) 0068 { 0069 switch (job->error()) { 0070 case KIMAP2::HostNotFound: 0071 return Imap::HostNotFoundError; 0072 case KIMAP2::CouldNotConnect: 0073 return Imap::CouldNotConnectError; 0074 case KIMAP2::SslHandshakeFailed: 0075 return Imap::SslHandshakeError; 0076 case KIMAP2::ConnectionLost: 0077 return Imap::ConnectionLost; 0078 case KIMAP2::LoginFailed: 0079 return Imap::LoginFailed; 0080 case KIMAP2::CommandFailed: 0081 return Imap::CommandFailed; 0082 } 0083 return Imap::UnknownError; 0084 } 0085 0086 template <typename T> 0087 static KAsync::Job<T> runJob(KJob *job, const std::function<T(KJob*)> &f) 0088 { 0089 return KAsync::start<T>([job, f](KAsync::Future<T> &future) { 0090 QObject::connect(job, &KJob::result, [&future, f](KJob *job) { 0091 SinkTrace() << "Job done: " << job->metaObject()->className(); 0092 if (job->error()) { 0093 SinkWarning() << "Job failed: " << job->errorString() << job->metaObject()->className() << job->error(); 0094 auto proxyError = translateImapError(job); 0095 future.setError(proxyError, job->errorString()); 0096 } else { 0097 future.setValue(f(job)); 0098 future.setFinished(); 0099 } 0100 }); 0101 SinkTrace() << "Starting job: " << job->metaObject()->className(); 0102 job->start(); 0103 }); 0104 } 0105 0106 static KAsync::Job<void> runJob(KJob *job) 0107 { 0108 return KAsync::start<void>([job](KAsync::Future<void> &future) { 0109 QObject::connect(job, &KJob::result, [&future](KJob *job) { 0110 SinkTrace() << "Job done: " << job->metaObject()->className(); 0111 if (job->error()) { 0112 SinkWarning() << "Job failed: " << job->errorString() << job->metaObject()->className() << job->error(); 0113 auto proxyError = translateImapError(job); 0114 future.setError(proxyError, job->errorString()); 0115 } else { 0116 future.setFinished(); 0117 } 0118 }); 0119 SinkTrace() << "Starting job: " << job->metaObject()->className(); 0120 job->start(); 0121 }); 0122 } 0123 0124 static int socketTimeout() 0125 { 0126 if (Sink::Test::testModeEnabled()) { 0127 return 5; 0128 } 0129 return 40; 0130 } 0131 0132 KIMAP2::Session *createNewSession(const QString &serverUrl, int port) 0133 { 0134 auto newSession = new KIMAP2::Session(serverUrl, qint16(port)); 0135 newSession->setTimeout(socketTimeout()); 0136 QObject::connect(newSession, &KIMAP2::Session::sslErrors, [=](const QList<QSslError> &errors) { 0137 SinkWarning() << "Received SSL errors:"; 0138 for (const auto &e : errors) { 0139 SinkWarning() << " " << e.error() << ":" << e.errorString() << "Certificate: " << e.certificate().toText(); 0140 } 0141 newSession->ignoreErrors(errors); 0142 }); 0143 return newSession; 0144 } 0145 0146 ImapServerProxy::ImapServerProxy(const QString &serverUrl, int port, EncryptionMode encryptionMode, AuthenticationMode authenticationMode, SessionCache *sessionCache) : mSessionCache(sessionCache), mSession(nullptr), mEncryptionMode(encryptionMode), mAuthenticationMode(authenticationMode), mServerUrl(serverUrl), mPort(port) 0147 { 0148 } 0149 0150 QDebug operator<<(QDebug debug, const KIMAP2::MailBoxDescriptor &c) 0151 { 0152 QDebugStateSaver saver(debug); 0153 debug.nospace() << c.name; 0154 return debug; 0155 } 0156 0157 KAsync::Job<void> ImapServerProxy::login(const QString &username, const QString &password) 0158 { 0159 if (password.isEmpty()) { 0160 return KAsync::error(Imap::MissingCredentialsError); 0161 } 0162 if (mSessionCache) { 0163 auto session = mSessionCache->getSession(); 0164 if (session.isValid()) { 0165 SinkLog() << "Got existing session from session cache."; 0166 mSession = session.mSession; 0167 mCapabilities = session.mCapabilities; 0168 mNamespaces = session.mNamespaces; 0169 } 0170 } 0171 if (!mSession) { 0172 mSession = createNewSession(mServerUrl, mPort); 0173 } 0174 Q_ASSERT(mSession); 0175 if (mSession->state() == KIMAP2::Session::Authenticated || mSession->state() == KIMAP2::Session::Selected) { 0176 //If we blindly reuse the socket it may very well be stale and then we have to wait for it to time out. 0177 //A hostlookup should be fast (a couple of milliseconds once cached), and can typcially tell us quickly 0178 //if the host is no longer available. 0179 auto info = QHostInfo::fromName(mSession->hostName()); 0180 if (info.error()) { 0181 SinkLog() << "Failed host lookup, closing the socket" << info.errorString(); 0182 mSession->close(); 0183 mSession = nullptr; 0184 return KAsync::error(Imap::HostNotFoundError); 0185 } else { 0186 //Prevent the socket from timing out right away, right here (otherwise it just might time out right before we were able to start the job) 0187 mSession->setTimeout(socketTimeout()); 0188 SinkLog() << "Reusing existing session."; 0189 return KAsync::null(); 0190 } 0191 } 0192 auto loginJob = new KIMAP2::LoginJob(mSession); 0193 loginJob->setUserName(username); 0194 loginJob->setPassword(password); 0195 if (mEncryptionMode == Starttls) { 0196 loginJob->setEncryptionMode(QSsl::TlsV1_0OrLater, true); 0197 } else if (mEncryptionMode == Tls) { 0198 loginJob->setEncryptionMode(QSsl::AnyProtocol, false); 0199 } 0200 loginJob->setAuthenticationMode(mAuthenticationMode); 0201 0202 auto capabilitiesJob = new KIMAP2::CapabilitiesJob(mSession); 0203 QObject::connect(capabilitiesJob, &KIMAP2::CapabilitiesJob::capabilitiesReceived, &mGuard, [this](const QStringList &capabilities) { 0204 mCapabilities = capabilities; 0205 }); 0206 auto namespaceJob = new KIMAP2::NamespaceJob(mSession); 0207 0208 return runJob(loginJob).then(runJob(capabilitiesJob)).then([this](){ 0209 SinkTrace() << "Supported capabilities: " << mCapabilities; 0210 QStringList requiredExtensions = QStringList() << Capabilities::Uidplus << Capabilities::Namespace; 0211 for (const auto &requiredExtension : requiredExtensions) { 0212 if (!mCapabilities.contains(requiredExtension)) { 0213 SinkWarning() << "Server doesn't support required capability: " << requiredExtension; 0214 //TODO fail the job 0215 } 0216 } 0217 }).then(runJob(namespaceJob)).then([this, namespaceJob] { 0218 mNamespaces.personal = namespaceJob->personalNamespaces(); 0219 mNamespaces.shared = namespaceJob->sharedNamespaces(); 0220 mNamespaces.user = namespaceJob->userNamespaces(); 0221 // SinkTrace() << "Found personal namespaces: " << mNamespaces.personal; 0222 // SinkTrace() << "Found shared namespaces: " << mNamespaces.shared; 0223 // SinkTrace() << "Found user namespaces: " << mNamespaces.user; 0224 }); 0225 } 0226 0227 KAsync::Job<void> ImapServerProxy::logout() 0228 { 0229 if (mSessionCache) { 0230 SinkLog() << "Recycling session."; 0231 mSessionCache->recycleSession({mSession, mCapabilities, mNamespaces}); 0232 return KAsync::null(); 0233 } 0234 if (mSession->state() == KIMAP2::Session::State::Authenticated || mSession->state() == KIMAP2::Session::State::Selected) { 0235 return runJob(new KIMAP2::LogoutJob(mSession)); 0236 } else { 0237 return KAsync::null(); 0238 } 0239 } 0240 0241 bool ImapServerProxy::isGmail() const 0242 { 0243 //Magic capability that only gmail has 0244 return mCapabilities.contains("X-GM-EXT-1"); 0245 } 0246 0247 KAsync::Job<SelectResult> ImapServerProxy::select(const QString &mailbox) 0248 { 0249 auto select = new KIMAP2::SelectJob(mSession); 0250 select->setMailBox(mailbox); 0251 select->setCondstoreEnabled(mCapabilities.contains(Capabilities::Condstore)); 0252 return runJob<SelectResult>(select, [select](KJob* job) -> SelectResult { 0253 return {select->uidValidity(), select->nextUid(), select->highestModSequence()}; 0254 }).then([=] (const KAsync::Error &error, const SelectResult &result) { 0255 if (error) { 0256 SinkWarning() << "Select failed: " << mailbox; 0257 return KAsync::error<SelectResult>(error); 0258 } 0259 return KAsync::value<SelectResult>(result); 0260 }); 0261 } 0262 0263 KAsync::Job<SelectResult> ImapServerProxy::select(const Folder &folder) 0264 { 0265 return select(mailboxFromFolder(folder)); 0266 } 0267 0268 KAsync::Job<SelectResult> ImapServerProxy::examine(const QString &mailbox) 0269 { 0270 auto select = new KIMAP2::SelectJob(mSession); 0271 select->setOpenReadOnly(true); 0272 select->setMailBox(mailbox); 0273 select->setCondstoreEnabled(mCapabilities.contains(Capabilities::Condstore)); 0274 return runJob<SelectResult>(select, [select](KJob* job) -> SelectResult { 0275 return {select->uidValidity(), select->nextUid(), select->highestModSequence()}; 0276 }).then([=] (const KAsync::Error &error, const SelectResult &result) { 0277 if (error) { 0278 SinkWarning() << "Examine failed: " << mailbox; 0279 return KAsync::error<SelectResult>(error); 0280 } 0281 return KAsync::value<SelectResult>(result); 0282 }); 0283 } 0284 0285 KAsync::Job<SelectResult> ImapServerProxy::examine(const Folder &folder) 0286 { 0287 return examine(mailboxFromFolder(folder)); 0288 } 0289 0290 KAsync::Job<qint64> ImapServerProxy::append(const QString &mailbox, const QByteArray &content, const QList<QByteArray> &flags, const QDateTime &internalDate) 0291 { 0292 Q_ASSERT(mSession); 0293 auto append = new KIMAP2::AppendJob(mSession); 0294 append->setMailBox(mailbox); 0295 append->setContent(content); 0296 append->setFlags(flags); 0297 append->setInternalDate(internalDate); 0298 return runJob<qint64>(append, [](KJob *job) -> qint64{ 0299 return static_cast<KIMAP2::AppendJob*>(job)->uid(); 0300 }); 0301 } 0302 0303 KAsync::Job<void> ImapServerProxy::store(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags) 0304 { 0305 return storeFlags(set, flags); 0306 } 0307 0308 KAsync::Job<void> ImapServerProxy::storeFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags) 0309 { 0310 Q_ASSERT(mSession); 0311 auto store = new KIMAP2::StoreJob(mSession); 0312 store->setUidBased(true); 0313 store->setMode(KIMAP2::StoreJob::SetFlags); 0314 store->setSequenceSet(set); 0315 store->setFlags(flags); 0316 return runJob(store); 0317 } 0318 0319 KAsync::Job<void> ImapServerProxy::addFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags) 0320 { 0321 Q_ASSERT(mSession); 0322 auto store = new KIMAP2::StoreJob(mSession); 0323 store->setUidBased(true); 0324 store->setMode(KIMAP2::StoreJob::AppendFlags); 0325 store->setSequenceSet(set); 0326 store->setFlags(flags); 0327 return runJob(store); 0328 } 0329 0330 KAsync::Job<void> ImapServerProxy::removeFlags(const KIMAP2::ImapSet &set, const QList<QByteArray> &flags) 0331 { 0332 Q_ASSERT(mSession); 0333 auto store = new KIMAP2::StoreJob(mSession); 0334 store->setUidBased(true); 0335 store->setMode(KIMAP2::StoreJob::RemoveFlags); 0336 store->setSequenceSet(set); 0337 store->setFlags(flags); 0338 return runJob(store); 0339 } 0340 0341 KAsync::Job<void> ImapServerProxy::create(const QString &mailbox) 0342 { 0343 Q_ASSERT(mSession); 0344 auto create = new KIMAP2::CreateJob(mSession); 0345 create->setMailBox(mailbox); 0346 return runJob(create); 0347 } 0348 0349 KAsync::Job<void> ImapServerProxy::subscribe(const QString &mailbox) 0350 { 0351 Q_ASSERT(mSession); 0352 auto job = new KIMAP2::SubscribeJob(mSession); 0353 job->setMailBox(mailbox); 0354 return runJob(job); 0355 } 0356 0357 KAsync::Job<void> ImapServerProxy::rename(const QString &mailbox, const QString &newMailbox) 0358 { 0359 Q_ASSERT(mSession); 0360 auto rename = new KIMAP2::RenameJob(mSession); 0361 rename->setSourceMailBox(mailbox); 0362 rename->setDestinationMailBox(newMailbox); 0363 return runJob(rename); 0364 } 0365 0366 KAsync::Job<void> ImapServerProxy::remove(const QString &mailbox) 0367 { 0368 Q_ASSERT(mSession); 0369 auto job = new KIMAP2::DeleteJob(mSession); 0370 job->setMailBox(mailbox); 0371 return runJob(job); 0372 } 0373 0374 KAsync::Job<void> ImapServerProxy::expunge() 0375 { 0376 Q_ASSERT(mSession); 0377 auto job = new KIMAP2::ExpungeJob(mSession); 0378 return runJob(job); 0379 } 0380 0381 KAsync::Job<void> ImapServerProxy::expunge(const KIMAP2::ImapSet &set) 0382 { 0383 Q_ASSERT(mSession); 0384 //FIXME implement UID EXPUNGE 0385 auto job = new KIMAP2::ExpungeJob(mSession); 0386 return runJob(job); 0387 } 0388 0389 KAsync::Job<void> ImapServerProxy::copy(const KIMAP2::ImapSet &set, const QString &newMailbox) 0390 { 0391 Q_ASSERT(mSession); 0392 auto copy = new KIMAP2::CopyJob(mSession); 0393 copy->setSequenceSet(set); 0394 copy->setUidBased(true); 0395 copy->setMailBox(newMailbox); 0396 return runJob(copy); 0397 } 0398 0399 KAsync::Job<void> ImapServerProxy::fetch(const KIMAP2::ImapSet &set, KIMAP2::FetchJob::FetchScope scope, FetchCallback callback) 0400 { 0401 Q_ASSERT(mSession); 0402 auto fetch = new KIMAP2::FetchJob(mSession); 0403 fetch->setSequenceSet(set); 0404 fetch->setUidBased(true); 0405 fetch->setScope(scope); 0406 fetch->setAvoidParsing(true); 0407 QObject::connect(fetch, &KIMAP2::FetchJob::resultReceived, callback); 0408 return runJob(fetch); 0409 } 0410 0411 KAsync::Job<QVector<qint64>> ImapServerProxy::search(const KIMAP2::ImapSet &set) 0412 { 0413 return search(KIMAP2::Term(KIMAP2::Term::Uid, set)); 0414 } 0415 0416 KAsync::Job<QVector<qint64>> ImapServerProxy::search(const KIMAP2::Term &term) 0417 { 0418 Q_ASSERT(mSession); 0419 auto search = new KIMAP2::SearchJob(mSession); 0420 search->setTerm(term); 0421 search->setUidBased(true); 0422 return runJob<QVector<qint64>>(search, [](KJob *job) -> QVector<qint64> { 0423 return static_cast<KIMAP2::SearchJob*>(job)->results(); 0424 }); 0425 } 0426 0427 KAsync::Job<void> ImapServerProxy::fetch(const KIMAP2::ImapSet &set, KIMAP2::FetchJob::FetchScope scope, const std::function<void(const Message &)> &callback) 0428 { 0429 const bool fullPayload = (scope.mode == KIMAP2::FetchJob::FetchScope::Full); 0430 return fetch(set, scope, 0431 [callback, fullPayload](const KIMAP2::FetchJob::Result &result) { 0432 callback(Message{result.uid, result.size, result.attributes, result.flags, result.message, fullPayload}); 0433 }); 0434 } 0435 0436 QStringList ImapServerProxy::getCapabilities() const 0437 { 0438 return mCapabilities; 0439 } 0440 0441 KAsync::Job<QVector<qint64>> ImapServerProxy::fetchHeaders(const QString &mailbox, const qint64 minUid) 0442 { 0443 auto list = QSharedPointer<QVector<qint64>>::create(); 0444 KIMAP2::FetchJob::FetchScope scope; 0445 scope.mode = KIMAP2::FetchJob::FetchScope::Flags; 0446 0447 //Fetch headers of all messages 0448 return fetch(KIMAP2::ImapSet(minUid, 0), scope, 0449 [list](const KIMAP2::FetchJob::Result &result) { 0450 // SinkTrace() << "Received " << uids.size() << " headers from " << mailbox; 0451 // SinkTrace() << uids.size() << sizes.size() << attrs.size() << flags.size() << messages.size(); 0452 0453 //TODO based on the data available here, figure out which messages to actually fetch 0454 //(we only fetched headers and structure so far) 0455 //We could i.e. build chunks to fetch based on the size 0456 0457 list->append(result.uid); 0458 }) 0459 .then([list](){ 0460 return *list; 0461 }); 0462 } 0463 0464 KAsync::Job<QVector<qint64>> ImapServerProxy::fetchUids() 0465 { 0466 auto notDeleted = KIMAP2::Term(KIMAP2::Term::Deleted); 0467 notDeleted.setNegated(true); 0468 return search(notDeleted); 0469 } 0470 0471 KAsync::Job<QVector<qint64>> ImapServerProxy::fetchUidsSince(const QDate &since, qint64 lowerBound) 0472 { 0473 auto notDeleted = KIMAP2::Term{KIMAP2::Term::Deleted}; 0474 notDeleted.setNegated(true); 0475 0476 return search( 0477 KIMAP2::Term{KIMAP2::Term::Or, { 0478 KIMAP2::Term{KIMAP2::Term::And, {{KIMAP2::Term::Since, since}, notDeleted}}, 0479 KIMAP2::Term{KIMAP2::Term::And, {{KIMAP2::Term::Uid, KIMAP2::ImapSet{lowerBound, 0}}, notDeleted}} 0480 }} 0481 ); 0482 } 0483 0484 KAsync::Job<QVector<qint64>> ImapServerProxy::fetchUidsSince(const QDate &since) 0485 { 0486 auto notDeleted = KIMAP2::Term{KIMAP2::Term::Deleted}; 0487 notDeleted.setNegated(true); 0488 0489 return search(KIMAP2::Term{KIMAP2::Term::And, {{KIMAP2::Term::Since, since}, notDeleted}}); 0490 } 0491 0492 KAsync::Job<void> ImapServerProxy::list(KIMAP2::ListJob::Option option, const std::function<void(const KIMAP2::MailBoxDescriptor &mailboxes, const QList<QByteArray> &flags)> &callback) 0493 { 0494 Q_ASSERT(mSession); 0495 auto listJob = new KIMAP2::ListJob(mSession); 0496 listJob->setOption(option); 0497 // listJob->setQueriedNamespaces(serverNamespaces()); 0498 QObject::connect(listJob, &KIMAP2::ListJob::resultReceived, 0499 listJob, callback); 0500 return runJob(listJob); 0501 } 0502 0503 KAsync::Job<void> ImapServerProxy::remove(const QString &mailbox, const KIMAP2::ImapSet &set) 0504 { 0505 return select(mailbox).then<void>(store(set, QByteArrayList() << Flags::Deleted)).then<void>(expunge(set)); 0506 } 0507 0508 KAsync::Job<void> ImapServerProxy::remove(const QString &mailbox, const QByteArray &imapSet) 0509 { 0510 const auto set = KIMAP2::ImapSet::fromImapSequenceSet(imapSet); 0511 return remove(mailbox, set); 0512 } 0513 0514 0515 KAsync::Job<void> ImapServerProxy::move(const QString &mailbox, const KIMAP2::ImapSet &set, const QString &newMailbox) 0516 { 0517 return select(mailbox).then<void>(copy(set, newMailbox)).then<void>(store(set, QByteArrayList() << Flags::Deleted)).then<void>(expunge(set)); 0518 } 0519 0520 KAsync::Job<QString> ImapServerProxy::createSubfolder(const QString &parentMailbox, const QString &folderName) 0521 { 0522 return KAsync::start<QString>([this, parentMailbox, folderName]() { 0523 QString folder; 0524 if (parentMailbox.isEmpty()) { 0525 auto ns = mNamespaces.getDefaultNamespace(); 0526 folder = ns.name + folderName; 0527 } else { 0528 auto ns = mNamespaces.getNamespace(parentMailbox); 0529 folder = parentMailbox + ns.separator + folderName; 0530 } 0531 SinkTrace() << "Creating subfolder: " << folder; 0532 return create(folder) 0533 .then([=]() { 0534 return folder; 0535 }); 0536 }); 0537 } 0538 0539 KAsync::Job<QString> ImapServerProxy::renameSubfolder(const QString &oldMailbox, const QString &newName) 0540 { 0541 return KAsync::start<QString>([this, oldMailbox, newName] { 0542 auto ns = mNamespaces.getNamespace(oldMailbox); 0543 auto parts = oldMailbox.split(ns.separator); 0544 parts.removeLast(); 0545 QString folder = parts.join(ns.separator) + ns.separator + newName; 0546 SinkTrace() << "Renaming subfolder: " << oldMailbox << folder; 0547 return rename(oldMailbox, folder) 0548 .then([=]() { 0549 return folder; 0550 }); 0551 }); 0552 } 0553 0554 QString ImapServerProxy::getNamespace(const QString &name) 0555 { 0556 auto ns = mNamespaces.getNamespace(name); 0557 return ns.name; 0558 } 0559 0560 static bool caseInsensitiveContains(const QByteArray &f, const QByteArrayList &list) { 0561 return list.contains(f) || list.contains(f.toLower()); 0562 } 0563 0564 bool Imap::flagsContain(const QByteArray &f, const QByteArrayList &flags) 0565 { 0566 return caseInsensitiveContains(f, flags); 0567 } 0568 0569 AuthenticationMode Imap::fromAuthString(const QString &s) 0570 { 0571 if (s == QStringLiteral("CLEARTEXT")) return KIMAP2::LoginJob::ClearText; 0572 if (s == QStringLiteral("LOGIN")) return KIMAP2::LoginJob::Login; 0573 if (s == QStringLiteral("PLAIN")) return KIMAP2::LoginJob::Plain; 0574 if (s == QStringLiteral("CRAM-MD5")) return KIMAP2::LoginJob::CramMD5; 0575 if (s == QStringLiteral("DIGEST-MD5")) return KIMAP2::LoginJob::DigestMD5; 0576 if (s == QStringLiteral("GSSAPI")) return KIMAP2::LoginJob::GSSAPI; 0577 if (s == QStringLiteral("ANONYMOUS")) return KIMAP2::LoginJob::Anonymous; 0578 if (s == QStringLiteral("XOAUTH2")) return KIMAP2::LoginJob::XOAuth2; 0579 return KIMAP2::LoginJob::Plain; 0580 } 0581 0582 static void reportFolder(const Folder &f, QSharedPointer<QSet<QString>> reportedList, std::function<void(const Folder &)> callback) { 0583 if (!reportedList->contains(f.path())) { 0584 reportedList->insert(f.path()); 0585 auto c = f; 0586 c.noselect = true; 0587 callback(c); 0588 if (!f.parentPath().isEmpty()){ 0589 reportFolder(f.parentFolder(), reportedList, callback); 0590 } 0591 } 0592 } 0593 0594 KAsync::Job<void> ImapServerProxy::getMetaData(std::function<void(const QHash<QString, QMap<QByteArray, QByteArray> > &metadata)> callback) 0595 { 0596 if (!mCapabilities.contains("METADATA")) { 0597 return KAsync::null(); 0598 } 0599 Q_ASSERT(mSession); 0600 KIMAP2::GetMetaDataJob *meta = new KIMAP2::GetMetaDataJob(mSession); 0601 meta->setMailBox(QLatin1String("*")); 0602 meta->setServerCapability( KIMAP2::MetaDataJobBase::Metadata ); 0603 meta->setDepth(KIMAP2::GetMetaDataJob::AllLevels); 0604 meta->addRequestedEntry("/shared/vendor/kolab/folder-type"); 0605 meta->addRequestedEntry("/private/vendor/kolab/folder-type"); 0606 return runJob(meta).then<void>([callback, meta] () { 0607 callback(meta->allMetaDataForMailboxes()); 0608 }); 0609 } 0610 0611 KAsync::Job<void> ImapServerProxy::fetchFolders(std::function<void(const Folder &)> callback) 0612 { 0613 SinkTrace() << "Fetching folders"; 0614 auto subscribedList = QSharedPointer<QSet<QString>>::create() ; 0615 auto reportedList = QSharedPointer<QSet<QString>>::create() ; 0616 auto metaData = QSharedPointer<QHash<QString, QMap<QByteArray, QByteArray>>>::create() ; 0617 return getMetaData([=] (const QHash<QString, QMap<QByteArray, QByteArray>> &m) { 0618 *metaData = m; 0619 }).then(list(KIMAP2::ListJob::NoOption, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &){ 0620 *subscribedList << mailbox.name; 0621 })).then(list(KIMAP2::ListJob::IncludeUnsubscribed, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &flags) { 0622 bool noselect = caseInsensitiveContains(FolderFlags::Noselect, flags); 0623 bool subscribed = subscribedList->contains(mailbox.name); 0624 if (isGmail()) { 0625 bool inbox = mailbox.name.toLower() == "inbox"; 0626 bool sent = caseInsensitiveContains(FolderFlags::Sent, flags); 0627 bool drafts = caseInsensitiveContains(FolderFlags::Drafts, flags); 0628 bool trash = caseInsensitiveContains(FolderFlags::Trash, flags); 0629 /** 0630 * Because gmail duplicates messages all over the place we only support a few selected folders for now that should be mostly exclusive. 0631 */ 0632 if (!(inbox || sent || drafts || trash)) { 0633 return; 0634 } 0635 } 0636 SinkTrace() << "Found mailbox: " << mailbox.name << flags << FolderFlags::Noselect << noselect << " sub: " << subscribed; 0637 //Ignore all non-mail folders 0638 if (metaData->contains(mailbox.name)) { 0639 auto m = metaData->value(mailbox.name); 0640 auto sharedType = m.value("/shared/vendor/kolab/folder-type"); 0641 auto privateType = m.value("/private/vendor/kolab/folder-type"); 0642 auto type = !privateType.isEmpty() ? privateType : sharedType; 0643 if (!type.isEmpty() && !type.contains("mail")) { 0644 SinkTrace() << "Skipping due to folder type: " << type; 0645 return; 0646 } 0647 } 0648 auto ns = getNamespace(mailbox.name); 0649 auto folder = Folder{mailbox.name, ns, mailbox.separator, noselect, subscribed, flags}; 0650 0651 //call callback for parents if that didn't already happen. 0652 //This is necessary because we can have missing bits in the hierarchy in IMAP, but this will not work in sink because we'd end up with an incomplete tree. 0653 if (!folder.parentPath().isEmpty() && !reportedList->contains(folder.parentPath())) { 0654 reportFolder(folder.parentFolder(), reportedList, callback); 0655 } 0656 reportedList->insert(folder.path()); 0657 callback(folder); 0658 })); 0659 } 0660 0661 QString ImapServerProxy::mailboxFromFolder(const Folder &folder) const 0662 { 0663 Q_ASSERT(!folder.path().isEmpty()); 0664 return folder.path(); 0665 } 0666 0667 KAsync::Job<void> ImapServerProxy::fetchFlags(const KIMAP2::ImapSet &set, qint64 changedsince, std::function<void(const Message &)> callback) 0668 { 0669 KIMAP2::FetchJob::FetchScope scope; 0670 scope.mode = KIMAP2::FetchJob::FetchScope::Flags; 0671 scope.changedSince = changedsince; 0672 0673 return fetch(set, scope, callback); 0674 } 0675 0676 KAsync::Job<void> ImapServerProxy::fetchMessages(const Folder &folder, qint64 uidNext, std::function<void(const Message &)> callback, std::function<void(int, int)> progress) 0677 { 0678 auto time = QSharedPointer<QTime>::create(); 0679 time->start(); 0680 return select(folder).then<void, SelectResult>([this, callback, folder, time, progress, uidNext](const SelectResult &selectResult) -> KAsync::Job<void> { 0681 SinkTrace() << "UIDNEXT " << folder.path() << selectResult.uidNext << uidNext; 0682 if (selectResult.uidNext == (uidNext + 1)) { 0683 SinkTrace()<< folder.path() << "Uidnext didn't change, nothing to do."; 0684 return KAsync::null<void>(); 0685 } 0686 0687 SinkTrace() << "Fetching messages from " << folder.path() << selectResult.uidNext << uidNext; 0688 return fetchHeaders(mailboxFromFolder(folder), (uidNext + 1)).then<void, QVector<qint64>>([this, callback, time, progress, folder](const QVector<qint64> &uidsToFetch){ 0689 SinkTrace() << "Fetched headers" << folder.path(); 0690 SinkTrace() << " Total: " << uidsToFetch.size(); 0691 SinkTrace() << " Uids to fetch: " << uidsToFetch; 0692 SinkTrace() << " Took: " << Sink::Log::TraceTime(time->elapsed()); 0693 return fetchMessages(folder, uidsToFetch, false, callback, progress); 0694 }); 0695 0696 }); 0697 } 0698 0699 KAsync::Job<void> ImapServerProxy::fetchMessages(const Folder &folder, const QVector<qint64> &uidsToFetch, bool headersOnly, std::function<void(const Message &)> callback, std::function<void(int, int)> progress) 0700 { 0701 auto time = QSharedPointer<QTime>::create(); 0702 time->start(); 0703 return select(folder).then<void, SelectResult>([this, callback, folder, time, progress, uidsToFetch, headersOnly](const SelectResult &selectResult) -> KAsync::Job<void> { 0704 0705 SinkTrace() << "Fetching messages" << folder.path(); 0706 SinkTrace() << " Total: " << uidsToFetch.size(); 0707 SinkTrace() << " Uids to fetch: " << uidsToFetch; 0708 auto totalCount = uidsToFetch.size(); 0709 if (progress) { 0710 progress(0, totalCount); 0711 } 0712 if (uidsToFetch.isEmpty()) { 0713 SinkTrace() << "Nothing to fetch"; 0714 return KAsync::null<void>(); 0715 } 0716 KIMAP2::FetchJob::FetchScope scope; 0717 scope.parts.clear(); 0718 if (headersOnly) { 0719 scope.mode = KIMAP2::FetchJob::FetchScope::Headers; 0720 } else { 0721 scope.mode = KIMAP2::FetchJob::FetchScope::Full; 0722 } 0723 0724 KIMAP2::ImapSet set; 0725 set.add(uidsToFetch); 0726 auto count = QSharedPointer<int>::create(); 0727 return fetch(set, scope, [=](const Message &message) { 0728 *count += 1; 0729 if (progress) { 0730 progress(*count, totalCount); 0731 } 0732 callback(message); 0733 }); 0734 }) 0735 .then([time]() { 0736 SinkTrace() << "The fetch took: " << Sink::Log::TraceTime(time->elapsed()); 0737 }); 0738 } 0739 0740 KAsync::Job<void> ImapServerProxy::fetchMessages(const Folder &folder, std::function<void(const Message &)> callback, std::function<void(int, int)> progress) 0741 { 0742 return fetchMessages(folder, 0, callback, progress); 0743 } 0744 0745 KAsync::Job<QVector<qint64>> ImapServerProxy::fetchUids(const Folder &folder) 0746 { 0747 return select(mailboxFromFolder(folder)).then(fetchUids()); 0748 }