summaryrefslogtreecommitdiffstats
path: root/examples/imapresource
diff options
context:
space:
mode:
Diffstat (limited to 'examples/imapresource')
-rw-r--r--examples/imapresource/CMakeLists.txt2
-rw-r--r--examples/imapresource/imapresource.cpp126
-rw-r--r--examples/imapresource/imapserverproxy.cpp88
-rw-r--r--examples/imapresource/imapserverproxy.h30
-rw-r--r--examples/imapresource/tests/imapmailsyncbenchmark.cpp2
-rw-r--r--examples/imapresource/tests/imapserverproxytest.cpp2
-rw-r--r--examples/imapresource/tests/populatemailbox.sh11
-rw-r--r--examples/imapresource/tests/resetmailbox.sh3
8 files changed, 198 insertions, 66 deletions
diff --git a/examples/imapresource/CMakeLists.txt b/examples/imapresource/CMakeLists.txt
index 46a8b08..5d2d38b 100644
--- a/examples/imapresource/CMakeLists.txt
+++ b/examples/imapresource/CMakeLists.txt
@@ -4,7 +4,7 @@ add_definitions(-DQT_PLUGIN)
4include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) 4include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
5 5
6find_package(KF5 COMPONENTS REQUIRED Mime) 6find_package(KF5 COMPONENTS REQUIRED Mime)
7find_package(KIMAP2 0.0.1 REQUIRED) 7find_package(KIMAP2 0.2 REQUIRED)
8 8
9include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) 9include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
10 10
diff --git a/examples/imapresource/imapresource.cpp b/examples/imapresource/imapresource.cpp
index 0579dae..81c808b 100644
--- a/examples/imapresource/imapresource.cpp
+++ b/examples/imapresource/imapresource.cpp
@@ -46,8 +46,6 @@
46#define ENTITY_TYPE_MAIL "mail" 46#define ENTITY_TYPE_MAIL "mail"
47#define ENTITY_TYPE_FOLDER "folder" 47#define ENTITY_TYPE_FOLDER "folder"
48 48
49SINK_DEBUG_AREA("imapresource")
50
51Q_DECLARE_METATYPE(QSharedPointer<Imap::ImapServerProxy>) 49Q_DECLARE_METATYPE(QSharedPointer<Imap::ImapServerProxy>)
52 50
53using namespace Imap; 51using namespace Imap;
@@ -87,6 +85,25 @@ static QByteArray parentRid(const Imap::Folder &folder)
87 return folder.parentPath().toUtf8(); 85 return folder.parentPath().toUtf8();
88} 86}
89 87
88static QByteArray getSpecialPurposeType(const QByteArrayList &flags)
89{
90 if (Imap::flagsContain(Imap::FolderFlags::Trash, flags)) {
91 return ApplicationDomain::SpecialPurpose::Mail::trash;
92 }
93 if (Imap::flagsContain(Imap::FolderFlags::Drafts, flags)) {
94 return ApplicationDomain::SpecialPurpose::Mail::drafts;
95 }
96 if (Imap::flagsContain(Imap::FolderFlags::Sent, flags)) {
97 return ApplicationDomain::SpecialPurpose::Mail::sent;
98 }
99 return {};
100}
101
102static bool hasSpecialPurposeFlag(const QByteArrayList &flags)
103{
104 return !getSpecialPurposeType(flags).isEmpty();
105}
106
90 107
91class ImapSynchronizer : public Sink::Synchronizer { 108class ImapSynchronizer : public Sink::Synchronizer {
92 Q_OBJECT 109 Q_OBJECT
@@ -100,22 +117,29 @@ public:
100 QByteArray createFolder(const Imap::Folder &f) 117 QByteArray createFolder(const Imap::Folder &f)
101 { 118 {
102 const auto parentFolderRid = parentRid(f); 119 const auto parentFolderRid = parentRid(f);
103 SinkTraceCtx(mLogCtx) << "Creating folder: " << f.name() << parentFolderRid; 120 bool isToplevel = parentFolderRid.isEmpty();
121
122 SinkTraceCtx(mLogCtx) << "Creating folder: " << f.name() << parentFolderRid << f.flags;
104 123
105 const auto remoteId = folderRid(f); 124 const auto remoteId = folderRid(f);
106 Sink::ApplicationDomain::Folder folder; 125 Sink::ApplicationDomain::Folder folder;
107 folder.setName(f.name()); 126 folder.setName(f.name());
108 folder.setIcon("folder"); 127 folder.setIcon("folder");
109 folder.setEnabled(f.subscribed); 128 folder.setEnabled(f.subscribed);
110 QHash<QByteArray, Query::Comparator> mergeCriteria; 129 auto specialPurpose = [&] {
111 if (SpecialPurpose::isSpecialPurposeFolderName(f.name()) && parentFolderRid.isEmpty()) { 130 if (hasSpecialPurposeFlag(f.flags)) {
112 auto type = SpecialPurpose::getSpecialPurposeType(f.name()); 131 return getSpecialPurposeType(f.flags);
113 folder.setSpecialPurpose(QByteArrayList() << type); 132 } else if (SpecialPurpose::isSpecialPurposeFolderName(f.name()) && isToplevel) {
114 mergeCriteria.insert(ApplicationDomain::Folder::SpecialPurpose::name, Query::Comparator(type, Query::Comparator::Contains)); 133 return SpecialPurpose::getSpecialPurposeType(f.name());
134 }
135 return QByteArray{};
136 }();
137 if (!specialPurpose.isEmpty()) {
138 folder.setSpecialPurpose(QByteArrayList() << specialPurpose);
115 } 139 }
116 140
117 if (!parentFolderRid.isEmpty()) { 141 if (!isToplevel) {
118 folder.setParent(syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, parentFolderRid)); 142 folder.setParent(syncStore().resolveRemoteId(ApplicationDomain::Folder::name, parentFolderRid));
119 } 143 }
120 createOrModify(ApplicationDomain::getTypeName<ApplicationDomain::Folder>(), remoteId, folder); 144 createOrModify(ApplicationDomain::getTypeName<ApplicationDomain::Folder>(), remoteId, folder);
121 return remoteId; 145 return remoteId;
@@ -160,22 +184,20 @@ public:
160 return flags; 184 return flags;
161 } 185 }
162 186
163 void synchronizeMails(const QByteArray &folderRid, const Message &message) 187 void synchronizeMails(const QByteArray &folderRid, const QByteArray &folderLocalId, const Message &message)
164 { 188 {
165 auto time = QSharedPointer<QTime>::create(); 189 auto time = QSharedPointer<QTime>::create();
166 time->start(); 190 time->start();
167 SinkTraceCtx(mLogCtx) << "Importing new mail." << folderRid; 191 SinkTraceCtx(mLogCtx) << "Importing new mail." << folderRid;
168 192
169 const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRid);
170
171 const auto remoteId = assembleMailRid(folderLocalId, message.uid); 193 const auto remoteId = assembleMailRid(folderLocalId, message.uid);
172 194
173 Q_ASSERT(message.msg); 195 Q_ASSERT(message.msg);
174 SinkTraceCtx(mLogCtx) << "Found a mail " << remoteId << message.msg->subject(true)->asUnicodeString() << message.flags; 196 SinkTraceCtx(mLogCtx) << "Found a mail " << remoteId << message.flags;
175 197
176 auto mail = Sink::ApplicationDomain::Mail::create(mResourceInstanceIdentifier); 198 auto mail = Sink::ApplicationDomain::Mail::create(mResourceInstanceIdentifier);
177 mail.setFolder(folderLocalId); 199 mail.setFolder(folderLocalId);
178 mail.setMimeMessage(message.msg->encodedContent()); 200 mail.setMimeMessage(message.msg->encodedContent(true));
179 mail.setExtractedFullPayloadAvailable(message.fullPayload); 201 mail.setExtractedFullPayloadAvailable(message.fullPayload);
180 setFlags(mail, message.flags); 202 setFlags(mail, message.flags);
181 203
@@ -291,14 +313,15 @@ public:
291 SinkTraceCtx(mLogCtx) << "Uids to fetch: " << filteredAndSorted; 313 SinkTraceCtx(mLogCtx) << "Uids to fetch: " << filteredAndSorted;
292 314
293 bool headersOnly = false; 315 bool headersOnly = false;
316 const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
294 return imap->fetchMessages(folder, filteredAndSorted, headersOnly, [=](const Message &m) { 317 return imap->fetchMessages(folder, filteredAndSorted, headersOnly, [=](const Message &m) {
295 if (*maxUid < m.uid) { 318 if (*maxUid < m.uid) {
296 *maxUid = m.uid; 319 *maxUid = m.uid;
297 } 320 }
298 synchronizeMails(folderRemoteId, m); 321 synchronizeMails(folderRemoteId, folderLocalId, m);
299 }, 322 },
300 [this, maxUid, folder](int progress, int total) { 323 [=](int progress, int total) {
301 SinkLog() << "Progress: " << progress << " out of " << total; 324 reportProgress(progress, total, QByteArrayList{} << folderLocalId);
302 //commit every 10 messages 325 //commit every 10 messages
303 if ((progress % 10) == 0) { 326 if ((progress % 10) == 0) {
304 commit(); 327 commit();
@@ -335,11 +358,12 @@ public:
335 SinkLogCtx(mLogCtx) << "Fetching headers for: " << toFetch; 358 SinkLogCtx(mLogCtx) << "Fetching headers for: " << toFetch;
336 359
337 bool headersOnly = true; 360 bool headersOnly = true;
361 const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
338 return imap->fetchMessages(folder, toFetch, headersOnly, [=](const Message &m) { 362 return imap->fetchMessages(folder, toFetch, headersOnly, [=](const Message &m) {
339 synchronizeMails(folderRemoteId, m); 363 synchronizeMails(folderRemoteId, folderLocalId, m);
340 }, 364 },
341 [=](int progress, int total) { 365 [=](int progress, int total) {
342 SinkLogCtx(mLogCtx) << "Progress: " << progress << " out of " << total; 366 reportProgress(progress, total, QByteArrayList{} << folderLocalId);
343 //commit every 100 messages 367 //commit every 100 messages
344 if ((progress % 100) == 0) { 368 if ((progress % 100) == 0) {
345 commit(); 369 commit();
@@ -466,7 +490,7 @@ public:
466 //Otherwise fetch full payload for daterange 490 //Otherwise fetch full payload for daterange
467 auto folderList = QSharedPointer<QVector<Folder>>::create(); 491 auto folderList = QSharedPointer<QVector<Folder>>::create();
468 return imap->fetchFolders([folderList](const Folder &folder) { 492 return imap->fetchFolders([folderList](const Folder &folder) {
469 if (!folder.noselect) { 493 if (!folder.noselect && folder.subscribed) {
470 *folderList << folder; 494 *folderList << folder;
471 } 495 }
472 }) 496 })
@@ -480,12 +504,16 @@ public:
480 KAsync::Error getError(const KAsync::Error &error) 504 KAsync::Error getError(const KAsync::Error &error)
481 { 505 {
482 if (error) { 506 if (error) {
483 if (error.errorCode == Imap::CouldNotConnectError) { 507 switch(error.errorCode) {
484 return {ApplicationDomain::ConnectionError, error.errorMessage}; 508 case Imap::CouldNotConnectError:
485 } else if (error.errorCode == Imap::SslHandshakeError) { 509 return {ApplicationDomain::ConnectionError, error.errorMessage};
486 return {ApplicationDomain::LoginError, error.errorMessage}; 510 case Imap::SslHandshakeError:
511 return {ApplicationDomain::LoginError, error.errorMessage};
512 case Imap::HostNotFoundError:
513 return {ApplicationDomain::NoServerError, error.errorMessage};
514 default:
515 return {ApplicationDomain::UnknownError, error.errorMessage};
487 } 516 }
488 return {ApplicationDomain::UnknownError, error.errorMessage};
489 } 517 }
490 return {}; 518 return {};
491 } 519 }
@@ -539,11 +567,12 @@ public:
539 } 567 }
540 SinkLog() << "Fetching messages: " << toFetch << folderRemoteId; 568 SinkLog() << "Fetching messages: " << toFetch << folderRemoteId;
541 bool headersOnly = false; 569 bool headersOnly = false;
570 const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
542 return imap->fetchMessages(Folder{folderRemoteId}, toFetch, headersOnly, [=](const Message &m) { 571 return imap->fetchMessages(Folder{folderRemoteId}, toFetch, headersOnly, [=](const Message &m) {
543 synchronizeMails(folderRemoteId, m); 572 synchronizeMails(folderRemoteId, folderLocalId, m);
544 }, 573 },
545 [=](int progress, int total) { 574 [=](int progress, int total) {
546 reportProgress(progress, total); 575 reportProgress(progress, total, QByteArrayList{} << folderLocalId);
547 //commit every 100 messages 576 //commit every 100 messages
548 if ((progress % 100) == 0) { 577 if ((progress % 100) == 0) {
549 commit(); 578 commit();
@@ -554,10 +583,7 @@ public:
554 bool syncHeaders = query.hasFilter<ApplicationDomain::Mail::Folder>(); 583 bool syncHeaders = query.hasFilter<ApplicationDomain::Mail::Folder>();
555 //FIXME If we were able to to flush in between we could just query the local store for the folder list. 584 //FIXME If we were able to to flush in between we could just query the local store for the folder list.
556 return getFolderList(imap, query) 585 return getFolderList(imap, query)
557 .then([=] (const QVector<Folder> &folders) { 586 .serialEach([=](const Folder &folder) {
558 //Synchronize folders
559 return KAsync::value(folders)
560 .serialEach<void>([=](const Folder &folder) {
561 SinkLog() << "Syncing folder " << folder.path(); 587 SinkLog() << "Syncing folder " << folder.path();
562 //Emit notification that the folder is being synced. 588 //Emit notification that the folder is being synced.
563 //The synchronizer can't do that because it has no concept of the folder filter on a mail sync scope meaning that the folder is being synchronized. 589 //The synchronizer can't do that because it has no concept of the folder filter on a mail sync scope meaning that the folder is being synchronized.
@@ -572,7 +598,6 @@ public:
572 SinkWarning() << "Failed to sync folder: " << folder.path() << "Error: " << error.errorMessage; 598 SinkWarning() << "Failed to sync folder: " << folder.path() << "Error: " << error.errorMessage;
573 }); 599 });
574 }); 600 });
575 });
576 } 601 }
577 }) 602 })
578 .then([=] (const KAsync::Error &error) { 603 .then([=] (const KAsync::Error &error) {
@@ -582,6 +607,15 @@ public:
582 } 607 }
583 return KAsync::error<void>("Nothing to do"); 608 return KAsync::error<void>("Nothing to do");
584 } 609 }
610 static QByteArray ensureCRLF(const QByteArray &data) {
611 auto index = data.indexOf('\n');
612 if (index > 0 && data.at(index - 1) == '\r') { //First line is LF-only terminated
613 //Convert back and forth in case there's a mix. We don't want to expand CRLF into CRCRLF.
614 return KMime::LFtoCRLF(KMime::CRLFtoLF(data));
615 } else {
616 return data;
617 }
618 }
585 619
586 KAsync::Job<QByteArray> replay(const ApplicationDomain::Mail &mail, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE 620 KAsync::Job<QByteArray> replay(const ApplicationDomain::Mail &mail, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE
587 { 621 {
@@ -589,10 +623,10 @@ public:
589 auto login = imap->login(mUser, mPassword); 623 auto login = imap->login(mUser, mPassword);
590 KAsync::Job<QByteArray> job = KAsync::null<QByteArray>(); 624 KAsync::Job<QByteArray> job = KAsync::null<QByteArray>();
591 if (operation == Sink::Operation_Creation) { 625 if (operation == Sink::Operation_Creation) {
592 QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder()); 626 const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder());
593 QByteArray content = KMime::LFtoCRLF(mail.getMimeMessage()); 627 const auto content = ensureCRLF(mail.getMimeMessage());
594 auto flags = getFlags(mail); 628 const auto flags = getFlags(mail);
595 QDateTime internalDate = mail.getDate(); 629 const QDateTime internalDate = mail.getDate();
596 job = login.then(imap->append(mailbox, content, flags, internalDate)) 630 job = login.then(imap->append(mailbox, content, flags, internalDate))
597 .addToContext(imap) 631 .addToContext(imap)
598 .then([mail](qint64 uid) { 632 .then([mail](qint64 uid) {
@@ -623,11 +657,11 @@ public:
623 const bool messageMoved = changedProperties.contains(ApplicationDomain::Mail::Folder::name); 657 const bool messageMoved = changedProperties.contains(ApplicationDomain::Mail::Folder::name);
624 const bool messageChanged = changedProperties.contains(ApplicationDomain::Mail::MimeMessage::name); 658 const bool messageChanged = changedProperties.contains(ApplicationDomain::Mail::MimeMessage::name);
625 if (messageChanged || messageMoved) { 659 if (messageChanged || messageMoved) {
626 SinkTrace() << "Replacing message.";
627 const auto folderId = folderIdFromMailRid(oldRemoteId); 660 const auto folderId = folderIdFromMailRid(oldRemoteId);
628 const QString oldMailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folderId); 661 const QString oldMailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folderId);
629 QByteArray content = KMime::LFtoCRLF(mail.getMimeMessage()); 662 const auto content = ensureCRLF(mail.getMimeMessage());
630 QDateTime internalDate = mail.getDate(); 663 const QDateTime internalDate = mail.getDate();
664 SinkTrace() << "Replacing message. Old mailbox: " << oldMailbox << "New mailbox: " << mailbox << "Flags: " << flags << "Content: " << content;
631 KIMAP2::ImapSet set; 665 KIMAP2::ImapSet set;
632 set.add(uid); 666 set.add(uid);
633 job = login.then(imap->append(mailbox, content, flags, internalDate)) 667 job = login.then(imap->append(mailbox, content, flags, internalDate))
@@ -655,7 +689,7 @@ public:
655 if (error) { 689 if (error) {
656 SinkWarning() << "Error during changereplay: " << error.errorMessage; 690 SinkWarning() << "Error during changereplay: " << error.errorMessage;
657 return imap->logout() 691 return imap->logout()
658 .then(KAsync::error<QByteArray>(error)); 692 .then(KAsync::error<QByteArray>(getError(error)));
659 } 693 }
660 return imap->logout() 694 return imap->logout()
661 .then(KAsync::value(remoteId)); 695 .then(KAsync::value(remoteId));
@@ -664,6 +698,12 @@ public:
664 698
665 KAsync::Job<QByteArray> replay(const ApplicationDomain::Folder &folder, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE 699 KAsync::Job<QByteArray> replay(const ApplicationDomain::Folder &folder, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE
666 { 700 {
701 if (operation != Sink::Operation_Creation) {
702 if(oldRemoteId.isEmpty()) {
703 Q_ASSERT(false);
704 return KAsync::error<QByteArray>("Tried to replay modification without old remoteId.");
705 }
706 }
667 auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, &mSessionCache); 707 auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, &mSessionCache);
668 auto login = imap->login(mUser, mPassword); 708 auto login = imap->login(mUser, mPassword);
669 if (operation == Sink::Operation_Creation) { 709 if (operation == Sink::Operation_Creation) {
@@ -793,6 +833,10 @@ protected:
793 .then(imap->select(folderRemoteId)) 833 .then(imap->select(folderRemoteId))
794 .then([](Imap::SelectResult){}) 834 .then([](Imap::SelectResult){})
795 .then(imap->fetch(set, scope, [imap, messageByUid](const Imap::Message &message) { 835 .then(imap->fetch(set, scope, [imap, messageByUid](const Imap::Message &message) {
836 //We avoid parsing normally, so we have to do it explicitly here
837 if (message.msg) {
838 message.msg->parse();
839 }
796 messageByUid->insert(message.uid, message); 840 messageByUid->insert(message.uid, message);
797 })); 841 }));
798 842
diff --git a/examples/imapresource/imapserverproxy.cpp b/examples/imapresource/imapserverproxy.cpp
index 0cc43b8..538105c 100644
--- a/examples/imapresource/imapserverproxy.cpp
+++ b/examples/imapresource/imapserverproxy.cpp
@@ -37,8 +37,6 @@
37#include "log.h" 37#include "log.h"
38#include "test.h" 38#include "test.h"
39 39
40SINK_DEBUG_AREA("imapserverproxy")
41
42using namespace Imap; 40using namespace Imap;
43 41
44const char* Imap::Flags::Seen = "\\Seen"; 42const char* Imap::Flags::Seen = "\\Seen";
@@ -57,6 +55,7 @@ const char* Imap::FolderFlags::Trash = "\\Trash";
57const char* Imap::FolderFlags::Archive = "\\Archive"; 55const char* Imap::FolderFlags::Archive = "\\Archive";
58const char* Imap::FolderFlags::Junk = "\\Junk"; 56const char* Imap::FolderFlags::Junk = "\\Junk";
59const char* Imap::FolderFlags::Flagged = "\\Flagged"; 57const char* Imap::FolderFlags::Flagged = "\\Flagged";
58const char* Imap::FolderFlags::Drafts = "\\Drafts";
60 59
61const char* Imap::Capabilities::Namespace = "NAMESPACE"; 60const char* Imap::Capabilities::Namespace = "NAMESPACE";
62const char* Imap::Capabilities::Uidplus = "UIDPLUS"; 61const char* Imap::Capabilities::Uidplus = "UIDPLUS";
@@ -98,17 +97,25 @@ static KAsync::Job<void> runJob(KJob *job)
98 }); 97 });
99} 98}
100 99
101ImapServerProxy::ImapServerProxy(const QString &serverUrl, int port, SessionCache *sessionCache) : mSession(new KIMAP2::Session(serverUrl, qint16(port))), mSessionCache(sessionCache) 100KIMAP2::Session *createNewSession(const QString &serverUrl, int port)
102{ 101{
103 QObject::connect(mSession, &KIMAP2::Session::sslErrors, [this](const QList<QSslError> &errors) { 102 auto newSession = new KIMAP2::Session(serverUrl, qint16(port));
103 if (Sink::Test::testModeEnabled()) {
104 newSession->setTimeout(1);
105 } else {
106 newSession->setTimeout(40);
107 }
108 QObject::connect(newSession, &KIMAP2::Session::sslErrors, [=](const QList<QSslError> &errors) {
104 SinkLog() << "Received ssl error: " << errors; 109 SinkLog() << "Received ssl error: " << errors;
105 mSession->ignoreErrors(errors); 110 newSession->ignoreErrors(errors);
106 }); 111 });
112 return newSession;
113}
107 114
108 if (Sink::Test::testModeEnabled()) { 115ImapServerProxy::ImapServerProxy(const QString &serverUrl, int port, SessionCache *sessionCache) : mSessionCache(sessionCache), mSession(nullptr)
109 mSession->setTimeout(1); 116{
110 } else { 117 if (!mSessionCache || mSessionCache->isEmpty()) {
111 mSession->setTimeout(40); 118 mSession = createNewSession(serverUrl, port);
112 } 119 }
113} 120}
114 121
@@ -161,12 +168,16 @@ KAsync::Job<void> ImapServerProxy::login(const QString &username, const QString
161 // SinkTrace() << "Found user namespaces: " << mNamespaces.user; 168 // SinkTrace() << "Found user namespaces: " << mNamespaces.user;
162 }).then([=] (const KAsync::Error &error) { 169 }).then([=] (const KAsync::Error &error) {
163 if (error) { 170 if (error) {
164 if (error.errorCode == KIMAP2::LoginJob::ErrorCode::ERR_COULD_NOT_CONNECT) { 171 switch (error.errorCode) {
172 case KIMAP2::LoginJob::ErrorCode::ERR_HOST_NOT_FOUND:
173 return KAsync::error(HostNotFoundError, "Host not found: " + error.errorMessage);
174 case KIMAP2::LoginJob::ErrorCode::ERR_COULD_NOT_CONNECT:
165 return KAsync::error(CouldNotConnectError, "Failed to connect: " + error.errorMessage); 175 return KAsync::error(CouldNotConnectError, "Failed to connect: " + error.errorMessage);
166 } else if (error.errorCode == KIMAP2::LoginJob::ErrorCode::ERR_SSL_HANDSHAKE_FAILED) { 176 case KIMAP2::LoginJob::ErrorCode::ERR_SSL_HANDSHAKE_FAILED:
167 return KAsync::error(SslHandshakeError, "Ssl handshake failed: " + error.errorMessage); 177 return KAsync::error(SslHandshakeError, "Ssl handshake failed: " + error.errorMessage);
178 default:
179 return KAsync::error(error);
168 } 180 }
169 return KAsync::error(error);
170 } 181 }
171 return KAsync::null(); 182 return KAsync::null();
172 }); 183 });
@@ -188,6 +199,12 @@ KAsync::Job<void> ImapServerProxy::logout()
188 } 199 }
189} 200}
190 201
202bool ImapServerProxy::isGmail() const
203{
204 //Magic capability that only gmail has
205 return mCapabilities.contains("X-GM-EXT-1");
206}
207
191KAsync::Job<SelectResult> ImapServerProxy::select(const QString &mailbox) 208KAsync::Job<SelectResult> ImapServerProxy::select(const QString &mailbox)
192{ 209{
193 auto select = new KIMAP2::SelectJob(mSession); 210 auto select = new KIMAP2::SelectJob(mSession);
@@ -297,6 +314,7 @@ KAsync::Job<void> ImapServerProxy::fetch(const KIMAP2::ImapSet &set, KIMAP2::Fet
297 fetch->setSequenceSet(set); 314 fetch->setSequenceSet(set);
298 fetch->setUidBased(true); 315 fetch->setUidBased(true);
299 fetch->setScope(scope); 316 fetch->setScope(scope);
317 fetch->setAvoidParsing(true);
300 QObject::connect(fetch, &KIMAP2::FetchJob::resultReceived, callback); 318 QObject::connect(fetch, &KIMAP2::FetchJob::resultReceived, callback);
301 return runJob(fetch); 319 return runJob(fetch);
302} 320}
@@ -437,18 +455,60 @@ QString ImapServerProxy::getNamespace(const QString &name)
437 return ns.name; 455 return ns.name;
438} 456}
439 457
458static bool caseInsensitiveContains(const QByteArray &f, const QByteArrayList &list) {
459 return list.contains(f) || list.contains(f.toLower());
460}
461
462bool Imap::flagsContain(const QByteArray &f, const QByteArrayList &flags)
463{
464 return caseInsensitiveContains(f, flags);
465}
466
467static void reportFolder(const Folder &f, QSharedPointer<QSet<QString>> reportedList, std::function<void(const Folder &)> callback) {
468 if (!reportedList->contains(f.path())) {
469 reportedList->insert(f.path());
470 auto c = f;
471 c.noselect = true;
472 callback(c);
473 if (!f.parentPath().isEmpty()){
474 reportFolder(f.parentFolder(), reportedList, callback);
475 }
476 }
477}
478
440KAsync::Job<void> ImapServerProxy::fetchFolders(std::function<void(const Folder &)> callback) 479KAsync::Job<void> ImapServerProxy::fetchFolders(std::function<void(const Folder &)> callback)
441{ 480{
442 SinkTrace() << "Fetching folders"; 481 SinkTrace() << "Fetching folders";
443 auto subscribedList = QSharedPointer<QSet<QString>>::create() ; 482 auto subscribedList = QSharedPointer<QSet<QString>>::create() ;
483 auto reportedList = QSharedPointer<QSet<QString>>::create() ;
444 return list(KIMAP2::ListJob::NoOption, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &){ 484 return list(KIMAP2::ListJob::NoOption, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &){
445 *subscribedList << mailbox.name; 485 *subscribedList << mailbox.name;
446 }).then(list(KIMAP2::ListJob::IncludeUnsubscribed, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &flags) { 486 }).then(list(KIMAP2::ListJob::IncludeUnsubscribed, [=](const KIMAP2::MailBoxDescriptor &mailbox, const QList<QByteArray> &flags) {
447 bool noselect = flags.contains(QByteArray(FolderFlags::Noselect).toLower()) || flags.contains(QByteArray(FolderFlags::Noselect)); 487 bool noselect = caseInsensitiveContains(FolderFlags::Noselect, flags);
448 bool subscribed = subscribedList->contains(mailbox.name); 488 bool subscribed = subscribedList->contains(mailbox.name);
489 if (isGmail()) {
490 bool inbox = mailbox.name.toLower() == "inbox";
491 bool sent = caseInsensitiveContains(FolderFlags::Sent, flags);
492 bool drafts = caseInsensitiveContains(FolderFlags::Drafts, flags);
493 bool trash = caseInsensitiveContains(FolderFlags::Trash, flags);
494 /**
495 * Because gmail duplicates messages all over the place we only support a few selected folders for now that should be mostly exclusive.
496 */
497 if (!(inbox || sent || drafts || trash)) {
498 return;
499 }
500 }
449 SinkLog() << "Found mailbox: " << mailbox.name << flags << FolderFlags::Noselect << noselect << " sub: " << subscribed; 501 SinkLog() << "Found mailbox: " << mailbox.name << flags << FolderFlags::Noselect << noselect << " sub: " << subscribed;
450 auto ns = getNamespace(mailbox.name); 502 auto ns = getNamespace(mailbox.name);
451 callback(Folder{mailbox.name, ns, mailbox.separator, noselect, subscribed, flags}); 503 auto folder = Folder{mailbox.name, ns, mailbox.separator, noselect, subscribed, flags};
504
505 //call callback for parents if that didn't already happen.
506 //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.
507 if (!folder.parentPath().isEmpty() && !reportedList->contains(folder.parentPath())) {
508 reportFolder(folder.parentFolder(), reportedList, callback);
509 }
510 reportedList->insert(folder.path());
511 callback(folder);
452 })); 512 }));
453} 513}
454 514
diff --git a/examples/imapresource/imapserverproxy.h b/examples/imapresource/imapserverproxy.h
index 872f032..82f4f58 100644
--- a/examples/imapresource/imapserverproxy.h
+++ b/examples/imapresource/imapserverproxy.h
@@ -31,6 +31,7 @@ namespace Imap {
31 31
32enum ErrorCode { 32enum ErrorCode {
33 NoError, 33 NoError,
34 HostNotFoundError,
34 CouldNotConnectError, 35 CouldNotConnectError,
35 SslHandshakeError 36 SslHandshakeError
36}; 37};
@@ -60,6 +61,7 @@ namespace FolderFlags
60 extern const char* Junk; 61 extern const char* Junk;
61 extern const char* Flagged; 62 extern const char* Flagged;
62 extern const char* All; 63 extern const char* All;
64 extern const char* Drafts;
63} 65}
64 66
65namespace Capabilities 67namespace Capabilities
@@ -78,6 +80,8 @@ struct Message {
78 bool fullPayload; 80 bool fullPayload;
79}; 81};
80 82
83bool flagsContain(const QByteArray &f, const QByteArrayList &flags);
84
81struct Folder { 85struct Folder {
82 Folder() = default; 86 Folder() = default;
83 Folder(const QString &path, const QString &ns, const QChar &separator, bool noselect_, bool subscribed_, const QByteArrayList &flags_) 87 Folder(const QString &path, const QString &ns, const QChar &separator, bool noselect_, bool subscribed_, const QByteArrayList &flags_)
@@ -114,6 +118,15 @@ struct Folder {
114 return parentPath; 118 return parentPath;
115 } 119 }
116 120
121 Folder parentFolder() const
122 {
123 Folder parent;
124 parent.mPath = parentPath();
125 parent.mNamespace = mNamespace;
126 parent.mSeparator = mSeparator;
127 return parent;
128 }
129
117 QString name() const 130 QString name() const
118 { 131 {
119 auto pathParts = mPath.split(mSeparator); 132 auto pathParts = mPath.split(mSeparator);
@@ -218,18 +231,18 @@ public:
218 return session; 231 return session;
219 } 232 }
220 } 233 }
221 return CachedSession{}; 234 return {};
235 }
236
237 bool isEmpty() const
238 {
239 return mSessions.isEmpty();
222 } 240 }
223private: 241private:
224 QList<CachedSession> mSessions; 242 QList<CachedSession> mSessions;
225}; 243};
226 244
227class ImapServerProxy { 245class ImapServerProxy {
228 KIMAP2::Session *mSession;
229 QStringList mCapabilities;
230 Namespaces mNamespaces;
231
232
233public: 246public:
234 ImapServerProxy(const QString &serverUrl, int port, SessionCache *sessionCache = nullptr); 247 ImapServerProxy(const QString &serverUrl, int port, SessionCache *sessionCache = nullptr);
235 248
@@ -279,9 +292,14 @@ public:
279 KAsync::Job<QVector<qint64>> fetchUids(const Folder &folder); 292 KAsync::Job<QVector<qint64>> fetchUids(const Folder &folder);
280 293
281private: 294private:
295 bool isGmail() const;
296
282 QString getNamespace(const QString &name); 297 QString getNamespace(const QString &name);
283 QObject mGuard; 298 QObject mGuard;
284 SessionCache *mSessionCache; 299 SessionCache *mSessionCache;
300 KIMAP2::Session *mSession;
301 QStringList mCapabilities;
302 Namespaces mNamespaces;
285}; 303};
286 304
287} 305}
diff --git a/examples/imapresource/tests/imapmailsyncbenchmark.cpp b/examples/imapresource/tests/imapmailsyncbenchmark.cpp
index a53c148..814e325 100644
--- a/examples/imapresource/tests/imapmailsyncbenchmark.cpp
+++ b/examples/imapresource/tests/imapmailsyncbenchmark.cpp
@@ -31,8 +31,6 @@
31using namespace Sink; 31using namespace Sink;
32using namespace Sink::ApplicationDomain; 32using namespace Sink::ApplicationDomain;
33 33
34SINK_DEBUG_AREA("ImapMailSyncBenchmark")
35
36/** 34/**
37 * Test of complete system using the imap resource. 35 * Test of complete system using the imap resource.
38 * 36 *
diff --git a/examples/imapresource/tests/imapserverproxytest.cpp b/examples/imapresource/tests/imapserverproxytest.cpp
index 476066d..271b3d9 100644
--- a/examples/imapresource/tests/imapserverproxytest.cpp
+++ b/examples/imapresource/tests/imapserverproxytest.cpp
@@ -12,8 +12,6 @@
12 12
13using namespace Imap; 13using namespace Imap;
14 14
15// SINK_DEBUG_AREA("imapserverproxytest")
16
17/** 15/**
18 */ 16 */
19class ImapServerProxyTest : public QObject 17class ImapServerProxyTest : public QObject
diff --git a/examples/imapresource/tests/populatemailbox.sh b/examples/imapresource/tests/populatemailbox.sh
index a435df7..800e2e7 100644
--- a/examples/imapresource/tests/populatemailbox.sh
+++ b/examples/imapresource/tests/populatemailbox.sh
@@ -1,12 +1,23 @@
1#!/bin/bash 1#!/bin/bash
2 2
3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
4#Delete all mailboxes
4sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost 5sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost
6#Create mailboxes
5sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost 7sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost
6sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost 8sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost
7sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost 9sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost
10
11#Set acls so we can create in INBOX
8sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 12sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
9 13
14#Subscribe to mailboxes
15sudo echo "sub INBOX" | cyradm --auth PLAIN -u doe -w doe localhost
16sudo echo "sub INBOX.test" | cyradm --auth PLAIN -u doe -w doe localhost
17sudo echo "sub INBOX.Drafts" | cyradm --auth PLAIN -u doe -w doe localhost
18sudo echo "sub INBOX.Trash" | cyradm --auth PLAIN -u doe -w doe localhost
19
20#Create a bunch of test messages in the test folder
10# for i in `seq 1 5000`; 21# for i in `seq 1 5000`;
11# do 22# do
12# # sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/$i. 23# # sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/$i.
diff --git a/examples/imapresource/tests/resetmailbox.sh b/examples/imapresource/tests/resetmailbox.sh
index 6ed198e..63d3478 100644
--- a/examples/imapresource/tests/resetmailbox.sh
+++ b/examples/imapresource/tests/resetmailbox.sh
@@ -3,8 +3,11 @@
3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
4sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost 4sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost
5sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost 5sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost
6sudo echo "subscribe INBOX.test" | cyradm --auth PLAIN -u doe -w doe localhost
6sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost 7sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost
8sudo echo "subscribe INBOX.Drafts" | cyradm --auth PLAIN -u doe -w doe localhost
7sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost 9sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost
10sudo echo "subscribe INBOX.Trash" | cyradm --auth PLAIN -u doe -w doe localhost
8sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 11sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
9sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1. 12sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1.
10sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1. 13sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1.