diff options
Diffstat (limited to 'examples/imapresource')
-rw-r--r-- | examples/imapresource/CMakeLists.txt | 2 | ||||
-rw-r--r-- | examples/imapresource/imapresource.cpp | 126 | ||||
-rw-r--r-- | examples/imapresource/imapserverproxy.cpp | 88 | ||||
-rw-r--r-- | examples/imapresource/imapserverproxy.h | 30 | ||||
-rw-r--r-- | examples/imapresource/tests/imapmailsyncbenchmark.cpp | 2 | ||||
-rw-r--r-- | examples/imapresource/tests/imapserverproxytest.cpp | 2 | ||||
-rw-r--r-- | examples/imapresource/tests/populatemailbox.sh | 11 | ||||
-rw-r--r-- | examples/imapresource/tests/resetmailbox.sh | 3 |
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) | |||
4 | include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) | 4 | include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) |
5 | 5 | ||
6 | find_package(KF5 COMPONENTS REQUIRED Mime) | 6 | find_package(KF5 COMPONENTS REQUIRED Mime) |
7 | find_package(KIMAP2 0.0.1 REQUIRED) | 7 | find_package(KIMAP2 0.2 REQUIRED) |
8 | 8 | ||
9 | include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) | 9 | include_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 | ||
49 | SINK_DEBUG_AREA("imapresource") | ||
50 | |||
51 | Q_DECLARE_METATYPE(QSharedPointer<Imap::ImapServerProxy>) | 49 | Q_DECLARE_METATYPE(QSharedPointer<Imap::ImapServerProxy>) |
52 | 50 | ||
53 | using namespace Imap; | 51 | using 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 | ||
88 | static 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 | |||
102 | static bool hasSpecialPurposeFlag(const QByteArrayList &flags) | ||
103 | { | ||
104 | return !getSpecialPurposeType(flags).isEmpty(); | ||
105 | } | ||
106 | |||
90 | 107 | ||
91 | class ImapSynchronizer : public Sink::Synchronizer { | 108 | class 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 | ||
40 | SINK_DEBUG_AREA("imapserverproxy") | ||
41 | |||
42 | using namespace Imap; | 40 | using namespace Imap; |
43 | 41 | ||
44 | const char* Imap::Flags::Seen = "\\Seen"; | 42 | const char* Imap::Flags::Seen = "\\Seen"; |
@@ -57,6 +55,7 @@ const char* Imap::FolderFlags::Trash = "\\Trash"; | |||
57 | const char* Imap::FolderFlags::Archive = "\\Archive"; | 55 | const char* Imap::FolderFlags::Archive = "\\Archive"; |
58 | const char* Imap::FolderFlags::Junk = "\\Junk"; | 56 | const char* Imap::FolderFlags::Junk = "\\Junk"; |
59 | const char* Imap::FolderFlags::Flagged = "\\Flagged"; | 57 | const char* Imap::FolderFlags::Flagged = "\\Flagged"; |
58 | const char* Imap::FolderFlags::Drafts = "\\Drafts"; | ||
60 | 59 | ||
61 | const char* Imap::Capabilities::Namespace = "NAMESPACE"; | 60 | const char* Imap::Capabilities::Namespace = "NAMESPACE"; |
62 | const char* Imap::Capabilities::Uidplus = "UIDPLUS"; | 61 | const char* Imap::Capabilities::Uidplus = "UIDPLUS"; |
@@ -98,17 +97,25 @@ static KAsync::Job<void> runJob(KJob *job) | |||
98 | }); | 97 | }); |
99 | } | 98 | } |
100 | 99 | ||
101 | ImapServerProxy::ImapServerProxy(const QString &serverUrl, int port, SessionCache *sessionCache) : mSession(new KIMAP2::Session(serverUrl, qint16(port))), mSessionCache(sessionCache) | 100 | KIMAP2::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()) { | 115 | ImapServerProxy::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 | ||
202 | bool ImapServerProxy::isGmail() const | ||
203 | { | ||
204 | //Magic capability that only gmail has | ||
205 | return mCapabilities.contains("X-GM-EXT-1"); | ||
206 | } | ||
207 | |||
191 | KAsync::Job<SelectResult> ImapServerProxy::select(const QString &mailbox) | 208 | KAsync::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 | ||
458 | static bool caseInsensitiveContains(const QByteArray &f, const QByteArrayList &list) { | ||
459 | return list.contains(f) || list.contains(f.toLower()); | ||
460 | } | ||
461 | |||
462 | bool Imap::flagsContain(const QByteArray &f, const QByteArrayList &flags) | ||
463 | { | ||
464 | return caseInsensitiveContains(f, flags); | ||
465 | } | ||
466 | |||
467 | static 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 | |||
440 | KAsync::Job<void> ImapServerProxy::fetchFolders(std::function<void(const Folder &)> callback) | 479 | KAsync::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 | ||
32 | enum ErrorCode { | 32 | enum 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 | ||
65 | namespace Capabilities | 67 | namespace Capabilities |
@@ -78,6 +80,8 @@ struct Message { | |||
78 | bool fullPayload; | 80 | bool fullPayload; |
79 | }; | 81 | }; |
80 | 82 | ||
83 | bool flagsContain(const QByteArray &f, const QByteArrayList &flags); | ||
84 | |||
81 | struct Folder { | 85 | struct 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 | } |
223 | private: | 241 | private: |
224 | QList<CachedSession> mSessions; | 242 | QList<CachedSession> mSessions; |
225 | }; | 243 | }; |
226 | 244 | ||
227 | class ImapServerProxy { | 245 | class ImapServerProxy { |
228 | KIMAP2::Session *mSession; | ||
229 | QStringList mCapabilities; | ||
230 | Namespaces mNamespaces; | ||
231 | |||
232 | |||
233 | public: | 246 | public: |
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 | ||
281 | private: | 294 | private: |
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 @@ | |||
31 | using namespace Sink; | 31 | using namespace Sink; |
32 | using namespace Sink::ApplicationDomain; | 32 | using namespace Sink::ApplicationDomain; |
33 | 33 | ||
34 | SINK_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 | ||
13 | using namespace Imap; | 13 | using namespace Imap; |
14 | 14 | ||
15 | // SINK_DEBUG_AREA("imapserverproxytest") | ||
16 | |||
17 | /** | 15 | /** |
18 | */ | 16 | */ |
19 | class ImapServerProxyTest : public QObject | 17 | class 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 | ||
3 | sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost | 3 | sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost |
4 | #Delete all mailboxes | ||
4 | sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost | 5 | sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost |
6 | #Create mailboxes | ||
5 | sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost | 7 | sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost |
6 | sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost | 8 | sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost |
7 | sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost | 9 | sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost |
10 | |||
11 | #Set acls so we can create in INBOX | ||
8 | sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost | 12 | sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost |
9 | 13 | ||
14 | #Subscribe to mailboxes | ||
15 | sudo echo "sub INBOX" | cyradm --auth PLAIN -u doe -w doe localhost | ||
16 | sudo echo "sub INBOX.test" | cyradm --auth PLAIN -u doe -w doe localhost | ||
17 | sudo echo "sub INBOX.Drafts" | cyradm --auth PLAIN -u doe -w doe localhost | ||
18 | sudo 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 @@ | |||
3 | sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost | 3 | sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost |
4 | sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost | 4 | sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost |
5 | sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost | 5 | sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost |
6 | sudo echo "subscribe INBOX.test" | cyradm --auth PLAIN -u doe -w doe localhost | ||
6 | sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost | 7 | sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost |
8 | sudo echo "subscribe INBOX.Drafts" | cyradm --auth PLAIN -u doe -w doe localhost | ||
7 | sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost | 9 | sudo echo "cm user.doe.Trash" | cyradm --auth PLAIN -u cyrus -w admin localhost |
10 | sudo echo "subscribe INBOX.Trash" | cyradm --auth PLAIN -u doe -w doe localhost | ||
8 | sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost | 11 | sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost |
9 | sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1. | 12 | sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1. |
10 | sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1. | 13 | sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1. |