Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--appinfo/info.xml4
-rw-r--r--lib/Address.php18
-rw-r--r--lib/AddressList.php8
-rw-r--r--lib/AppInfo/BootstrapSingleton.php2
-rw-r--r--lib/BackgroundJob/SyncJob.php78
-rw-r--r--lib/Command/SyncAccount.php73
-rw-r--r--lib/Contracts/IMailManager.php11
-rw-r--r--lib/Controller/FoldersController.php35
-rwxr-xr-xlib/Controller/MessagesController.php11
-rw-r--r--lib/Db/MailAccountMapper.php11
-rw-r--r--lib/Db/Mailbox.php25
-rw-r--r--lib/Db/MailboxMapper.php97
-rw-r--r--lib/Db/Message.php186
-rw-r--r--lib/Db/MessageMapper.php378
-rw-r--r--lib/Exception/ConcurrentSyncException.php (renamed from lib/IMAP/Search/ISearchStrategy.php)12
-rw-r--r--lib/Exception/MailboxNotCachedException.php7
-rw-r--r--lib/Exception/UidValidityChangedException.php30
-rw-r--r--lib/Folder.php14
-rw-r--r--lib/IMAP/FolderMapper.php7
-rw-r--r--lib/IMAP/MailboxSync.php2
-rw-r--r--lib/IMAP/MessageMapper.php29
-rw-r--r--lib/IMAP/Search/FullScanSearchStrategy.php86
-rw-r--r--lib/IMAP/Search/ImapSortSearchStrategy.php109
-rw-r--r--lib/IMAP/Search/Provider.php82
-rw-r--r--lib/IMAP/Sync/ISyncStrategy.php4
-rw-r--r--lib/IMAP/Sync/Response.php48
-rw-r--r--lib/IMAP/Sync/SimpleMailboxSync.php4
-rw-r--r--lib/IMAP/Sync/Synchronizer.php28
-rw-r--r--lib/Migration/FixAccountSyncs.php64
-rw-r--r--lib/Migration/Version1020Date20191002091034.php33
-rw-r--r--lib/Migration/Version1020Date20191002091035.php157
-rw-r--r--lib/Model/IMAPMessage.php43
-rw-r--r--lib/Service/AccountService.php34
-rw-r--r--lib/Service/AutoCompletion/AddressCollector.php2
-rw-r--r--lib/Service/MailManager.php31
-rw-r--r--lib/Service/MailSearch.php139
-rw-r--r--lib/Service/Search/FilterStringParser.php (renamed from lib/IMAP/Search/SearchFilterStringParser.php)30
-rw-r--r--lib/Service/Search/MailSearch.php130
-rw-r--r--lib/Service/Search/SearchQuery.php143
-rw-r--r--lib/Service/SyncService.php393
-rw-r--r--lib/Support/PerformanceLogger.php (renamed from lib/IMAP/Search/SearchStrategyFactory.php)45
-rw-r--r--lib/Support/PerformanceLoggerTask.php72
-rw-r--r--src/components/FolderContent.vue6
-rw-r--r--src/components/Loading.vue35
-rw-r--r--src/service/MessageService.js3
-rw-r--r--src/store/actions.js7
-rw-r--r--src/store/mutations.js3
-rw-r--r--tests/Integration/Db/MailboxMapperTest.php20
-rw-r--r--tests/Integration/FolderSynchronizationTest.php80
-rw-r--r--tests/Integration/Framework/ImapTestAccount.php10
-rw-r--r--tests/Unit/Controller/FoldersControllerTest.php25
-rw-r--r--tests/Unit/Controller/MessagesControllerTest.php6
-rw-r--r--tests/Unit/IMAP/Search/SearchFilterStringParserTest.php60
-rw-r--r--tests/Unit/IMAP/Search/SearchStrategyFactoryTest.php152
-rw-r--r--tests/Unit/IMAP/Sync/ResponseTest.php4
-rw-r--r--tests/Unit/IMAP/Sync/SimpleMailboxSyncTest.php2
-rw-r--r--tests/Unit/IMAP/Sync/SynchronizerTest.php20
-rw-r--r--tests/Unit/Service/AccountServiceTest.php14
-rw-r--r--tests/Unit/Service/MailManagerTest.php23
-rw-r--r--tests/Unit/Service/MailSearchTest.php43
60 files changed, 2398 insertions, 830 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 4f66b13dd..9979486c7 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -12,7 +12,7 @@
- **🙈 We’re not reinventing the wheel!** Based on the great [Horde](http://horde.org) libraries.
- **📬 Want to host your own mail server?** We don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)!
]]></description>
- <version>1.1.2</version>
+ <version>1.2.0</version>
<licence>agpl</licence>
<author>Christoph Wurst</author>
<author>Jan-Christoph Borchardt</author>
@@ -34,6 +34,7 @@
<repair-steps>
<post-migration>
<step>OCA\Mail\Migration\FixCollectedAddresses</step>
+ <step>OCA\Mail\Migration\FixAccountSyncs</step>
<step>OCA\Mail\Migration\MakeItineraryExtractorExecutable</step>
<step>OCA\Mail\Migration\MigrateProvisioningConfig</step>
<step>OCA\Mail\Migration\ProvisionAccounts</step>
@@ -43,6 +44,7 @@
<command>OCA\Mail\Command\CreateAccount</command>
<command>OCA\Mail\Command\DiagnoseAccount</command>
<command>OCA\Mail\Command\ExportAccount</command>
+ <command>OCA\Mail\Command\SyncAccount</command>
</commands>
<settings>
<admin>OCA\Mail\Settings\AdminSettings</admin>
diff --git a/lib/Address.php b/lib/Address.php
index 4eff928b3..c099893fd 100644
--- a/lib/Address.php
+++ b/lib/Address.php
@@ -1,4 +1,5 @@
<?php
+
declare(strict_types=1);
/**
@@ -30,6 +31,11 @@ use JsonSerializable;
class Address implements JsonSerializable {
+ public const TYPE_FROM = 0;
+ public const TYPE_TO = 1;
+ public const TYPE_CC = 2;
+ public const TYPE_BCC = 3;
+
/** @var Horde_Mail_Rfc822_Address */
private $wrapped;
@@ -40,17 +46,17 @@ class Address implements JsonSerializable {
public function __construct($label, $email) {
$this->wrapped = new Horde_Mail_Rfc822_Address($email);
// If no label is set we use the email
- if ($label !== $email && !is_null($label)) {
+ if ($label !== $email && $label !== null) {
$this->wrapped->personal = $label;
}
}
/**
- * @return string
+ * @return string|null
*/
- public function getLabel(): string {
+ public function getLabel(): ?string {
$personal = $this->wrapped->personal;
- if (is_null($personal)) {
+ if ($personal === null) {
// Fallback
return $this->getEmail();
}
@@ -58,9 +64,9 @@ class Address implements JsonSerializable {
}
/**
- * @return string
+ * @return string|null
*/
- public function getEmail(): string {
+ public function getEmail(): ?string {
return $this->wrapped->bare_address;
}
diff --git a/lib/AddressList.php b/lib/AddressList.php
index 5b2c184ca..619ffba37 100644
--- a/lib/AddressList.php
+++ b/lib/AddressList.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
@@ -69,6 +69,12 @@ class AddressList implements Countable, JsonSerializable {
return new AddressList($addresses);
}
+ public static function fromRow(array $recipient): self {
+ return new self([
+ new Address($recipient['label'], $recipient['email'])
+ ]);
+ }
+
/**
* Get first element
*
diff --git a/lib/AppInfo/BootstrapSingleton.php b/lib/AppInfo/BootstrapSingleton.php
index 3c6af088d..f0451845b 100644
--- a/lib/AppInfo/BootstrapSingleton.php
+++ b/lib/AppInfo/BootstrapSingleton.php
@@ -48,7 +48,7 @@ use OCA\Mail\Service\Group\IGroupService;
use OCA\Mail\Service\Group\NextcloudGroupService;
use OCA\Mail\Service\Group\ContactsGroupService;
use OCA\Mail\Service\MailManager;
-use OCA\Mail\Service\MailSearch;
+use OCA\Mail\Service\Search\MailSearch;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\UserPreferenceSevice;
use OCP\AppFramework\IAppContainer;
diff --git a/lib/BackgroundJob/SyncJob.php b/lib/BackgroundJob/SyncJob.php
new file mode 100644
index 000000000..6dfab3d2e
--- /dev/null
+++ b/lib/BackgroundJob/SyncJob.php
@@ -0,0 +1,78 @@
+<?php declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Mail\BackgroundJob;
+
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\SyncService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\TimedJob;
+use OCP\ILogger;
+
+class SyncJob extends TimedJob {
+
+ /** @var AccountService */
+ private $accountService;
+ /** @var SyncService */
+ private $syncService;
+ /** @var ILogger */
+ private $logger;
+ /** @var IJobList */
+ private $jobList;
+
+ public function __construct(ITimeFactory $time,
+ AccountService $accountService,
+ SyncService $syncService,
+ ILogger $logger,
+ IJobList $jobList) {
+ parent::__construct($time);
+
+ $this->accountService = $accountService;
+ $this->syncService = $syncService;
+ $this->logger = $logger;
+
+ $this->setInterval(3600);
+ $this->jobList = $jobList;
+ }
+
+ protected function run($argument) {
+ $accountId = (int)$argument['accountId'];
+
+ try {
+ $account = $this->accountService->findById($accountId);
+ } catch (DoesNotExistException $e) {
+ $this->logger->debug('Could not find account <' . $accountId . '> removing from jobs');
+ $this->jobList->remove(self::class, $argument);
+ return;
+ }
+
+ try {
+ $this->syncService->syncAccount($account);
+ } catch (\Exception $e) {
+ $this->logger->logException($e);
+ }
+ }
+
+}
diff --git a/lib/Command/SyncAccount.php b/lib/Command/SyncAccount.php
new file mode 100644
index 000000000..459d7e5d7
--- /dev/null
+++ b/lib/Command/SyncAccount.php
@@ -0,0 +1,73 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Command;
+
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\SyncService;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SyncAccount extends Command {
+
+ const ARGUMENT_ACCOUNT_ID = 'account-id';
+ const OPTION_FORCE = 'force';
+
+ /** @var AccountService */
+ private $accountService;
+
+ /** @var MailboxMapper */
+ private $mailboxMapper;
+
+ /** @var SyncService */
+ private $syncService;
+
+ public function __construct(AccountService $service,
+ MailboxMapper $mailboxMapper,
+ SyncService $syncService) {
+ parent::__construct();
+
+ $this->accountService = $service;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->syncService = $syncService;
+ }
+
+ protected function configure() {
+ $this->setName('mail:account:sync');
+ $this->setDescription('Synchronize an IMAP account');
+ $this->addArgument(self::ARGUMENT_ACCOUNT_ID, InputArgument::REQUIRED);
+ $this->addOption(self::OPTION_FORCE, 'f', InputOption::VALUE_NONE);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $accountId = (int)$input->getArgument(self::ARGUMENT_ACCOUNT_ID);
+ $force = $input->getOption(self::OPTION_FORCE);
+
+ $account = $this->accountService->findById($accountId);
+ $this->syncService->syncAccount($account, $force);
+ }
+}
diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php
index e82077d7a..9575c6cfc 100644
--- a/lib/Contracts/IMailManager.php
+++ b/lib/Contracts/IMailManager.php
@@ -69,17 +69,6 @@ interface IMailManager {
public function getMessage(Account $account, string $mailbox, int $id, bool $loadBody = false): IMAPMessage;
/**
- * @param Account
- * @param SyncRequest $syncRequest
- *
- * @return SyncResponse
- *
- * @throws ClientException
- * @throws ServiceException
- */
- public function syncMessages(Account $account, SyncRequest $syncRequest): SyncResponse;
-
- /**
* @param Account $sourceAccount
* @param string $sourceFolderId
* @param int $messageId
diff --git a/lib/Controller/FoldersController.php b/lib/Controller/FoldersController.php
index 5316bfb1c..ca1b10f93 100644
--- a/lib/Controller/FoldersController.php
+++ b/lib/Controller/FoldersController.php
@@ -25,6 +25,10 @@ declare(strict_types=1);
namespace OCA\Mail\Controller;
+use Horde_Imap_Client;
+use OCA\Mail\Exception\MailboxNotCachedException;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Service\SyncService;
use function base64_decode;
use function is_array;
use OCA\Mail\Contracts\IMailManager;
@@ -47,6 +51,9 @@ class FoldersController extends Controller {
/** @var IMailManager */
private $mailManager;
+ /** @var SyncService */
+ private $syncService;
+
/**
* @param string $appName
* @param IRequest $request
@@ -54,13 +61,18 @@ class FoldersController extends Controller {
* @param string $UserId
* @param IMailManager $mailManager
*/
- public function __construct(string $appName, IRequest $request,
- AccountService $accountService, $UserId, IMailManager $mailManager) {
+ public function __construct(string $appName,
+ IRequest $request,
+ AccountService $accountService,
+ $UserId,
+ IMailManager $mailManager,
+ SyncService $syncService) {
parent::__construct($appName, $request);
$this->accountService = $accountService;
$this->currentUserId = $UserId;
$this->mailManager = $mailManager;
+ $this->syncService = $syncService;
}
/**
@@ -91,15 +103,28 @@ class FoldersController extends Controller {
* @param string $syncToken
* @param int[] $uids
* @return JSONResponse
+ * @throws ServiceException
*/
- public function sync(int $accountId, string $folderId, string $syncToken, array $uids = []): JSONResponse {
+ public function sync(int $accountId, string $folderId, array $uids): JSONResponse {
$account = $this->accountService->find($this->currentUserId, $accountId);
- if (empty($accountId) || empty($folderId) || empty($syncToken) || !is_array($uids)) {
+ if (empty($accountId) || empty($folderId) || !is_array($uids)) {
return new JSONResponse(null, Http::STATUS_BAD_REQUEST);
}
- $syncResponse = $this->mailManager->syncMessages($account, new SyncRequest(base64_decode($folderId), $syncToken, $uids));
+ try {
+ $syncResponse = $this->syncService->syncMailbox(
+ $account,
+ base64_decode($folderId),
+ Horde_Imap_Client::SYNC_NEWMSGSUIDS | Horde_Imap_Client::SYNC_FLAGSUIDS | Horde_Imap_Client::SYNC_VANISHEDUIDS,
+ array_map(function($uid) {
+ return (int) $uid;
+ }, $uids),
+ true
+ );
+ } catch (MailboxNotCachedException $e) {
+ return new JSONResponse(null, Http::STATUS_PRECONDITION_REQUIRED);
+ }
return new JSONResponse($syncResponse);
}
diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php
index 2764bbb3c..7e075621e 100755
--- a/lib/Controller/MessagesController.php
+++ b/lib/Controller/MessagesController.php
@@ -40,6 +40,7 @@ use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\IMailBox;
use OCA\Mail\Service\ItineraryService;
+use OCA\Mail\Service\SyncService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
@@ -69,6 +70,9 @@ class MessagesController extends Controller {
/** @var ItineraryService */
private $itineraryService;
+ /** @var SyncService */
+ private $syncService;
+
/** @var string */
private $currentUserId;
@@ -104,6 +108,7 @@ class MessagesController extends Controller {
IMailManager $mailManager,
IMailSearch $mailSearch,
ItineraryService $itineraryService,
+ SyncService $syncService,
string $UserId,
$userFolder,
ILogger $logger,
@@ -116,6 +121,7 @@ class MessagesController extends Controller {
$this->mailManager = $mailManager;
$this->mailSearch = $mailSearch;
$this->itineraryService = $itineraryService;
+ $this->syncService = $syncService;
$this->currentUserId = $UserId;
$this->userFolder = $userFolder;
$this->logger = $logger;
@@ -144,6 +150,11 @@ class MessagesController extends Controller {
return new JSONResponse(null, Http::STATUS_FORBIDDEN);
}
+ $this->syncService->ensurePopulated(
+ $account,
+ base64_decode($folderId)
+ );
+
$this->logger->debug("loading messages of folder <$folderId>");
return new JSONResponse(
diff --git a/lib/Db/MailAccountMapper.php b/lib/Db/MailAccountMapper.php
index 3881912cc..a483b5e78 100644
--- a/lib/Db/MailAccountMapper.php
+++ b/lib/Db/MailAccountMapper.php
@@ -60,7 +60,7 @@ class MailAccountMapper extends QBMapper {
}
/**
- * @param int $id
+ * Finds an mail account by id
*
* @return MailAccount
* @throws DoesNotExistException
@@ -134,4 +134,13 @@ class MailAccountMapper extends QBMapper {
$delete->execute();
}
+ public function getAllAccounts(): array {
+ $qb = $this->db->getQueryBuilder();
+ $query = $qb
+ ->select('*')
+ ->from($this->getTableName());
+
+ return $this->findEntities($query);
+ }
+
}
diff --git a/lib/Db/Mailbox.php b/lib/Db/Mailbox.php
index d5727bf7f..c09b3981a 100644
--- a/lib/Db/Mailbox.php
+++ b/lib/Db/Mailbox.php
@@ -37,8 +37,18 @@ use function strtolower;
* @method void setName(string $name)
* @method int getAccountId()
* @method void setAccountId(int $accountId)
- * @method string|null getSyncToken()
- * @method void setSyncToken(string|null $syncToken)
+ * @method string|null getSyncNewToken()
+ * @method void setSyncNewToken(string|null $syncNewToken)
+ * @method string|null getSyncChangedToken()
+ * @method void setSyncChangedToken(string|null $syncNewToken)
+ * @method string|null getSyncVanishedToken()
+ * @method void setSyncVanishedToken(string|null $syncNewToken)
+ * @method int|null getSyncNewLock()
+ * @method void setSyncNewLock(int|null $ts)
+ * @method int|null getSyncChangedLock()
+ * @method void setSyncChangedLock(int|null $ts)
+ * @method int|null getSyncVanishedLock()
+ * @method void setSyncVanishedLock(int|null $ts)
* @method string getAttributes()
* @method void setAttributes(string $attributes)
* @method string getDelimiter()
@@ -56,7 +66,12 @@ class Mailbox extends Entity {
protected $name;
protected $accountId;
- protected $syncToken;
+ protected $syncNewToken;
+ protected $syncChangedToken;
+ protected $syncVanishedToken;
+ protected $syncNewLock;
+ protected $syncChangedLock;
+ protected $syncVanishedLock;
protected $attributes;
protected $delimiter;
protected $messages;
@@ -68,6 +83,9 @@ class Mailbox extends Entity {
$this->addType('accountId', 'integer');
$this->addType('messages', 'integer');
$this->addType('unseen', 'integer');
+ $this->addType('syncNewLock', 'integer');
+ $this->addType('syncChangedLock', 'integer');
+ $this->addType('syncVanishedLock', 'integer');
$this->addType('selectable', 'boolean');
}
@@ -78,7 +96,6 @@ class Mailbox extends Entity {
json_decode($this->getAttributes() ?? '[]', true) ?? [],
$this->delimiter
);
- $folder->setSyncToken($this->getSyncToken());
foreach ($this->getSpecialUseParsed() as $use) {
$folder->addSpecialUse($use);
}
diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php
index 4cf8e1baf..3acbde53b 100644
--- a/lib/Db/MailboxMapper.php
+++ b/lib/Db/MailboxMapper.php
@@ -24,16 +24,25 @@
namespace OCA\Mail\Db;
use OCA\Mail\Account;
+use OCA\Mail\Exception\ConcurrentSyncException;
use OCA\Mail\Exception\ServiceException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
+use OCP\Security\ISecureRandom;
class MailboxMapper extends QBMapper {
- public function __construct(IDBConnection $db) {
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ public function __construct(IDBConnection $db,
+ ITimeFactory $timeFactory) {
parent::__construct($db, 'mail_mailboxes');
+ $this->timeFactory = $timeFactory;
}
/**
@@ -99,4 +108,90 @@ class MailboxMapper extends QBMapper {
throw new DoesNotExistException("Special mailbox $specialUse does not exist");
}
+ /**
+ * @throws ConcurrentSyncException
+ */
+ private function lockForSync(Mailbox $mailbox, string $attr, ?int $lock): int {
+ $now = $this->timeFactory->getTime();
+
+ if ($lock !== null
+ && $lock > ($now - 5 * 60)) {
+ // Another process is syncing
+ throw new ConcurrentSyncException($mailbox->getId() . ' is already being synced');
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->update($this->getTableName())
+ ->set($attr, $query->createNamedParameter($now, IQueryBuilder::PARAM_INT))
+ ->where(
+ $query->expr()->eq('id', $query->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)),
+ $this->eqOrNull($query, $attr, $lock, IQueryBuilder::PARAM_INT)
+ );
+ if ($query->execute() === 0) {
+ // Another process just started syncing
+
+ throw new ConcurrentSyncException();
+ }
+
+ return $now;
+ }
+
+ /**
+ * @throws ConcurrentSyncException
+ */
+ public function lockForNewSync(Mailbox $mailbox): void {
+ $mailbox->setSyncNewLock(
+ $this->lockForSync($mailbox, 'sync_new_lock', $mailbox->getSyncNewLock())
+ );
+ }
+
+ /**
+ * @throws ConcurrentSyncException
+ */
+ public function lockForChangeSync(Mailbox $mailbox): void {
+ $mailbox->setSyncChangedLock(
+ $this->lockForSync($mailbox, 'sync_changed_lock', $mailbox->getSyncChangedLock())
+ );
+ }
+
+ /**
+ * @throws ConcurrentSyncException
+ */
+ public function lockForVanishedSync(Mailbox $mailbox): void {
+ $mailbox->setSyncVanishedLock(
+ $this->lockForSync($mailbox, 'sync_vanished_lock', $mailbox->getSyncVanishedLock())
+ );
+ }
+
+ /**
+ * @param Mailbox $mailbox
+ * @param IQueryBuilder $query
+ *
+ * @return string
+ */
+ private function eqOrNull(IQueryBuilder $query, string $column, $value, int $type): string {
+ if ($value === null) {
+ return $query->expr()->isNull($column);
+ }
+ return $query->expr()->eq($column, $query->createNamedParameter($value, $type));
+ }
+
+ public function unlockFromNewSync(Mailbox $mailbox): void {
+ $mailbox->setSyncNewLock(null);
+
+ $this->update($mailbox);
+ }
+
+ public function unlockFromChangedSync(Mailbox $mailbox): void {
+ $mailbox->setSyncChangedLock(null);
+
+ $this->update($mailbox);
+ }
+
+ public function unlockFromVanishedSync(Mailbox $mailbox): void {
+ $mailbox->setSyncVanishedLock(null);
+
+ $this->update($mailbox);
+ }
+
}
diff --git a/lib/Db/Message.php b/lib/Db/Message.php
new file mode 100644
index 000000000..9ed4391bd
--- /dev/null
+++ b/lib/Db/Message.php
@@ -0,0 +1,186 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Db;
+
+use JsonSerializable;
+use OCA\Mail\AddressList;
+use OCP\AppFramework\Db\Entity;
+use function json_encode;
+
+/**
+ * @method void setUid(int $uid)
+ * @method int getUid()
+ * @method void setMessageId(string $id)
+ * @method string getMessageId()
+ * @method void setMailboxId(int $mailbox)
+ * @method int getMailboxId()
+ * @method void setSubject(string $subject)
+ * @method string getSubject()
+ * @method void setSentAt(int $time)
+ * @method int getSentAt()
+ * @method void setFlagAnswered(bool $answered)
+ * @method bool getFlagAnswered()
+ * @method void setFlagDeleted(bool $deleted)
+ * @method bool getFlagDeleted()
+ * @method void setFlagDraft(bool $answered)
+ * @method bool getFlagDraft()
+ * @method void setFlagFlagged(bool $flagged)
+ * @method bool getFlagFlagged()
+ * @method void setFlagSeen(bool $seen)
+ * @method bool getFlagSeen()
+ * @method void setFlagForwarded(bool $forwarded)
+ * @method bool getFlagForwarded()
+ * @method void setFlagJunk(bool $junk)
+ * @method bool getFlagJunk()
+ * @method void setFlagNotjunk(bool $notjunk)
+ * @method bool getFlagNotjunk()
+ * @method void setUpdatedAt(int $time)
+ * @method int getUpdatedAt()
+ */
+class Message extends Entity implements JsonSerializable {
+
+ protected $uid;
+ protected $messageId;
+ protected $mailboxId;
+ protected $subject;
+ protected $sentAt;
+ protected $flagAnswered;
+ protected $flagDeleted;
+ protected $flagDraft;
+ protected $flagFlagged;
+ protected $flagSeen;
+ protected $flagForwarded;
+ protected $flagJunk;
+ protected $flagNotjunk;
+ protected $updatedAt;
+
+ /** @var AddressList */
+ private $from;
+
+ /** @var AddressList */
+ private $to;
+
+ /** @var AddressList */
+ private $cc;
+
+ /** @var AddressList */
+ private $bcc;
+
+ public function __construct() {
+ $this->from = new AddressList([]);
+ $this->to = new AddressList([]);
+ $this->cc = new AddressList([]);
+ $this->bcc = new AddressList([]);
+
+ $this->addType('uid', 'integer');
+ $this->addType('sentAt', 'integer');
+ $this->addType('flagAnswered', 'bool');
+ $this->addType('flagDeleted', 'bool');
+ $this->addType('flagDraft', 'bool');
+ $this->addType('flagFlagged', 'bool');
+ $this->addType('flagSeen', 'bool');
+ $this->addType('flagForwarded', 'bool');
+ $this->addType('flagJunk', 'bool');
+ $this->addType('flagNotjunk', 'bool');
+ $this->addType('updatedAt', 'integer');
+ }
+
+ /**
+ * @return AddressList
+ */
+ public function getFrom(): AddressList {
+ return $this->from;
+ }
+
+ /**
+ * @param AddressList $from
+ */
+ public function setFrom(AddressList $from): void {
+ $this->from = $from;
+ }
+
+ /**
+ * @return AddressList
+ */
+ public function getTo(): AddressList {
+ return $this->to;
+ }
+
+ /**
+ * @param AddressList $to
+ */
+ public function setTo(AddressList $to): void {
+ $this->to = $to;
+ }
+
+ /**
+ * @return AddressList
+ */
+ public function getCc(): AddressList {
+ return $this->cc;
+ }
+
+ /**
+ * @param AddressList $cc
+ */
+ public function setCc(AddressList $cc): void {
+ $this->cc = $cc;
+ }
+
+ /**
+ * @return AddressList
+ */
+ public function getBcc(): AddressList {
+ return $this->bcc;
+ }
+
+ /**
+ * @param AddressList $bcc
+ */
+ public function setBcc(AddressList $bcc): void {
+ $this->bcc = $bcc;
+ }
+
+ public function jsonSerialize() {
+ return [
+ 'id' => $this->getUid(), // Change to UID on front-end
+ 'subject' => $this->getSubject(),
+ 'dateInt' => $this->getSentAt(),
+ 'flags' => [
+ 'unseen' => !$this->getFlagSeen(),
+ 'flagged' => $this->getFlagFlagged(),
+ 'answered' => $this->getFlagAnswered(),
+ 'deleted' => $this->getFlagDeleted(),
+ 'draft' => $this->getFlagDraft(),
+ 'forwarded' => $this->getFlagForwarded(),
+ 'hasAttachments' => false, // TODO
+ ],
+ 'from' => $this->getFrom()->jsonSerialize(),
+ 'to' => $this->getTo()->jsonSerialize(),
+ 'cc' => $this->getCc()->jsonSerialize(),
+ 'bcc' => $this->getBcc()->jsonSerialize(),
+ ];
+ }
+
+}
diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php
new file mode 100644
index 000000000..c3cb8b42b
--- /dev/null
+++ b/lib/Db/MessageMapper.php
@@ -0,0 +1,378 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Db;
+
+use Horde_Imap_Client;
+use OCA\Mail\Address;
+use OCA\Mail\AddressList;
+use OCA\Mail\Service\Search\SearchQuery;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use function array_combine;
+use function array_keys;
+use function array_map;
+
+class MessageMapper extends QBMapper {
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ public function __construct(IDBConnection $db,
+ ITimeFactory $timeFactory) {
+ parent::__construct($db, 'mail_messages');
+ $this->timeFactory = $timeFactory;
+ }
+
+ public function findAllUids(Mailbox $mailbox): array {
+ $query = $this->db->getQueryBuilder();
+
+ $query->select('uid')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('mailbox_id', $query->createNamedParameter($mailbox->getId())));
+
+ $result = $query->execute();
+ $uids = array_map(function (array $row) {
+ return (int) $row['uid'];
+ }, $result->fetchAll());
+ $result->closeCursor();
+
+ return $uids;
+ }
+
+ public function insertBulk(Message ...$messages): void {
+ $this->db->beginTransaction();
+
+ $qb1 = $this->db->getQueryBuilder();
+ $qb1->insert($this->getTableName());
+ $qb1->setValue('uid', $qb1->createParameter('uid'));
+ $qb1->setValue('message_id', $qb1->createParameter('message_id'));
+ $qb1->setValue('mailbox_id', $qb1->createParameter('mailbox_id'));
+ $qb1->setValue('subject', $qb1->createParameter('subject'));
+ $qb1->setValue('sent_at', $qb1->createParameter('sent_at'));
+ $qb1->setValue('flag_answered', $qb1->createParameter('flag_answered'));
+ $qb1->setValue('flag_deleted', $qb1->createParameter('flag_deleted'));
+ $qb1->setValue('flag_draft', $qb1->createParameter('flag_draft'));
+ $qb1->setValue('flag_flagged', $qb1->createParameter('flag_flagged'));
+ $qb1->setValue('flag_seen', $qb1->createParameter('flag_seen'));
+ $qb1->setValue('flag_forwarded', $qb1->createParameter('flag_forwarded'));
+ $qb1->setValue('flag_junk', $qb1->createParameter('flag_junk'));
+ $qb1->setValue('flag_notjunk', $qb1->createParameter('flag_notjunk'));
+ $qb2 = $this->db->getQueryBuilder();
+
+ $qb2->insert('mail_recipients')
+ ->setValue('message_id', $qb2->createParameter('message_id'))
+ ->setValue('type', $qb2->createParameter('type'))
+ ->setValue('label', $qb2->createParameter('label'))
+ ->setValue('email', $qb2->createParameter('email'));
+
+ foreach ($messages as $message) {
+ $qb1->setParameter('uid', $message->getUid(), IQueryBuilder::PARAM_INT);
+ $qb1->setParameter('message_id', $message->getMessageId(), IQueryBuilder::PARAM_STR);
+ $qb1->setParameter('mailbox_id', $message->getMailboxId(), IQueryBuilder::PARAM_INT);
+ $qb1->setParameter('subject', $message->getSubject(), IQueryBuilder::PARAM_STR);
+ $qb1->setParameter('sent_at', $message->getSentAt(), IQueryBuilder::PARAM_INT);
+ $qb1->setParameter('flag_answered', $message->getFlagAnswered(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_deleted', $message->getFlagDeleted(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_draft', $message->getFlagDraft(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_flagged', $message->getFlagFlagged(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_seen', $message->getFlagSeen(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_forwarded', $message->getFlagForwarded(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_junk', $message->getFlagJunk(), IQueryBuilder::PARAM_BOOL);
+ $qb1->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL);
+
+ $qb1->execute();
+
+ $messageId = $qb1->getLastInsertId();
+ $recipientTypes = [
+ Address::TYPE_FROM => $message->getFrom(),
+ Address::TYPE_TO => $message->getTo(),
+ Address::TYPE_CC => $message->getCc(),
+ Address::TYPE_BCC => $message->getBcc(),
+ ];
+ foreach ($recipientTypes as $type => $recipients) {
+ /** @var AddressList $recipients */
+ foreach ($recipients->iterate() as $recipient) {
+ /** @var Address $recipient */
+ if ($recipient->getEmail() === null) {
+ // If for some reason the e-mail is not set we should ignore this entry
+ continue;
+ }
+
+ $qb2->setParameter('message_id', $messageId, IQueryBuilder::PARAM_INT);
+ $qb2->setParameter('type', $type, IQueryBuilder::PARAM_INT);
+ $qb2->setParameter('label', $recipient->getLabel(), IQueryBuilder::PARAM_STR);
+ $qb2->setParameter('email', $recipient->getEmail(), IQueryBuilder::PARAM_STR);
+
+ $qb2->execute();
+ }
+ }
+ }
+
+ $this->db->commit();
+ }
+
+ public function updateBulk(Message ...$messages): void {
+ $this->db->beginTransaction();
+
+ $query = $this->db->getQueryBuilder();
+ $query->update($this->getTableName())
+ ->set('flag_answered', $query->createParameter('flag_answered'))
+ ->set('flag_deleted', $query->createParameter('flag_deleted'))
+ ->set('flag_draft', $query->createParameter('flag_draft'))
+ ->set('flag_flagged', $query->createParameter('flag_flagged'))
+ ->set('flag_seen', $query->createParameter('flag_seen'))
+ ->set('flag_forwarded', $query->createParameter('flag_forwarded'))
+ ->set('flag_junk', $query->createParameter('flag_junk'))
+ ->set('flag_notjunk', $query->createParameter('flag_notjunk'))
+ ->set('updated_at', $query->createNamedParameter($this->timeFactory->getTime()))
+ ->where($query->expr()->andX(
+ $query->expr()->eq('uid', $query->createParameter('uid')),
+ $query->expr()->eq('mailbox_id', $query->createParameter('mailbox_id'))
+ ));
+
+ foreach ($messages as $message) {
+ $query->setParameter('uid', $message->getUid(), IQueryBuilder::PARAM_INT);
+ $query->setParameter('mailbox_id', $message->getMailboxId(), IQueryBuilder::PARAM_INT);
+ $query->setParameter('flag_answered', $message->getFlagAnswered(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_deleted', $message->getFlagDeleted(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_draft', $message->getFlagDraft(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_flagged', $message->getFlagFlagged(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_seen', $message->getFlagSeen(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_forwarded', $message->getFlagForwarded(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_junk', $message->getFlagJunk(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL);
+
+ $query->execute();
+ }
+
+ $this->db->commit();
+ }
+
+ public function deleteAll(Mailbox $mailbox): void {
+ $query = $this->db->getQueryBuilder();
+
+ $query->delete($this->getTableName())
+ ->where($query->expr()->eq('mailbox_id', $query->createNamedParameter($mailbox->getId())));
+
+ $query->execute();
+ }
+
+ public function deleteByUid(Mailbox $mailbox, int ...$uids): void {
+ $query = $this->db->getQueryBuilder();
+
+ $query->delete($this->getTableName())
+ ->where(
+ $query->expr()->eq('mailbox_id', $query->createNamedParameter($mailbox->getId())),
+ $query->expr()->in('uid', $query->createNamedParameter($uids, IQueryBuilder::PARAM_INT_ARRAY))
+ );
+
+ $query->execute();
+ }
+
+ /**
+ * @param Mailbox $mailbox
+ * @param SearchQuery $query
+ *
+ * @return int[]
+ */
+ public function findUidsByQuery(Mailbox $mailbox, SearchQuery $query): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb
+ ->selectDistinct('m.uid')
+ ->from($this->getTableName(), 'm');
+
+ if (!empty($query->getFrom())) {
+ $select->innerJoin('m', 'mail_recipients', 'r0', 'm.id = r0.message_id');
+ }
+ if (!empty($query->getTo())) {
+ $select->innerJoin('m', 'mail_recipients', 'r1', 'm.id = r1.message_id');
+ }
+ if (!empty($query->getCc())) {
+ $select->innerJoin('m', 'mail_recipients', 'r2', 'm.id = r2.message_id');
+ }
+ if (!empty($query->getBcc())) {
+ $select->innerJoin('m', 'mail_recipients', 'r3', 'm.id = r3.message_id');
+ }
+
+ $select->where(
+ $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT)
+ );
+
+ if (!empty($query->getFrom())) {
+ $select->andWhere(
+ $qb->expr()->in('r0.email', $qb->createNamedParameter($query->getFrom(), IQueryBuilder::PARAM_STR_ARRAY))
+ );
+ }
+ if (!empty($query->getTo())) {
+ $select->andWhere(
+ $qb->expr()->in('r1.email', $qb->createNamedParameter($query->getTo(), IQueryBuilder::PARAM_STR_ARRAY))
+ );
+ }
+ if (!empty($query->getTo())) {
+ $select->andWhere(
+ $qb->expr()->in('r2.email', $qb->createNamedParameter($query->getCc(), IQueryBuilder::PARAM_STR_ARRAY))
+ );
+ }
+ if (!empty($query->getTo())) {
+ $select->andWhere(
+ $qb->expr()->in('r3.email', $qb->createNamedParameter($query->getBcc(), IQueryBuilder::PARAM_STR_ARRAY))
+ );
+ }
+
+ if ($query->getCursor() !== null) {
+ $select->andWhere(
+ $qb->expr()->lt('sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT))
+ );
+ }
+
+ $flags = $query->getFlags();
+ $flagKeys = array_keys($flags);
+ foreach ([
+ Horde_Imap_Client::FLAG_ANSWERED,
+ Horde_Imap_Client::FLAG_DELETED,
+ Horde_Imap_Client::FLAG_DRAFT,
+ Horde_Imap_Client::FLAG_FLAGGED,
+ Horde_Imap_Client::FLAG_RECENT,
+ Horde_Imap_Client::FLAG_SEEN,
+ Horde_Imap_Client::FLAG_FORWARDED,
+ Horde_Imap_Client::FLAG_JUNK,
+ Horde_Imap_Client::FLAG_NOTJUNK,
+ ] as $flag) {
+ if (in_array($flag, $flagKeys, true)) {
+ $key = ltrim($flag, '\\');
+ $select->andWhere($qb->expr()->eq("flag_$key", $qb->createNamedParameter($flags[$flag], IQueryBuilder::PARAM_BOOL)));
+ }
+ }
+
+ $select = $select
+ ->orderBy('sent_at', 'desc')
+ ->setMaxResults(20);
+
+ return array_map(function (Message $message) {
+ return $message->getUid();
+ }, $this->findEntities($select));
+ }
+
+ /**
+ * @param Mailbox $mailbox
+ * @param int[] $uids
+ *
+ * @return Message[]
+ */
+ public function findByUids(Mailbox $mailbox, array $uids): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb
+ ->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT),
+ $qb->expr()->in('uid', $qb->createNamedParameter($uids, IQueryBuilder::PARAM_INT_ARRAY))
+ )
+ ->orderBy('sent_at', 'desc');
+
+ return $this->findRecipients($this->findEntities($select));
+ }
+
+ /**
+ * @param Message[] $messages
+ * @return Message[]
+ */
+ private function findRecipients(array $messages): array {
+ /** @var Message[] $indexedMessages */
+ $indexedMessages = array_combine(
+ array_map(function (Message $msg) {
+ return $msg->getId();
+ }, $messages),
+ $messages
+ );
+ $qb2 = $this->db->getQueryBuilder();
+ $qb2->select('label', 'email', 'type', 'message_id')
+ ->from('mail_recipients')
+ ->where(
+ $qb2->expr()->in('message_id', $qb2->createNamedParameter(array_keys($indexedMessages), IQueryBuilder::PARAM_INT_ARRAY))
+ );
+ $recipientsResult = $qb2->execute();
+ foreach ($recipientsResult->fetchAll() as $recipient) {
+ $message = $indexedMessages[(int)$recipient['message_id']];
+ switch ($recipient['type']) {
+ case Address::TYPE_FROM:
+ $message->setFrom(
+ $message->getFrom()->merge(AddressList::fromRow($recipient))
+ );
+ break;
+ case Address::TYPE_TO:
+ $message->setTo(
+ $message->getTo()->merge(AddressList::fromRow($recipient))
+ );
+ break;
+ case Address::TYPE_CC:
+ $message->setCc(
+ $message->getCc()->merge(AddressList::fromRow($recipient))
+ );
+ break;
+ case Address::TYPE_BCC:
+ $message->setFrom(
+ $message->getFrom()->merge(AddressList::fromRow($recipient))
+ );
+ break;
+ }
+ }
+ $recipientsResult->closeCursor();
+
+ return $messages;
+ }
+
+ public function findNew(Mailbox $mailbox, int $highest): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb
+ ->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)),
+ $qb->expr()->gt('uid', $qb->createNamedParameter($highest, IQueryBuilder::PARAM_INT))
+ );
+
+ return $this->findRecipients($this->findEntities($select));
+ }
+
+ public function findChanged(Mailbox $mailbox, int $since): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb
+ ->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)),
+ $qb->expr()->gt('updated_at', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT))
+ );
+
+ return $this->findRecipients($this->findEntities($select));
+ }
+
+}
diff --git a/lib/IMAP/Search/ISearchStrategy.php b/lib/Exception/ConcurrentSyncException.php
index 264a0f33c..7cc91136d 100644
--- a/lib/IMAP/Search/ISearchStrategy.php
+++ b/lib/Exception/ConcurrentSyncException.php
@@ -21,16 +21,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-namespace OCA\Mail\IMAP\Search;
+namespace OCA\Mail\Exception;
-use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Ids;
+use Exception;
-interface ISearchStrategy {
-
- /**
- * @throws Horde_Imap_Client_Exception
- */
- public function getIds(int $maxResults, array $flags = []): Horde_Imap_Client_Ids;
+class ConcurrentSyncException extends Exception {
}
diff --git a/lib/Exception/MailboxNotCachedException.php b/lib/Exception/MailboxNotCachedException.php
new file mode 100644
index 000000000..37242b1ad
--- /dev/null
+++ b/lib/Exception/MailboxNotCachedException.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Mail\Exception;
+
+class MailboxNotCachedException extends ServiceException {
+
+}
diff --git a/lib/Exception/UidValidityChangedException.php b/lib/Exception/UidValidityChangedException.php
new file mode 100644
index 000000000..9000909c2
--- /dev/null
+++ b/lib/Exception/UidValidityChangedException.php
@@ -0,0 +1,30 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\mail\lib\Exception;
+
+use OCA\Mail\Exception\ServiceException;
+
+class UidValidityChangedException extends ServiceException {
+
+}
diff --git a/lib/Folder.php b/lib/Folder.php
index 486c21c4a..9048bd68d 100644
--- a/lib/Folder.php
+++ b/lib/Folder.php
@@ -48,8 +48,6 @@ class Folder implements JsonSerializable {
private $specialUse;
/** @var string */
- private $syncToken;
-
/**
* @param Account $account
* @param Horde_Imap_Client_Mailbox $mailbox
@@ -130,17 +128,6 @@ class Folder implements JsonSerializable {
}
/**
- * @param string $syncToken
- */
- public function setSyncToken($syncToken) {
- $this->syncToken = $syncToken;
- }
-
- public function getSyncToken(): ?string {
- return $this->syncToken;
- }
-
- /**
* @return array
*/
public function jsonSerialize() {
@@ -161,7 +148,6 @@ class Folder implements JsonSerializable {
'folders' => array_values($folders),
'specialUse' => $this->specialUse,
'specialRole' => empty($this->specialUse) ? null : $this->specialUse[0],
- 'syncToken' => $this->syncToken,
];
}
diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php
index 87ce28722..0a6bd8f7a 100644
--- a/lib/IMAP/FolderMapper.php
+++ b/lib/IMAP/FolderMapper.php
@@ -76,16 +76,9 @@ class FolderMapper {
$mailbox['delimiter']
);
- if ($folder->isSearchable()) {
- $folder->setSyncToken($client->getSyncToken($folder->getMailbox()));
- }
-
$folders[] = $folder;
if ($mailbox['mailbox']->utf8 === 'INBOX') {
$searchFolder = new SearchFolder($account->getId(), $mailbox['mailbox'], $mailbox['attributes'], $mailbox['delimiter']);
- if ($folder->isSearchable()) {
- $searchFolder->setSyncToken($client->getSyncToken($folder->getMailbox()));
- }
$folders[] = $searchFolder;
}
}
diff --git a/lib/IMAP/MailboxSync.php b/lib/IMAP/MailboxSync.php
index bbd19e796..c631b4156 100644
--- a/lib/IMAP/MailboxSync.php
+++ b/lib/IMAP/MailboxSync.php
@@ -115,7 +115,6 @@ class MailboxSync {
private function updateMailboxFromFolder(Folder $folder, Mailbox $mailbox): void {
$mailbox->setDelimiter($folder->getDelimiter());
- $mailbox->setSyncToken($folder->getSyncToken());
$mailbox->setAttributes(json_encode($folder->getAttributes()));
$mailbox->setDelimiter($folder->getDelimiter());
$mailbox->setMessages(0); // TODO
@@ -129,7 +128,6 @@ class MailboxSync {
$mailbox = new Mailbox();
$mailbox->setName($folder->getMailbox());
$mailbox->setAccountId($account->getId());
- $mailbox->setSyncToken($folder->getSyncToken());
$mailbox->setAttributes(json_encode($folder->getAttributes()));
$mailbox->setDelimiter($folder->getDelimiter());
$mailbox->setMessages(0); // TODO
diff --git a/lib/IMAP/MessageMapper.php b/lib/IMAP/MessageMapper.php
index f42f8bc35..d0366912a 100644
--- a/lib/IMAP/MessageMapper.php
+++ b/lib/IMAP/MessageMapper.php
@@ -67,6 +67,33 @@ class MessageMapper {
}
/**
+ * @param Horde_Imap_Client_Socket $client
+ * @param Mailbox $mailbox
+ *
+ * @return IMAPMessage[]
+ * @throws Horde_Imap_Client_Exception
+ */
+ public function findAll(Horde_Imap_Client_Socket $client, Mailbox $mailbox): array {
+ $query = new Horde_Imap_Client_Fetch_Query();
+ $query->uid();
+
+ return $this->findByIds(
+ $client,
+ $mailbox->getMailbox(),
+ array_map(
+ function(Horde_Imap_Client_Data_Fetch $data) {
+ return $data->getUid();
+ },
+ iterator_to_array($client->fetch(
+ $mailbox->getMailbox(),
+ $query,
+ []
+ ))
+ )
+ );
+ }
+
+ /**
* @return IMAPMessage[]
* @throws Horde_Imap_Client_Exception
*/
@@ -77,10 +104,8 @@ class MessageMapper {
$query = new Horde_Imap_Client_Fetch_Query();
$query->envelope();
$query->flags();
- $query->size();
$query->uid();
$query->imapDate();
- $query->structure();
$fetchResults = iterator_to_array($client->fetch($mailbox, $query, [
'ids' => new Horde_Imap_Client_Ids($ids),
diff --git a/lib/IMAP/Search/FullScanSearchStrategy.php b/lib/IMAP/Search/FullScanSearchStrategy.php
deleted file mode 100644
index 758dde1be..000000000
--- a/lib/IMAP/Search/FullScanSearchStrategy.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php declare(strict_types=1);
-
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-namespace OCA\Mail\IMAP\Search;
-
-use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Fetch_Query;
-use Horde_Imap_Client_Ids;
-use Horde_Imap_Client_Socket;
-use function array_keys;
-use function array_slice;
-use function uasort;
-
-class FullScanSearchStrategy implements ISearchStrategy {
-
- /** @var Horde_Imap_Client_Socket */
- private $client;
-
- /** @var string */
- private $mailbox;
-
- /** @var int|null */
- private $cursor;
-
- public function __construct(Horde_Imap_Client_Socket $client,
- string $mailbox,
- ?int $cursor) {
- $this->client = $client;
- $this->mailbox = $mailbox;
- $this->cursor = $cursor;
- }
-
- /**
- * Scan all messages of a mailbox and filter out matching ones
- *
- * This is slow, but some IMAP server don't support the SORT capability.
- *
- * @throws Horde_Imap_Client_Exception
- */
- public function getIds(int $maxResults, array $flags = []): Horde_Imap_Client_Ids {
- $query = new Horde_Imap_Client_Fetch_Query();
- $query->uid();
- $query->imapDate();
-
- $result = $this->client->fetch($this->mailbox, $query);
- $uidMap = [];
- foreach ($result as $r) {
- $ts = $r->getImapDate()->getTimeStamp();
- if ($this->cursor === null || $ts < $this->cursor) {
- $uidMap[$r->getUid()] = $ts;
- }
- }
- // sort by time
- uasort($uidMap, function ($a, $b) {
- return $a < $b;
- });
- return new Horde_Imap_Client_Ids(
- array_slice(
- array_keys($uidMap),
- 0,
- $maxResults
- )
- );
- }
-
-}
diff --git a/lib/IMAP/Search/ImapSortSearchStrategy.php b/lib/IMAP/Search/ImapSortSearchStrategy.php
deleted file mode 100644
index 7c61de35d..000000000
--- a/lib/IMAP/Search/ImapSortSearchStrategy.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php declare(strict_types=1);
-
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-namespace OCA\Mail\IMAP\Search;
-
-use DateTime;
-use Horde_Imap_Client;
-use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Ids;
-use Horde_Imap_Client_Search_Query;
-use Horde_Imap_Client_Socket;
-use OCA\Mail\IMAP\Search\SearchFilterStringParser;
-use function array_reverse;
-use function array_slice;
-
-class ImapSortSearchStrategy implements ISearchStrategy {
-
- /** @var Horde_Imap_Client_Socket */
- private $client;
-
- /** @var string */
- private $mailbox;
-
- /** @var Horde_Imap_Client_Search_Query */
- private $query;
-
- /** @var int|null */
- private $cursor;
-
- /** @var ISearchStrategy */
- private $fallback;
-
- public function __construct(Horde_Imap_Client_Socket $client,
- string $mailbox,
- Horde_Imap_Client_Search_Query $query,
- ?int $cursor,
- ISearchStrategy $fallback) {
- $this->client = $client;
- $this->mailbox = $mailbox;
- $this->query = $query;
- $this->cursor = $cursor;
- $this->fallback = $fallback;
- }
-
- /**
- * @param int $maxResults
- * @param array $flags
- *
- * @return Horde_Imap_Client_Ids
- * @throws Horde_Imap_Client_Exception
- */
- public function getIds(int $maxResults, array $flags = []): Horde_Imap_Client_Ids {
- $query = clone $this->query;
-
- if ($this->cursor !== null) {
- $query->dateTimeSearch(
- DateTime::createFromFormat("U", (string) $this->cursor),
- Horde_Imap_Client_Search_Query::DATE_BEFORE
- );
- }
-
- try {
- $result = $this->client->search(
- $this->mailbox,
- $query,
- [
- 'sort' => [
- Horde_Imap_Client::SORT_REVERSE,
- Horde_Imap_Client::SORT_DATE
- ],
- ]
- );
- } catch (Horde_Imap_Client_Exception $e) {
- // maybe the server's advertisement of SORT was a fake
- // see https://github.com/nextcloud/mail/issues/50
- // try again without SORT
- return $this->fallback->getIds($maxResults, $flags);
- }
-
- return new Horde_Imap_Client_Ids(
- array_slice(
- $result['match']->ids,
- 0,
- $maxResults
- )
- );
- }
-
-}
diff --git a/lib/IMAP/Search/Provider.php b/lib/IMAP/Search/Provider.php
new file mode 100644
index 000000000..4cccc9fa8
--- /dev/null
+++ b/lib/IMAP/Search/Provider.php
@@ -0,0 +1,82 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\IMAP\Search;
+
+use Horde_Imap_Client_Exception;
+use Horde_Imap_Client_Search_Query;
+use OCA\Mail\Account;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Service\Search\SearchQuery;
+use OCP\ILogger;
+
+class Provider {
+
+ /** @var IMAPClientFactory */
+ private $clientFactory;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(IMAPClientFactory $clientFactory,
+ ILogger $logger) {
+ $this->clientFactory = $clientFactory;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return int[]
+ * @throws ServiceException
+ */
+ public function findMatches(Account $account,
+ Mailbox $mailbox,
+ SearchQuery $searchQuery): array {
+ $client = $this->clientFactory->getClient($account);
+
+ try {
+ $fetchResult = $client->search(
+ $mailbox->getMailbox(),
+ $this->convertMailQueryToHordeQuery($searchQuery)
+ );
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not get message IDs: ' . $e->getMessage(), 0, $e);
+ }
+
+ return $fetchResult['match']->ids;
+ }
+
+ private function convertMailQueryToHordeQuery(SearchQuery $searchQuery): Horde_Imap_Client_Search_Query {
+ $query = new Horde_Imap_Client_Search_Query();
+
+ foreach ($searchQuery->getFlags() as $flag => $set) {
+ $query->flag($flag, $set);
+ }
+
+ // TODO: text, header text
+
+ return $query;
+ }
+
+}
diff --git a/lib/IMAP/Sync/ISyncStrategy.php b/lib/IMAP/Sync/ISyncStrategy.php
index e8e69504d..028d419a9 100644
--- a/lib/IMAP/Sync/ISyncStrategy.php
+++ b/lib/IMAP/Sync/ISyncStrategy.php
@@ -57,6 +57,6 @@ interface ISyncStrategy {
* @param Horde_Imap_Client_Data_Sync $hordeSync
* @return int[]
*/
- public function getVanishedMessages(Horde_Imap_Client_Base $imapClient,
- Request $syncRequest, Horde_Imap_Client_Data_Sync $hordeSync): array;
+ public function getVanishedMessageUids(Horde_Imap_Client_Base $imapClient,
+ Request $syncRequest, Horde_Imap_Client_Data_Sync $hordeSync): array;
}
diff --git a/lib/IMAP/Sync/Response.php b/lib/IMAP/Sync/Response.php
index 00276f13c..1ae46b595 100644
--- a/lib/IMAP/Sync/Response.php
+++ b/lib/IMAP/Sync/Response.php
@@ -28,39 +28,63 @@ use OCA\Mail\Model\IMAPMessage;
class Response implements JsonSerializable {
- /** @var string */
- private $syncToken;
-
/** @var IMAPMessage[] */
private $newMessages;
/** @var IMAPMessage[] */
private $changedMessages;
- /** @var array */
- private $vanishedMessages;
+ /** @var int[] */
+ private $vanishedMessageUids;
/**
* @param string $syncToken
* @param IMAPMessage[] $newMessages
* @param IMAPMessage[] $changedMessages
- * @param array $vanishedMessages
+ * @param int[] $vanishedMessageUids
*/
- public function __construct(string $syncToken, array $newMessages = [], array $changedMessages = [],
- array $vanishedMessages = []) {
- $this->syncToken = $syncToken;
+ public function __construct(array $newMessages = [], array $changedMessages = [],
+ array $vanishedMessageUids = []) {
$this->newMessages = $newMessages;
$this->changedMessages = $changedMessages;
- $this->vanishedMessages = $vanishedMessages;
+ $this->vanishedMessageUids = $vanishedMessageUids;
+ }
+
+ /**
+ * @return IMAPMessage[]
+ */
+ public function getNewMessages(): array {
+ return $this->newMessages;
+ }
+
+ /**
+ * @return IMAPMessage[]
+ */
+ public function getChangedMessages(): array {
+ return $this->changedMessages;
+ }
+
+ /**
+ * @return int[]
+ */
+ public function getVanishedMessageUids(): array {
+ return $this->vanishedMessageUids;
}
public function jsonSerialize(): array {
return [
'newMessages' => $this->newMessages,
'changedMessages' => $this->changedMessages,
- 'vanishedMessages' => $this->vanishedMessages,
- 'token' => $this->syncToken,
+ 'vanishedMessages' => $this->vanishedMessageUids,
];
}
+ public function merge(Response $other): self {
+ return new self(
+ array_merge($this->getNewMessages(), $other->getNewMessages()),
+ array_merge($this->getChangedMessages(), $other->getChangedMessages()),
+ array_merge($this->getVanishedMessageUids(), $other->getVanishedMessageUids())
+ );
+ }
+
}
diff --git a/lib/IMAP/Sync/SimpleMailboxSync.php b/lib/IMAP/Sync/SimpleMailboxSync.php
index 002c49c2b..919ea7367 100644
--- a/lib/IMAP/Sync/SimpleMailboxSync.php
+++ b/lib/IMAP/Sync/SimpleMailboxSync.php
@@ -69,8 +69,8 @@ class SimpleMailboxSync implements ISyncStrategy {
* @param Horde_Imap_Client_Data_Sync $hordeSync
* @return IMAPMessage[]
*/
- public function getVanishedMessages(Horde_Imap_Client_Base $imapClient,
- Request $syncRequest, Horde_Imap_Client_Data_Sync $hordeSync): array {
+ public function getVanishedMessageUids(Horde_Imap_Client_Base $imapClient,
+ Request $syncRequest, Horde_Imap_Client_Data_Sync $hordeSync): array {
return $hordeSync->vanisheduids->ids;
}
diff --git a/lib/IMAP/Sync/Synchronizer.php b/lib/IMAP/Sync/Synchronizer.php
index 54db5128b..d286f33ec 100644
--- a/lib/IMAP/Sync/Synchronizer.php
+++ b/lib/IMAP/Sync/Synchronizer.php
@@ -23,11 +23,13 @@ declare(strict_types=1);
namespace OCA\Mail\IMAP\Sync;
+use Horde_Imap_Client;
use Horde_Imap_Client_Base;
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Exception_Sync;
use Horde_Imap_Client_Ids;
use Horde_Imap_Client_Mailbox;
+use OCA\mail\lib\Exception\UidValidityChangedException;
class Synchronizer {
@@ -50,24 +52,36 @@ class Synchronizer {
/**
* @param Horde_Imap_Client_Base $imapClient
* @param Request $request
+ * @param int $criteria
+ *
* @return Response
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
+ * @throws UidValidityChangedException
*/
- public function sync(Horde_Imap_Client_Base $imapClient, Request $request): Response {
+ public function sync(Horde_Imap_Client_Base $imapClient,
+ Request $request,
+ int $criteria = Horde_Imap_Client::SYNC_NEWMSGSUIDS|Horde_Imap_Client::SYNC_FLAGSUIDS|Horde_Imap_Client::SYNC_VANISHEDUIDS): Response {
$mailbox = new Horde_Imap_Client_Mailbox($request->getMailbox());
$ids = new Horde_Imap_Client_Ids($request->getUids());
- $hordeSync = $imapClient->sync($mailbox, $request->getToken(), [
- 'ids' => $ids
- ]);
+ try {
+ $hordeSync = $imapClient->sync($mailbox, $request->getToken(), [
+ 'criteria' => $criteria,
+ 'ids' => $ids
+ ]);
+ } catch (Horde_Imap_Client_Exception_Sync $e) {
+ if ($e->getCode() === Horde_Imap_Client_Exception_Sync::UIDVALIDITY_CHANGED) {
+ throw new UidValidityChangedException();
+ }
+ throw $e;
+ }
$syncStrategy = $this->getSyncStrategy($request);
$newMessages = $syncStrategy->getNewMessages($imapClient, $request, $hordeSync);
$changedMessages = $syncStrategy->getChangedMessages($imapClient, $request, $hordeSync);
- $vanishedMessages = $syncStrategy->getVanishedMessages($imapClient, $request, $hordeSync);
+ $vanishedMessageUids = $syncStrategy->getVanishedMessageUids($imapClient, $request, $hordeSync);
- $newSyncToken = $imapClient->getSyncToken($request->getMailbox());
- return new Response($newSyncToken, $newMessages, $changedMessages, $vanishedMessages);
+ return new Response($newMessages, $changedMessages, $vanishedMessageUids);
}
/**
diff --git a/lib/Migration/FixAccountSyncs.php b/lib/Migration/FixAccountSyncs.php
new file mode 100644
index 000000000..319e0cb4e
--- /dev/null
+++ b/lib/Migration/FixAccountSyncs.php
@@ -0,0 +1,64 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Mail\Migration;
+
+use OCA\Mail\BackgroundJob\SyncJob;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Db\MailAccountMapper;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class FixAccountSyncs implements IRepairStep {
+
+ /** @var IJobList */
+ private $jobList;
+ /** @var MailAccountMapper */
+ private $mapper;
+
+ public function __construct(IJobList $jobList, MailAccountMapper $mapper) {
+ $this->jobList = $jobList;
+ $this->mapper = $mapper;
+ }
+
+ public function getName(): string {
+ return 'Insert sync background job for all accounts';
+ }
+
+ public function run(IOutput $output) {
+ /** @var MailAccount[] $accounts */
+ $accounts = $this->mapper->getAllAccounts();
+
+ $output->startProgress(count($accounts));
+
+ foreach ($accounts as $account) {
+ $this->jobList->add(SyncJob::class, ['accountId' => $account->getId()]);
+ $output->advance();
+ }
+
+ $output->finishProgress();
+ }
+
+}
diff --git a/lib/Migration/Version1020Date20191002091034.php b/lib/Migration/Version1020Date20191002091034.php
new file mode 100644
index 000000000..4020be614
--- /dev/null
+++ b/lib/Migration/Version1020Date20191002091034.php
@@ -0,0 +1,33 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Mail\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\IDBConnection;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Migration\IOutput;
+
+class Version1020Date20191002091034 extends SimpleMigrationStep {
+
+ /** @var IDBConnection */
+ protected $connection;
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ *
+ * @return ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $mailboxTable = $schema->getTable('mail_mailboxes');
+ $mailboxTable->dropColumn('sync_token');
+
+ return $schema;
+ }
+
+}
diff --git a/lib/Migration/Version1020Date20191002091035.php b/lib/Migration/Version1020Date20191002091035.php
new file mode 100644
index 000000000..d0cb3b054
--- /dev/null
+++ b/lib/Migration/Version1020Date20191002091035.php
@@ -0,0 +1,157 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Mail\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\IDBConnection;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Migration\IOutput;
+
+class Version1020Date20191002091035 extends SimpleMigrationStep {
+
+ /** @var IDBConnection */
+ protected $connection;
+
+ public function __construct(IDBConnection $connection) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ *
+ * @return ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $messagesTable = $schema->createTable('mail_messages');
+ $messagesTable->addColumn('id', 'integer', [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $messagesTable->addColumn('uid', 'integer', [
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $messagesTable->addColumn('message_id', 'string', [
+ 'notnull' => false,
+ 'length' => 255,
+ ]);
+ $messagesTable->addColumn('mailbox_id', 'string', [
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $messagesTable->addColumn('subject', 'string', [
+ 'notnull' => true,
+ 'length' => 255,
+ 'default' => '',
+ ]);
+ $messagesTable->addColumn('sent_at', 'integer', [
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $messagesTable->addColumn('flag_answered', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_deleted', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_draft', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_flagged', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_seen', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_forwarded', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_junk', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('flag_notjunk', 'boolean', [
+ 'notnull' => true,
+ 'default' => false,
+ ]);
+ $messagesTable->addColumn('updated_at', 'integer', [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $messagesTable->setPrimaryKey(['id']);
+ // We allow each UID just once
+ $messagesTable->addUniqueIndex([
+ 'uid',
+ 'mailbox_id',
+ ]);
+ $messagesTable->addIndex(['sent_at'], 'mail_message_sent_idx');
+
+ $recipientsTable = $schema->createTable('mail_recipients');
+ $recipientsTable->addColumn('id', 'integer', [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $recipientsTable->addColumn('message_id', 'integer', [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $recipientsTable->addColumn('type', 'integer', [
+ 'notnull' => true,
+ 'length' => 2,
+ ]);
+ $recipientsTable->addColumn('label', 'string', [
+ 'notnull' => false,
+ 'length' => 255,
+ ]);
+ $recipientsTable->addColumn('email', 'string', [
+ 'notnull' => true,
+ 'length' => 255,
+ ]);
+ $recipientsTable->setPrimaryKey(['id']);
+ $recipientsTable->addIndex(['message_id'], 'mail_recipient_msg_id_idx');
+ $recipientsTable->addIndex(['email'], 'mail_recipient_email_idx');
+
+ $mailboxTable = $schema->getTable('mail_mailboxes');
+ $mailboxTable->addColumn('sync_new_lock', 'integer', [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $mailboxTable->addColumn('sync_changed_lock', 'integer', [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $mailboxTable->addColumn('sync_vanished_lock', 'integer', [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $mailboxTable->addColumn('sync_new_token', 'string', [
+ 'notnull' => false,
+ 'length' => 255,
+ ]);
+ $mailboxTable->addColumn('sync_changed_token', 'string', [
+ 'notnull' => false,
+ 'length' => 255,
+ ]);
+ $mailboxTable->addColumn('sync_vanished_token', 'string', [
+ 'notnull' => false,
+ 'length' => 255,
+ ]);
+
+ return $schema;
+ }
+
+}
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index 714342d6a..be30a78ad 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -48,6 +48,7 @@ use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Files\File;
use OCP\Files\SimpleFS\ISimpleFile;
use function base64_encode;
+use function json_encode;
use function mb_convert_encoding;
class IMAPMessage implements IMessage, JsonSerializable {
@@ -69,6 +70,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
* @param Horde_Imap_Client_Data_Fetch|null $fetch
* @param bool $loadHtmlMessage
* @param Html|null $htmlService
+ *
* @throws DoesNotExistException
*/
public function __construct($conn,
@@ -145,6 +147,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param array $flags
+ *
* @throws Exception
*/
public function setFlags(array $flags) {
@@ -168,6 +171,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param AddressList $from
+ *
* @throws Exception
*/
public function setFrom(AddressList $from) {
@@ -183,6 +187,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param AddressList $to
+ *
* @throws Exception
*/
public function setTo(AddressList $to) {
@@ -198,6 +203,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param AddressList $cc
+ *
* @throws Exception
*/
public function setCC(AddressList $cc) {
@@ -213,6 +219,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param AddressList $bcc
+ *
* @throws Exception
*/
public function setBcc(AddressList $bcc) {
@@ -237,6 +244,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param string $subject
+ *
* @throws Exception
*/
public function setSubject(string $subject) {
@@ -259,6 +267,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $part
+ *
* @return bool
*/
private function hasAttachments($part) {
@@ -319,7 +328,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
} else {
if (!is_null($structure->findBody())) {
// get the body from the server
- $partId = (int) $structure->findBody();
+ $partId = (int)$structure->findBody();
$this->getPart($structure->getPart($partId), $partId);
}
}
@@ -328,6 +337,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $p
* @param mixed $partNo
+ *
* @throws DoesNotExistException
*/
private function getPart(Horde_Mime_Part $p, $partNo) {
@@ -427,6 +437,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
* @param int $accountId
* @param string $folderId
* @param int $messageId
+ *
* @return string
*/
public function getHtmlBody(int $accountId, string $folderId, int $messageId): string {
@@ -457,6 +468,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $part
* @param mixed $partNo
+ *
* @throws DoesNotExistException
*/
private function handleMultiPartMessage(Horde_Mime_Part $part, $partNo) {
@@ -470,6 +482,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $p
* @param mixed $partNo
+ *
* @throws DoesNotExistException
*/
private function handleTextMessage(Horde_Mime_Part $p, $partNo) {
@@ -480,6 +493,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $p
* @param mixed $partNo
+ *
* @throws DoesNotExistException
*/
private function handleHtmlMessage(Horde_Mime_Part $p, $partNo) {
@@ -493,6 +507,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
/**
* @param Horde_Mime_Part $p
* @param mixed $partNo
+ *
* @return string
* @throws DoesNotExistException
* @throws Exception
@@ -582,4 +597,30 @@ class IMAPMessage implements IMessage, JsonSerializable {
throw new Exception('not implemented');
}
+ public function toDbMessage(int $mailboxId): \OCA\Mail\Db\Message {
+ $msg = new \OCA\Mail\Db\Message();
+
+ $msg->setUid($this->getUid());
+ $msg->setMessageId($this->getMessageId());
+ $msg->setMailboxId($mailboxId);
+ $msg->setFrom($this->getFrom());
+ $msg->setTo($this->getTo());
+ $msg->setCc($this->getCc());
+ $msg->setBcc($this->getBcc());
+ $msg->setSubject(mb_substr($this->getSubject(), 0, 255));
+ $msg->setSentAt($this->getSentDate()->getTimestamp());
+
+ $flags = $this->fetch->getFlags();
+ $msg->setFlagAnswered(in_array(Horde_Imap_Client::FLAG_ANSWERED, $flags, true));
+ $msg->setFlagDeleted(in_array(Horde_Imap_Client::FLAG_DELETED, $flags, true));
+ $msg->setFlagDraft(in_array(Horde_Imap_Client::FLAG_DRAFT, $flags, true));
+ $msg->setFlagFlagged(in_array(Horde_Imap_Client::FLAG_FLAGGED, $flags, true));
+ $msg->setFlagSeen(in_array(Horde_Imap_Client::FLAG_SEEN, $flags, true));
+ $msg->setFlagForwarded(in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags, true));
+ $msg->setFlagJunk(in_array(Horde_Imap_Client::FLAG_JUNK, $flags, true));
+ $msg->setFlagNotjunk(in_array(Horde_Imap_Client::FLAG_NOTJUNK, $flags, true));
+
+ return $msg;
+ }
+
}
diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php
index 70472ba9b..e7b7e9ed6 100644
--- a/lib/Service/AccountService.php
+++ b/lib/Service/AccountService.php
@@ -25,10 +25,12 @@ declare(strict_types=1);
namespace OCA\Mail\Service;
use OCA\Mail\Account;
+use OCA\Mail\BackgroundJob\SyncJob;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Exception\ServiceException;
use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\BackgroundJob\IJobList;
use function array_map;
class AccountService {
@@ -46,10 +48,15 @@ class AccountService {
/** @var AliasesService */
private $aliasesService;
+ /** @var IJobList */
+ private $jobList;
+
public function __construct(MailAccountMapper $mapper,
- AliasesService $aliasesService) {
+ AliasesService $aliasesService,
+ IJobList $jobList) {
$this->mapper = $mapper;
$this->aliasesService = $aliasesService;
+ $this->jobList = $jobList;
}
/**
@@ -67,6 +74,14 @@ class AccountService {
}
/**
+ * @param string $id
+ * @return Account
+ */
+ public function findById(int $id): Account {
+ return new Account($this->mapper->findById($id));
+ }
+
+ /**
* @param string $uid
* @param int $accountId
*
@@ -87,16 +102,6 @@ class AccountService {
}
/**
- * @param int $id
- *
- * @return Account
- * @throws DoesNotExistException
- */
- public function findById(int $id): Account {
- return new Account($this->mapper->findById($id));
- }
-
- /**
* @param int $accountId
*/
public function delete(string $currentUserId, int $accountId): void {
@@ -110,7 +115,12 @@ class AccountService {
* @return MailAccount
*/
public function save(MailAccount $newAccount): MailAccount {
- return $this->mapper->save($newAccount);
+ $newAccount = $this->mapper->save($newAccount);
+
+ // Insert a background sync job for this account
+ $this->jobList->add(SyncJob::class, ['accountId' => $newAccount->getId()]);
+
+ return $newAccount;
}
public function updateSignature(int $id, string $uid, string $signature = null): void {
diff --git a/lib/Service/AutoCompletion/AddressCollector.php b/lib/Service/AutoCompletion/AddressCollector.php
index 53131e74b..4f22080e4 100644
--- a/lib/Service/AutoCompletion/AddressCollector.php
+++ b/lib/Service/AutoCompletion/AddressCollector.php
@@ -76,7 +76,7 @@ class AddressCollector {
$this->logger->debug("<$address> is not a valid RFC822 mail address");
return;
}
- if (!$this->mapper->exists($this->userId, $address->getEmail())) {
+ if ($address->getEmail() !== null && !$this->mapper->exists($this->userId, $address->getEmail())) {
$this->logger->debug("saving new address <{$address->getEmail()}>");
$entity = new CollectedAddress();
diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php
index 9ccc6866d..4c5359dc2 100644
--- a/lib/Service/MailManager.php
+++ b/lib/Service/MailManager.php
@@ -24,13 +24,11 @@ declare(strict_types=1);
namespace OCA\Mail\Service;
use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Exception_Sync;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Events\BeforeMessageDeletedEvent;
-use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Folder;
use OCA\Mail\IMAP\FolderMapper;
@@ -38,9 +36,6 @@ use OCA\Mail\IMAP\FolderStats;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\IMAP\MessageMapper;
-use OCA\Mail\IMAP\Sync\Request;
-use OCA\Mail\IMAP\Sync\Response;
-use OCA\Mail\IMAP\Sync\Synchronizer;
use OCA\Mail\Model\IMAPMessage;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
@@ -59,9 +54,6 @@ class MailManager implements IMailManager {
/** @var FolderMapper */
private $folderMapper;
- /** @var Synchronizer */
- private $synchronizer;
-
/** @var MessageMapper */
private $messageMapper;
@@ -72,14 +64,12 @@ class MailManager implements IMailManager {
MailboxMapper $mailboxMapper,
MailboxSync $mailboxSync,
FolderMapper $folderMapper,
- Synchronizer $synchronizer,
MessageMapper $messageMapper,
IEventDispatcher $eventDispatcher) {
$this->imapClientFactory = $imapClientFactory;
$this->mailboxMapper = $mailboxMapper;
$this->mailboxSync = $mailboxSync;
$this->folderMapper = $folderMapper;
- $this->synchronizer = $synchronizer;
$this->messageMapper = $messageMapper;
$this->eventDispatcher = $eventDispatcher;
}
@@ -102,27 +92,6 @@ class MailManager implements IMailManager {
/**
* @param Account $account
- * @param Request $syncRequest
- *
- * @return Response
- *
- * @throws ClientException
- * @throws ServiceException
- */
- public function syncMessages(Account $account, Request $syncRequest): Response {
- $client = $this->imapClientFactory->getClient($account);
-
- try {
- return $this->synchronizer->sync($client, $syncRequest);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException("Could not sync messages", 0, $e);
- } catch (Horde_Imap_Client_Exception_Sync $e) {
- throw new ClientException("Sync failed because of an invalid sync token or UID validity changed", 0, $e);
- }
- }
-
- /**
- * @param Account $account
* @param string $name
*
* @return Folder
diff --git a/lib/Service/MailSearch.php b/lib/Service/MailSearch.php
deleted file mode 100644
index a2729705b..000000000
--- a/lib/Service/MailSearch.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-namespace OCA\Mail\Service;
-
-
-use DateTime;
-use Horde_Imap_Client;
-use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Exception_NoSupportExtension;
-use Horde_Imap_Client_Fetch_Query;
-use Horde_Imap_Client_Ids;
-use Horde_Imap_Client_Search_Query;
-use Horde_Imap_Client_Socket;
-use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailSearch;
-use OCA\Mail\Db\MailboxMapper;
-use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\Search\SearchStrategyFactory;
-use OCA\Mail\Model\IMAPMessage;
-use OCA\Mail\IMAP\Search\SearchFilterStringParser;
-use OCP\AppFramework\Db\DoesNotExistException;
-use OCP\ILogger;
-use function array_keys;
-use function array_reverse;
-use function in_array;
-use function uasort;
-
-class MailSearch implements IMailSearch {
-
- /** @var IMAPClientFactory */
- private $clientFactory;
-
- /** @var SearchStrategyFactory */
- private $searchStrategyFactory;
-
- /** @var SearchFilterStringParser */
- private $filterStringParser;
-
- /** @var MailboxMapper */
- private $mailboxMapper;
-
- /** @var ILogger */
- private $logger;
-
- public function __construct(IMAPClientFactory $clientFactory,
- SearchStrategyFactory $searchStrategyFactory,
- SearchFilterStringParser $filterStringParser,
- MailboxMapper $mailboxMapper,
- ILogger $logger) {
- $this->clientFactory = $clientFactory;
- $this->searchStrategyFactory = $searchStrategyFactory;
- $this->filterStringParser = $filterStringParser;
- $this->mailboxMapper = $mailboxMapper;
- $this->logger = $logger;
- }
-
- /**
- * @param Account $account
- * @param string $mailboxName
- * @param string|null $filter
- * @param string|null $cursor
- *
- * @return IMAPMessage[]
- * @throws ServiceException
- */
- public function findMessages(Account $account, string $mailboxName, ?string $filter, ?int $cursor): array {
- $client = $this->clientFactory->getClient($account);
- try {
- $mailbox = $this->mailboxMapper->find($account, $mailboxName);
- } catch (DoesNotExistException $e) {
- throw new ServiceException('Mailbox does not exist', 0, $e);
- }
-
- try {
- $query = $this->filterStringParser->parse($filter);
-
- // In flagged we don't want anything but flagged messages
- if ($mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_FLAGGED)) {
- $query->flag(Horde_Imap_Client::FLAG_FLAGGED);
- }
-
- // Don't show deleted messages unless for folders
- if (!$mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_TRASH)) {
- $query->flag(Horde_Imap_Client::FLAG_DELETED, false);
- }
-
- $ids = $this->searchStrategyFactory
- ->getStrategy($client, $mailbox->getMailbox(), $query, $cursor)
- ->getIds(20);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException('Could not get message IDs: ' . $e->getMessage(), 0, $e);
- }
-
- try {
- $fetchQuery = new Horde_Imap_Client_Fetch_Query();
- $fetchQuery->envelope();
- $fetchQuery->flags();
- $fetchQuery->size();
- $fetchQuery->uid();
- $fetchQuery->imapDate();
- $fetchQuery->structure();
-
- $fetchResult = $client->fetch($mailbox->getMailbox(), $fetchQuery, ['ids' => $ids]);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException('Could not fetch messages', 0, $e);
- }
-
- // TODO: do we still need this fix?
- ob_start(); // fix for Horde warnings
- $messages = array_map(function (int $messageId) use ($mailbox, $client, $fetchResult) {
- $header = $fetchResult[$messageId];
- return new IMAPMessage($client, $mailbox->getMailbox(), $messageId, $header);
- }, $fetchResult->ids());
- ob_get_clean();
-
- return $messages;
- }
-}
diff --git a/lib/IMAP/Search/SearchFilterStringParser.php b/lib/Service/Search/FilterStringParser.php
index 665700a83..d3779f5e8 100644
--- a/lib/IMAP/Search/SearchFilterStringParser.php
+++ b/lib/Service/Search/FilterStringParser.php
@@ -21,11 +21,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-namespace OCA\Mail\IMAP\Search;
+namespace OCA\Mail\Service\Search;
-use Horde_Imap_Client_Search_Query;
-
-class SearchFilterStringParser {
+class FilterStringParser {
private const FLAG_MAP = [
'read' => ['SEEN', true],
@@ -33,26 +31,22 @@ class SearchFilterStringParser {
'answered' => ['ANSWERED', true],
];
- public function parse(?string $filter): Horde_Imap_Client_Search_Query {
- $query = new Horde_Imap_Client_Search_Query();
+ public function parse(?string $filter): SearchQuery {
+ $query = new SearchQuery();
if (empty($filter)) {
return $query;
}
$tokens = explode(' ', $filter);
- $textTokens = [];
foreach ($tokens as $token) {
if (!$this->parseFilterToken($query, $token)) {
- $textTokens[] = $token;
+ $query->addTextToken($token);
}
}
- if (count($textTokens)) {
- $query->text(implode(' ', $textTokens), false);
- }
return $query;
}
- private function parseFilterToken(Horde_Imap_Client_Search_Query $query, $token): bool {
+ private function parseFilterToken(SearchQuery $query, $token): bool {
if (strpos($token, ':') === false) {
return false;
}
@@ -65,16 +59,24 @@ class SearchFilterStringParser {
case 'not':
if (array_key_exists($param, self::FLAG_MAP)) {
$flag = self::FLAG_MAP[$param];
- $query->flag($flag[0], $type === 'is' ? $flag[1] : !$flag[1]);
+ $query->addFlag($flag[0], $type === 'is' ? $flag[1] : !$flag[1]);
return true;
}
break;
case 'from':
+ $query->addFrom($param);
+ return true;
case 'to':
+ $query->addTo($param);
+ return true;
case 'cc':
+ $query->addCc($param);
+ return true;
case 'bcc':
+ $query->addBcc($param);
+ return true;
case 'subject':
- $query->headerText($type, $param);
+ $query->setSubject($param);
return true;
}
diff --git a/lib/Service/Search/MailSearch.php b/lib/Service/Search/MailSearch.php
new file mode 100644
index 000000000..1927af9aa
--- /dev/null
+++ b/lib/Service/Search/MailSearch.php
@@ -0,0 +1,130 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Service\Search;
+
+use Horde_Imap_Client;
+use OCA\Mail\Account;
+use OCA\Mail\Contracts\IMailSearch;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\IMAP\Search\Provider as ImapSearchProvider;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\ILogger;
+
+class MailSearch implements IMailSearch {
+
+ /** @var FilterStringParser */
+ private $filterStringParser;
+
+ /** @var MailboxMapper */
+ private $mailboxMapper;
+
+ /** @var ImapSearchProvider */
+ private $imapSearchProvider;
+
+ /** @var MessageMapper */
+ private $messageMapper;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(FilterStringParser $filterStringParser,
+ MailboxMapper $mailboxMapper,
+ ImapSearchProvider $imapSearchProvider,
+ MessageMapper $messageMapper,
+ ILogger $logger) {
+ $this->filterStringParser = $filterStringParser;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->imapSearchProvider = $imapSearchProvider;
+ $this->messageMapper = $messageMapper;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param Account $account
+ * @param string $mailboxName
+ * @param string|null $filter
+ * @param string|null $cursor
+ *
+ * @return Message[]
+ * @throws ServiceException
+ */
+ public function findMessages(Account $account,
+ string $mailboxName,
+ ?string $filter,
+ ?int $cursor): array {
+ try {
+ $mailbox = $this->mailboxMapper->find($account, $mailboxName);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException('Mailbox does not exist', 0, $e);
+ }
+
+ $query = $this->filterStringParser->parse($filter);
+ if ($cursor !== null) {
+ $query->setCursor($cursor);
+ }
+ // In flagged we don't want anything but flagged messages
+ if ($mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_FLAGGED)) {
+ $query->addFlag(Horde_Imap_Client::FLAG_FLAGGED);
+ }
+ // Don't show deleted messages except for trash folders
+ if (!$mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_TRASH)) {
+ $query->addFlag(Horde_Imap_Client::FLAG_DELETED, false);
+ }
+
+ $uids = array_merge(
+ $this->getDbUids($mailbox, $query),
+ $this->getImapUids($account, $mailbox, $query)
+ );
+
+ return $this->messageMapper->findByUids($mailbox, $uids);
+ }
+
+ private function getDbUids(Mailbox $mailbox, SearchQuery $query) {
+ return $this->messageMapper->findUidsByQuery($mailbox, $query);
+ }
+
+ /**
+ * @param Account $account
+ * @param SearchQuery $query
+ * @param Mailbox $mailbox
+ *
+ * @throws ServiceException
+ */
+ private function getImapUids(Account $account, Mailbox $mailbox, SearchQuery $query): array {
+ if (empty($query->getTextTokens())) {
+ return [];
+ }
+
+ return $this->imapSearchProvider->findMatches(
+ $account,
+ $mailbox,
+ $query
+ );
+ }
+
+}
diff --git a/lib/Service/Search/SearchQuery.php b/lib/Service/Search/SearchQuery.php
new file mode 100644
index 000000000..560420d90
--- /dev/null
+++ b/lib/Service/Search/SearchQuery.php
@@ -0,0 +1,143 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Service\Search;
+
+class SearchQuery {
+
+ /** @var int|null */
+ private $cursor;
+
+ /** @var bool[] */
+ private $flags = [];
+
+ /** @var string[] */
+ private $to = [];
+
+ /** @var string[] */
+ private $from = [];
+
+ /** @var string[] */
+ private $cc = [];
+
+ /** @var string[] */
+ private $bcc = [];
+
+ /** @var string|null */
+ private $subject;
+
+ /** @var string[] */
+ private $textTokens = [];
+
+ /**
+ * @return int|null
+ */
+ public function getCursor(): ?int {
+ return $this->cursor;
+ }
+
+ /**
+ * @param int $cursor
+ */
+ public function setCursor(int $cursor): void {
+ $this->cursor = $cursor;
+ }
+
+ /**
+ * @return bool[]
+ */
+ public function getFlags(): array {
+ return $this->flags;
+ }
+
+ public function addFlag(string $flag, bool $value = true): void {
+ $this->flags[$flag] = $value;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getTo(): array {
+ return $this->to;
+ }
+
+ public function addTo(string $to): void {
+ $this->to[] = $to;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getFrom(): array {
+ return $this->from;
+ }
+
+ public function addFrom(string $from): void {
+ $this->from[] = $from;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getCc(): array {
+ return $this->cc;
+ }
+
+ public function addCc(string $cc): void {
+ $this->cc[] = $cc;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getBcc(): array {
+ return $this->bcc;
+ }
+
+ public function addBcc(string $bcc): void {
+ $this->bcc[] = $bcc;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSubject(): ?string {
+ return $this->subject;
+ }
+
+ public function setSubject(?string $subject): void {
+ $this->subject = $subject;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getTextTokens(): array {
+ return $this->textTokens;
+ }
+
+ public function addTextToken(string $textToken): void {
+ $this->textTokens[] = $textToken;
+ }
+
+}
diff --git a/lib/Service/SyncService.php b/lib/Service/SyncService.php
new file mode 100644
index 000000000..6a084a575
--- /dev/null
+++ b/lib/Service/SyncService.php
@@ -0,0 +1,393 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Service;
+
+use Horde_Imap_Client;
+use Horde_Imap_Client_Exception;
+use Horde_Imap_Client_Exception_Sync;
+use OCA\Mail\Account;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Db\MessageMapper as DatabaseMessageMapper;
+use OCA\Mail\Exception\ConcurrentSyncException;
+use OCA\Mail\Exception\MailboxNotCachedException;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
+use OCA\Mail\IMAP\Sync\Request;
+use OCA\Mail\IMAP\Sync\Response;
+use OCA\Mail\IMAP\Sync\Synchronizer;
+use OCA\mail\lib\Exception\UidValidityChangedException;
+use OCA\Mail\Model\IMAPMessage;
+use OCA\Mail\Support\PerformanceLogger;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\ILogger;
+use Throwable;
+use function array_chunk;
+use function array_map;
+
+class SyncService {
+
+ /** @var DatabaseMessageMapper */
+ private $dbMapper;
+
+ /** @var IMAPClientFactory */
+ private $clientFactory;
+
+ /** @var ImapMessageMapper */
+ private $imapMapper;
+
+ /** @var MailboxMapper */
+ private $mailboxMapper;
+
+ /** @var DatabaseMessageMapper */
+ private $messageMapper;
+
+ /** @var Synchronizer */
+ private $synchronizer;
+
+ /** @var PerformanceLogger */
+ private $performanceLogger;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(DatabaseMessageMapper $dbMapper,
+ IMAPClientFactory $clientFactory,
+ ImapMessageMapper $imapMapper,
+ MailboxMapper $mailboxMapper,
+ MessageMapper $messageMapper,
+ Synchronizer $synchronizer,
+ PerformanceLogger $performanceLogger,
+ ILogger $logger) {
+ $this->dbMapper = $dbMapper;
+ $this->clientFactory = $clientFactory;
+ $this->imapMapper = $imapMapper;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->messageMapper = $messageMapper;
+ $this->synchronizer = $synchronizer;
+ $this->performanceLogger = $performanceLogger;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @throws ServiceException
+ */
+ public function syncAccount(Account $account,
+ bool $force = false,
+ int $criteria = Horde_Imap_Client::SYNC_NEWMSGSUIDS | Horde_Imap_Client::SYNC_FLAGSUIDS | Horde_Imap_Client::SYNC_VANISHEDUIDS): void {
+ foreach ($this->mailboxMapper->findAll($account) as $mailbox) {
+ $this->sync(
+ $account,
+ $mailbox,
+ $criteria,
+ null,
+ $force
+ );
+ }
+ }
+
+ /**
+ * @param int[] $knownUids
+ *
+ * @throws ServiceException
+ */
+ public function syncMailbox(Account $account,
+ string $mailbox,
+ int $criteria = Horde_Imap_Client::SYNC_NEWMSGSUIDS | Horde_Imap_Client::SYNC_FLAGSUIDS | Horde_Imap_Client::SYNC_VANISHEDUIDS,
+ array $knownUids = null,
+ bool $partialOnly = true): Response {
+ try {
+ $mb = $this->mailboxMapper->find($account, $mailbox);
+
+ if ($partialOnly && $mb->getSyncNewToken() === null) {
+ throw new MailboxNotCachedException();
+ }
+
+ return $this->sync(
+ $account,
+ $mb,
+ $criteria,
+ $knownUids
+ );
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException('Mailbox to sync does not exist in the database', 0, $e);
+ }
+ }
+
+ /**
+ * @throws ServiceException
+ */
+ public function ensurePopulated(Account $account, string $mailbox): void {
+ try {
+ $mb = $this->mailboxMapper->find($account, $mailbox);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException('Mailbox does not exist', 0, $e);
+ }
+
+ if ($mb->getSyncNewToken() !== null) {
+ return;
+ }
+
+ try {
+ $this->mailboxMapper->lockForNewSync($mb);
+ $this->mailboxMapper->lockForChangeSync($mb);
+ $this->mailboxMapper->lockForVanishedSync($mb);
+
+ $this->runInitialSync($account, $mb);
+ } catch (ConcurrentSyncException $e) {
+ // Fine, then we don't have to do it
+ } finally {
+ $this->mailboxMapper->unlockFromNewSync($mb);
+ $this->mailboxMapper->unlockFromChangedSync($mb);
+ $this->mailboxMapper->unlockFromVanishedSync($mb);
+ }
+ }
+
+ /**
+ * @param int[] $knownUids
+ *
+ * @throws ServiceException
+ */
+ private function sync(Account $account,
+ Mailbox $mailbox,
+ int $criteria,
+ array $knownUids = null,
+ bool $force = false): Response {
+ if ($mailbox->getSelectable() === false) {
+ return new Response();
+ }
+
+ try {
+ if ($criteria & Horde_Imap_Client::SYNC_NEWMSGSUIDS) {
+ $this->mailboxMapper->lockForNewSync($mailbox);
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_FLAGSUIDS) {
+ $this->mailboxMapper->lockForChangeSync($mailbox);
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_VANISHEDUIDS) {
+ $this->mailboxMapper->lockForVanishedSync($mailbox);
+ }
+ } catch (ConcurrentSyncException $e) {
+ throw new ServiceException('Another sync is in progress for ' . $mailbox->getId(), 0, $e);
+ }
+
+ try {
+ if ($force
+ || $mailbox->getSyncNewToken() === null
+ || $mailbox->getSyncChangedToken() === null
+ || $mailbox->getSyncVanishedToken() === null) {
+ $response = $this->runInitialSync($account, $mailbox);
+ } else {
+ $response = $this->runPartialSync($account, $mailbox, $criteria, $knownUids);
+ }
+ } catch (Throwable $e) {
+ throw new ServiceException('Sync failed', 0, $e);
+ } finally {
+ if ($criteria & Horde_Imap_Client::SYNC_VANISHEDUIDS) {
+ $this->mailboxMapper->unlockFromVanishedSync($mailbox);
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_FLAGSUIDS) {
+ $this->mailboxMapper->unlockFromChangedSync($mailbox);
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_NEWMSGSUIDS) {
+ $this->mailboxMapper->unlockFromNewSync($mailbox);
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * @throws ServiceException
+ */
+ private function runInitialSync(Account $account, Mailbox $mailbox): Response {
+ $perf = $this->performanceLogger->start('Initial sync ' . $account->getId() . ':' . $mailbox->getName());
+
+ $client = $this->clientFactory->getClient($account);
+ try {
+ $imapMessages = $this->imapMapper->findAll($client, $mailbox);
+ $perf->step('fetch all messages from IMAP');
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Can not get messages from mailbox ' . $mailbox->getName() . ': ' . $e->getMessage(), 0, $e);
+ }
+
+ // The sync token could be reset by a migration, hence there could be existing data
+ $this->dbMapper->deleteAll($mailbox);
+ $perf->step('delete existing messages');
+
+ foreach (array_chunk($imapMessages, 500) as $chunk) {
+ $this->dbMapper->insertBulk(...array_map(function (IMAPMessage $imapMessage) use ($mailbox) {
+ return $imapMessage->toDbMessage($mailbox->getId());
+ }, $chunk));
+ }
+ $perf->step('persist messages in database');
+
+ $mailbox->setSyncNewToken($client->getSyncToken($mailbox->getMailbox()));
+ $mailbox->setSyncChangedToken($client->getSyncToken($mailbox->getMailbox()));
+ $mailbox->setSyncVanishedToken($client->getSyncToken($mailbox->getMailbox()));
+ $this->mailboxMapper->update($mailbox);
+
+ $perf->end();
+
+ // Not returning *all* new messages here as this could exhaust the memory
+ return new Response();
+ }
+
+ /**
+ * @param int[] $knownUids
+ *
+ * @throws ServiceException
+ */
+ private function runPartialSync(Account $account,
+ Mailbox $mailbox,
+ int $criteria,
+ array $knownUids = null): Response {
+ $perf = $this->performanceLogger->start('partial sync ' . $account->getId() . ':' . $mailbox->getName());
+
+ $client = $this->clientFactory->getClient($account);
+ $uids = $knownUids ?? $this->dbMapper->findAllUids($mailbox);
+ $perf->step('get all known UIDs');
+
+ $response = new Response();
+ if ($criteria & Horde_Imap_Client::SYNC_NEWMSGSUIDS) {
+ try {
+ $response = $response->merge($this->synchronizer->sync(
+ $client,
+ new Request(
+ $mailbox->getMailbox(),
+ $mailbox->getSyncNewToken(),
+ $uids
+ ),
+ Horde_Imap_Client::SYNC_NEWMSGSUIDS
+ ));
+ } catch (UidValidityChangedException $e) {
+ $this->logger->warning('Mailbox UID validity changed. Performing full sync.');
+
+ return $this->runInitialSync($account, $mailbox);
+ }
+ $perf->step('get new messages via Horde');
+
+ foreach (array_chunk($response->getNewMessages(), 500) as $chunk) {
+ $this->dbMapper->insertBulk(...array_map(function (IMAPMessage $imapMessage) use ($mailbox) {
+ return $imapMessage->toDbMessage($mailbox->getId());
+ }, $chunk));
+ }
+ $perf->step('persist new messages');
+
+ $mailbox->setSyncNewToken($client->getSyncToken($mailbox->getMailbox()));
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_FLAGSUIDS) {
+ try {
+ $response = $response->merge($this->synchronizer->sync(
+ $client,
+ new Request(
+ $mailbox->getMailbox(),
+ $mailbox->getSyncChangedToken(),
+ $uids
+ ),
+ Horde_Imap_Client::SYNC_FLAGSUIDS
+ ));
+ } catch (UidValidityChangedException $e) {
+ $this->logger->warning('Mailbox UID validity changed. Performing full sync.');
+
+ return $this->runInitialSync($account, $mailbox);
+ }
+ $perf->step('get changed messages via Horde');
+
+ foreach (array_chunk($response->getChangedMessages(), 500) as $chunk) {
+ $this->dbMapper->updateBulk(...array_map(function (IMAPMessage $imapMessage) use ($mailbox) {
+ return $imapMessage->toDbMessage($mailbox->getId());
+ }, $chunk));
+ }
+ $perf->step('persist changed messages');
+
+ // If a list of UIDs was *provided* (as opposed to loaded from the DB,
+ // we can not assume that all changes were detected, hence this is kinda
+ // a silent sync and we don't update the change token until the next full
+ // mailbox sync
+ if ($knownUids === null) {
+ $mailbox->setSyncChangedToken($client->getSyncToken($mailbox->getMailbox()));
+ }
+ }
+ if ($criteria & Horde_Imap_Client::SYNC_VANISHEDUIDS) {
+ try {
+ $response = $response->merge($this->synchronizer->sync(
+ $client,
+ new Request(
+ $mailbox->getMailbox(),
+ $mailbox->getSyncVanishedToken(),
+ $uids
+ ),
+ Horde_Imap_Client::SYNC_VANISHEDUIDS
+ ));
+ } catch (UidValidityChangedException $e) {
+ $this->logger->warning('Mailbox UID validity changed. Performing full sync.');
+
+ return $this->runInitialSync($account, $mailbox);
+ }
+ $perf->step('get vanished messages via Horde');
+
+ foreach (array_chunk($response->getVanishedMessageUids(), 500) as $chunk) {
+ $this->dbMapper->deleteByUid($mailbox, ...$chunk);
+ }
+ $perf->step('persist new messages');
+
+ $mailbox->setSyncVanishedToken($client->getSyncToken($mailbox->getMailbox()));
+ }
+ $this->mailboxMapper->update($mailbox);
+
+ $response = $response->merge(
+ $this->getDatabaseSyncChanges($mailbox, $uids)
+ );
+
+ $perf->end();
+
+ return $response;
+ }
+
+ private function getDatabaseSyncChanges(Mailbox $mailbox, array $uids): Response {
+ if (empty($uids)) {
+ return new Response();
+ }
+
+ sort($uids, SORT_NUMERIC);
+ $last = end($uids);
+
+ $new = $this->messageMapper->findNew($mailbox, $last);
+ // TODO: $changed = $this->messageMapper->findChanged($mailbox, $uids);
+ $changed = $this->messageMapper->findByUids($mailbox, $uids);
+ $old = array_map(function (Message $msg) {
+ return $msg->getUid();
+ }, $this->messageMapper->findByUids($mailbox, $uids));
+ $vanished = array_filter($uids, function (int $uid) use ($old) {
+ return !in_array($uid, $old, true);
+ });
+
+ return new Response($new, $changed, $vanished);
+ }
+
+}
diff --git a/lib/IMAP/Search/SearchStrategyFactory.php b/lib/Support/PerformanceLogger.php
index 0ad13f8f3..7043fbb36 100644
--- a/lib/IMAP/Search/SearchStrategyFactory.php
+++ b/lib/Support/PerformanceLogger.php
@@ -21,27 +21,30 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-namespace OCA\Mail\IMAP\Search;
-
-use Horde_Imap_Client_Search_Query;
-use Horde_Imap_Client_Socket;
-
-class SearchStrategyFactory {
-
- public function getStrategy(Horde_Imap_Client_Socket $client,
- string $mailbox,
- Horde_Imap_Client_Search_Query $query,
- ?int $cursor): ISearchStrategy {
- if (!$client->capability->query('SORT') && 'ALL' === $query->__toString()) {
- return new FullScanSearchStrategy($client, $mailbox, $cursor);
- }
-
- return new ImapSortSearchStrategy(
- $client,
- $mailbox,
- $query,
- $cursor,
- new FullScanSearchStrategy($client, $mailbox, $cursor)
+namespace OCA\Mail\Support;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\ILogger;
+
+class PerformanceLogger {
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(ITimeFactory $timeFactory,
+ ILogger $logger) {
+ $this->timeFactory = $timeFactory;
+ $this->logger = $logger;
+ }
+
+ public function start(string $task): PerformanceLoggerTask {
+ return new PerformanceLoggerTask(
+ $task,
+ $this->timeFactory,
+ $this->logger
);
}
diff --git a/lib/Support/PerformanceLoggerTask.php b/lib/Support/PerformanceLoggerTask.php
new file mode 100644
index 000000000..1452f2754
--- /dev/null
+++ b/lib/Support/PerformanceLoggerTask.php
@@ -0,0 +1,72 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Support;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\ILogger;
+
+class PerformanceLoggerTask {
+
+ /** @var string */
+ private $task;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var int */
+ private $start;
+
+ /** @var int */
+ private $rel;
+
+ public function __construct(string $task,
+ ITimeFactory $timeFactory,
+ ILogger $logger) {
+ $this->task = $task;
+ $this->timeFactory = $timeFactory;
+ $this->logger = $logger;
+
+ $this->start = $this->rel = $timeFactory->getTime();
+ }
+
+ public function step(string $description): void {
+ $now = $this->timeFactory->getTime();
+ $passed = $now - $this->rel;
+
+ $this->logger->debug($this->task . " - $description took ${passed}s");
+
+ $this->rel = $now;
+ }
+
+ public function end(): void {
+ $now = $this->timeFactory->getTime();
+ $passed = $now - $this->start;
+
+ $this->logger->debug($this->task . " took ${passed}s");
+ }
+
+}
diff --git a/src/components/FolderContent.vue b/src/components/FolderContent.vue
index 0237d26a9..78a5abdb3 100644
--- a/src/components/FolderContent.vue
+++ b/src/components/FolderContent.vue
@@ -2,7 +2,11 @@
<AppContent>
<AppDetailsToggle v-if="showMessage" @close="hideMessage" />
<div id="app-content-wrapper">
- <Loading v-if="loading" :hint="t('mail', 'Loading messages')" />
+ <Loading
+ v-if="loading"
+ :hint="t('mail', 'Loading messages')"
+ :slow-hint="t('mail', 'This can take a bit longer when the mailbox is accessed for the first time.')"
+ />
<template v-else>
<EnvelopeList
:account="account"
diff --git a/src/components/Loading.vue b/src/components/Loading.vue
index 4547301f3..ffa747352 100644
--- a/src/components/Loading.vue
+++ b/src/components/Loading.vue
@@ -2,6 +2,9 @@
<div v-if="hint" class="emptycontent">
<a class="icon-loading"></a>
<h2>{{ hint }}</h2>
+ <transition name="fade">
+ <em v-if="slowHint && slow">{{ slowHint }}</em>
+ </transition>
</div>
<div v-else class="container icon-loading"></div>
</template>
@@ -12,10 +15,38 @@ export default {
props: {
hint: {
type: String,
- default: () => undefined,
},
+ slowHint: {
+ type: String,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ slow: false,
+ slowTimer: undefined,
+ }
+ },
+ mounted() {
+ clearTimeout(this.slowTimer)
+
+ this.slowTimer = setTimeout(() => {
+ this.slow = true
+ }, 3500)
+ },
+ beforeDestroy() {
+ clearTimeout(this.slowTimer)
},
}
</script>
-<style scoped></style>
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.5s;
+}
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+</style>
diff --git a/src/service/MessageService.js b/src/service/MessageService.js
index 1897ad176..cf1b5308f 100644
--- a/src/service/MessageService.js
+++ b/src/service/MessageService.js
@@ -22,7 +22,7 @@ export function fetchEnvelopes(accountId, folderId, query, cursor) {
}).then(resp => resp.data)
}
-export function syncEnvelopes(accountId, folderId, syncToken, uids) {
+export function syncEnvelopes(accountId, folderId, uids) {
const url = generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/sync', {
accountId,
folderId,
@@ -30,7 +30,6 @@ export function syncEnvelopes(accountId, folderId, syncToken, uids) {
return HttpClient.get(url, {
params: {
- syncToken,
uids,
},
}).then(resp => resp.data)
diff --git a/src/store/actions.js b/src/store/actions.js
index 9a2008e7d..ca5e4a321 100644
--- a/src/store/actions.js
+++ b/src/store/actions.js
@@ -357,10 +357,9 @@ export default {
)
}
- const syncToken = folder.syncToken
const uids = getters.getEnvelopes(accountId, folderId).map(env => env.id)
- return syncEnvelopes(accountId, folderId, syncToken, uids).then(syncData => {
+ return syncEnvelopes(accountId, folderId, uids).then(syncData => {
const unifiedFolder = getters.getUnifiedFolder(folder.specialRole)
syncData.newMessages.forEach(envelope => {
@@ -391,10 +390,6 @@ export default {
})
// Already removed from unified inbox
})
- commit('updateFolderSyncToken', {
- folder,
- syncToken: syncData.token,
- })
return syncData.newMessages
})
diff --git a/src/store/mutations.js b/src/store/mutations.js
index 28ee0f2b1..ad2e7a738 100644
--- a/src/store/mutations.js
+++ b/src/store/mutations.js
@@ -104,9 +104,6 @@ export default {
account.folders.push(id)
})
},
- updateFolderSyncToken(state, {folder, syncToken}) {
- folder.syncToken = syncToken
- },
addEnvelope(state, {accountId, folder, envelope}) {
const uid = accountId + '-' + folder.id + '-' + envelope.id
envelope.accountId = accountId
diff --git a/tests/Integration/Db/MailboxMapperTest.php b/tests/Integration/Db/MailboxMapperTest.php
index a1107145c..0200b0f55 100644
--- a/tests/Integration/Db/MailboxMapperTest.php
+++ b/tests/Integration/Db/MailboxMapperTest.php
@@ -28,6 +28,7 @@ use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Db\MailboxMapper;
use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use PHPUnit\Framework\MockObject\MockObject;
@@ -42,12 +43,17 @@ class MailboxMapperTest extends TestCase {
/** @var MailboxMapper */
private $mapper;
+ /** @var ITimeFactory| MockObject */
+ private $timeFactory;
+
protected function setUp(): void {
parent::setUp();
$this->db = \OC::$server->getDatabaseConnection();
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
$this->mapper = new MailboxMapper(
- $this->db
+ $this->db,
+ $this->timeFactory
);
$qb = $this->db->getQueryBuilder();
@@ -74,7 +80,9 @@ class MailboxMapperTest extends TestCase {
->values([
'name' => $qb->createNamedParameter("folder$i"),
'account_id' => $qb->createNamedParameter($i <= 5 ? 13 : 14, IQueryBuilder::PARAM_INT),
- 'sync_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
'delimiter' => $qb->createNamedParameter('.'),
'messages' => $qb->createNamedParameter($i * 100, IQueryBuilder::PARAM_INT),
'unseen' => $qb->createNamedParameter($i, IQueryBuilder::PARAM_INT),
@@ -106,7 +114,9 @@ class MailboxMapperTest extends TestCase {
->values([
'name' => $qb->createNamedParameter('INBOX'),
'account_id' => $qb->createNamedParameter(13, IQueryBuilder::PARAM_INT),
- 'sync_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
'delimiter' => $qb->createNamedParameter('.'),
'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT),
@@ -137,7 +147,9 @@ class MailboxMapperTest extends TestCase {
->values([
'name' => $qb->createNamedParameter('Trash'),
'account_id' => $qb->createNamedParameter(13, IQueryBuilder::PARAM_INT),
- 'sync_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
+ 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='),
'delimiter' => $qb->createNamedParameter('.'),
'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT),
diff --git a/tests/Integration/FolderSynchronizationTest.php b/tests/Integration/FolderSynchronizationTest.php
index 841a10508..c9c28bfdc 100644
--- a/tests/Integration/FolderSynchronizationTest.php
+++ b/tests/Integration/FolderSynchronizationTest.php
@@ -22,16 +22,18 @@
namespace OCA\Mail\Tests\Integration;
use OC;
+use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Controller\FoldersController;
use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
use OCA\Mail\Tests\Integration\Framework\ImapTestAccount;
class FolderSynchronizationTest extends TestCase {
use ImapTest,
- ImapTestAccount;
+ ImapTestAccount;
/** @var FoldersController */
private $foldersController;
@@ -39,31 +41,45 @@ class FolderSynchronizationTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->foldersController = new FoldersController('mail', OC::$server->getRequest(), OC::$server->query(AccountService::class), $this->getTestAccountUserId(), OC::$server->query(IMailManager::class));
+ $this->foldersController = new FoldersController(
+ 'mail',
+ OC::$server->getRequest(),
+ OC::$server->query(AccountService::class),
+ $this->getTestAccountUserId(),
+ OC::$server->query(IMailManager::class),
+ OC::$server->query(SyncService::class)
+ );
}
public function testSyncEmptyMailbox() {
$account = $this->createTestAccount();
+ /** @var SyncService $syncService */
+ $syncService = OC::$server->query(SyncService::class);
+ $syncService->syncAccount(new Account($account), true);
$mailbox = 'INBOX';
- $syncToken = $this->getMailboxSyncToken($mailbox);
-
- $jsonResponse = $this->foldersController->sync($account->getId(), base64_encode($mailbox), $syncToken);
- $syncJson = $jsonResponse->getData()->jsonSerialize();
- $this->assertArrayHasKey('newMessages', $syncJson);
- $this->assertArrayHasKey('changedMessages', $syncJson);
- $this->assertArrayHasKey('vanishedMessages', $syncJson);
- $this->assertArrayHasKey('token', $syncJson);
- $this->assertEmpty($syncJson['newMessages']);
- $this->assertEmpty($syncJson['changedMessages']);
- $this->assertEmpty($syncJson['vanishedMessages']);
+ $jsonResponse = $this->foldersController->sync(
+ $account->getId(),
+ base64_encode($mailbox),
+ []
+ );
+
+ $data = $jsonResponse->getData()->jsonSerialize();
+ $this->assertArrayHasKey('newMessages', $data);
+ $this->assertArrayHasKey('changedMessages', $data);
+ $this->assertArrayHasKey('vanishedMessages', $data);
+ $this->assertEmpty($data['newMessages']);
+ $this->assertEmpty($data['changedMessages']);
+ $this->assertEmpty($data['vanishedMessages']);
}
public function testSyncNewMessage() {
// First, set up account and retrieve sync token
$account = $this->createTestAccount();
+ /** @var SyncService $syncService */
+ $syncService = OC::$server->query(SyncService::class);
+ $syncService->syncAccount(new Account($account), true);
$mailbox = 'INBOX';
- $syncToken = $this->getMailboxSyncToken($mailbox);
// Second, put a new message into the mailbox
$message = $this->getMessageBuilder()
->from('ralph@buffington@domain.tld')
@@ -71,7 +87,11 @@ class FolderSynchronizationTest extends TestCase {
->finish();
$this->saveMessage($mailbox, $message);
- $jsonResponse = $this->foldersController->sync($account->getId(), base64_encode($mailbox), $syncToken);
+ $jsonResponse = $this->foldersController->sync(
+ $account->getId(),
+ base64_encode($mailbox),
+ []
+ );
$syncJson = $jsonResponse->getData()->jsonSerialize();
$this->assertCount(1, $syncJson['newMessages']);
@@ -82,6 +102,9 @@ class FolderSynchronizationTest extends TestCase {
public function testSyncChangedMessage() {
// First, put a message into the mailbox
$account = $this->createTestAccount();
+ /** @var SyncService $syncService */
+ $syncService = OC::$server->query(SyncService::class);
+ $syncService->syncAccount(new Account($account), true);
$mailbox = 'INBOX';
$message = $this->getMessageBuilder()
->from('ralph@buffington@domain.tld')
@@ -93,19 +116,25 @@ class FolderSynchronizationTest extends TestCase {
// Third, flag it
$this->flagMessage($mailbox, $id);
- $jsonResponse = $this->foldersController->sync($account->getId(), base64_encode($mailbox), $syncToken, [
- $id
- ]);
+ $jsonResponse = $this->foldersController->sync(
+ $account->getId(),
+ base64_encode($mailbox),
+ [
+ $id
+ ]);
$syncJson = $jsonResponse->getData()->jsonSerialize();
- $this->assertCount(0, $syncJson['newMessages']);
- $this->assertCount(1, $syncJson['changedMessages']);
+ $this->assertCount(1, $syncJson['newMessages']);
+ $this->assertCount(2, $syncJson['changedMessages']);
$this->assertCount(0, $syncJson['vanishedMessages']);
}
public function testSyncVanishedMessage() {
// First, put a message into the mailbox
$account = $this->createTestAccount();
+ /** @var SyncService $syncService */
+ $syncService = OC::$server->query(SyncService::class);
+ $syncService->syncAccount(new Account($account), true);
$mailbox = 'INBOX';
$message = $this->getMessageBuilder()
->from('ralph@buffington@domain.tld')
@@ -117,15 +146,18 @@ class FolderSynchronizationTest extends TestCase {
// Third, remove it again
$this->deleteMessage($mailbox, $id);
- $jsonResponse = $this->foldersController->sync($account->getId(), base64_encode($mailbox), $syncToken, [
- $id
- ]);
+ $jsonResponse = $this->foldersController->sync(
+ $account->getId(),
+ base64_encode($mailbox),
+ [
+ $id
+ ]);
$syncJson = $jsonResponse->getData()->jsonSerialize();
$this->assertCount(0, $syncJson['newMessages']);
// TODO: deleted messages are flagged as changed? could be a testing-only issue
// $this->assertCount(0, $syncJson['changedMessages']);
- $this->assertCount(1, $syncJson['vanishedMessages']);
+ $this->assertCount(2, $syncJson['vanishedMessages']);
}
}
diff --git a/tests/Integration/Framework/ImapTestAccount.php b/tests/Integration/Framework/ImapTestAccount.php
index d6f15da67..30271542a 100644
--- a/tests/Integration/Framework/ImapTestAccount.php
+++ b/tests/Integration/Framework/ImapTestAccount.php
@@ -22,7 +22,9 @@
namespace OCA\Mail\Tests\Integration\Framework;
use OC;
+use OCA\Mail\Account;
use OCA\Mail\Db\MailAccount;
+use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\Service\AccountService;
trait ImapTestAccount {
@@ -57,7 +59,13 @@ trait ImapTestAccount {
$mailAccount->setOutboundUser('user@domain.tld');
$mailAccount->setOutboundPassword(OC::$server->getCrypto()->encrypt('mypassword'));
$mailAccount->setOutboundSslMode('none');
- return $accountService->save($mailAccount);
+ $acc = $accountService->save($mailAccount);
+
+ /** @var MailboxSync $mbSync */
+ $mbSync = OC::$server->query(MailboxSync::class);
+ $mbSync->sync(new Account($mailAccount));
+
+ return $acc;
}
}
diff --git a/tests/Unit/Controller/FoldersControllerTest.php b/tests/Unit/Controller/FoldersControllerTest.php
index c2aaf68e7..2b338b1ba 100644
--- a/tests/Unit/Controller/FoldersControllerTest.php
+++ b/tests/Unit/Controller/FoldersControllerTest.php
@@ -21,6 +21,8 @@
namespace OCA\Mail\Tests\Unit\Controller;
+use OCA\Mail\Service\SyncService;
+use PHPUnit\Framework\MockObject\MockObject;
use function base64_encode;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
@@ -32,34 +34,45 @@ use OCA\Mail\Service\AccountService;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
-use PHPUnit_Framework_MockObject_MockObject;
class FoldersControllerTest extends TestCase {
/** @var string */
private $appName = 'mail';
- /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */
+ /** @var IRequest|MockObject */
private $request;
- /** @var AccountService|PHPUnit_Framework_MockObject_MockObject */
+ /** @var AccountService|MockObject */
private $accountService;
/** @var string */
private $userId = 'john';
- /** @var IMailManager|PHPUnit_Framework_MockObject_MockObject */
+ /** @var IMailManager|MockObject */
private $mailManager;
/** @var FoldersController */
private $controller;
+ /** @var SyncService|MockObject */
+ private $syncService;
+
public function setUp(): void {
parent::setUp();
+
$this->request = $this->createMock(IRequest::class);
$this->accountService = $this->createMock(AccountService::class);
$this->mailManager = $this->createMock(IMailManager::class);
- $this->controller = new FoldersController($this->appName, $this->request, $this->accountService, $this->userId, $this->mailManager);
+ $this->syncService = $this->createMock(SyncService::class);
+ $this->controller = new FoldersController(
+ $this->appName,
+ $this->request,
+ $this->accountService,
+ $this->userId,
+ $this->mailManager,
+ $this->syncService
+ );
}
public function testIndex() {
@@ -75,7 +88,7 @@ class FoldersControllerTest extends TestCase {
->with($this->equalTo($account))
->willReturn([
$folder
- ]);
+ ]);
$account->expects($this->once())
->method('getEmail')
->willReturn('user@example.com');
diff --git a/tests/Unit/Controller/MessagesControllerTest.php b/tests/Unit/Controller/MessagesControllerTest.php
index 1178a03c4..0b215c557 100644
--- a/tests/Unit/Controller/MessagesControllerTest.php
+++ b/tests/Unit/Controller/MessagesControllerTest.php
@@ -38,6 +38,7 @@ use OCA\Mail\Model\Message;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\ItineraryService;
use OCA\Mail\Service\MailManager;
+use OCA\Mail\Service\SyncService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ContentSecurityPolicy;
@@ -71,6 +72,9 @@ class MessagesControllerTest extends TestCase {
/** @var ItineraryService|MockObject */
private $itineraryService;
+ /** @var SyncService|MockObject */
+ private $syncService;
+
/** @var string */
private $userId;
@@ -116,6 +120,7 @@ class MessagesControllerTest extends TestCase {
$this->mailManager = $this->createMock(IMailManager::class);
$this->mailSearch = $this->createMock(IMailSearch::class);
$this->itineraryService = $this->createMock(ItineraryService::class);
+ $this->syncService = $this->createMock(SyncService::class);
$this->userId = 'john';
$this->userFolder = $this->createMock(Folder::class);
$this->request = $this->createMock(Request::class);
@@ -140,6 +145,7 @@ class MessagesControllerTest extends TestCase {
$this->mailManager,
$this->mailSearch,
$this->itineraryService,
+ $this->syncService,
$this->userId,
$this->userFolder,
$this->logger,
diff --git a/tests/Unit/IMAP/Search/SearchFilterStringParserTest.php b/tests/Unit/IMAP/Search/SearchFilterStringParserTest.php
deleted file mode 100644
index 9a1affe75..000000000
--- a/tests/Unit/IMAP/Search/SearchFilterStringParserTest.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php declare(strict_types=1);
-
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-namespace OCA\Mail\Tests\Unit\IMAP\Search;
-
-use ChristophWurst\Nextcloud\Testing\TestCase;
-use OCA\Mail\IMAP\Search\SearchFilterStringParser;
-
-class SearchFilterStringParserTest extends TestCase {
-
- private function search($filter) {
- $helper = new SearchFilterStringParser();
- $query = $helper->parse($filter);
- return (string)($query->build()['query']);
- }
-
- public function testSearchEmpty() {
- $this->assertEquals('ALL', $this->search(''));
- }
-
- public function testSearchTest() {
- $this->assertEquals('TEXT "dummy text"', $this->search('dummy text'));
- }
-
- public function testSearchUnread() {
- $this->assertEquals('UNSEEN', $this->search('is:unread'));
- }
-
- public function testSearchNotAnswered() {
- $this->assertEquals('UNANSWERED', $this->search('not:answered'));
- }
-
- public function testSearchFrom() {
- $this->assertEquals('FROM somebody@example.com', $this->search('from:somebody@example.com'));
- }
-
- public function testSearchMixed() {
- $this->assertEquals('UNSEEN FROM somebody@example.com TEXT nextcloud', $this->search('from:somebody@example.com is:unread nextcloud'));
- }
-}
diff --git a/tests/Unit/IMAP/Search/SearchStrategyFactoryTest.php b/tests/Unit/IMAP/Search/SearchStrategyFactoryTest.php
deleted file mode 100644
index 5f926125b..000000000
--- a/tests/Unit/IMAP/Search/SearchStrategyFactoryTest.php
+++ /dev/null
@@ -1,152 +0,0 @@
-<?php declare(strict_types=1);
-
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-namespace OCA\Mail\Tests\Unit\IMAP\Search;
-
-use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Data_Capability;
-use Horde_Imap_Client_Search_Query;
-use Horde_Imap_Client_Socket;
-use OCA\Mail\IMAP\Search\FullScanSearchStrategy;
-use OCA\Mail\IMAP\Search\ImapSortSearchStrategy;
-use OCA\Mail\IMAP\Search\SearchStrategyFactory;
-use PHPUnit\Framework\MockObject\MockObject;
-
-class SearchStrategyFactoryTest extends TestCase {
-
- /** @var SearchStrategyFactory */
- private $factory;
-
- protected function setUp(): void {
- parent::setUp();
-
- $this->factory = new SearchStrategyFactory();
- }
-
- public function testGetStrategyForFetchNoFilter() {
- /** @var MockObject|Horde_Imap_Client_Socket $client */
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $mailbox = 'INBOX';
- $filter = new Horde_Imap_Client_Search_Query();
- $cursor = null;
- $capability = $this->createMock(Horde_Imap_Client_Data_Capability::class);
- $client->expects($this->once())
- ->method('__get')
- ->with('capability')
- ->willReturn($capability);
- $capability->expects($this->once())
- ->method('query')
- ->with('SORT')
- ->willReturn(true);
-
- $strategy = $this->factory->getStrategy(
- $client,
- $mailbox,
- $filter,
- $cursor
- );
-
- $this->assertInstanceOf(ImapSortSearchStrategy::class, $strategy);
- }
-
- public function testGetStrategyForFetchNoSort() {
- /** @var MockObject|Horde_Imap_Client_Socket $client */
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $mailbox = 'INBOX';
- $filter = new Horde_Imap_Client_Search_Query();
- $cursor = null;
- $capability = $this->createMock(Horde_Imap_Client_Data_Capability::class);
- $client->expects($this->once())
- ->method('__get')
- ->with('capability')
- ->willReturn($capability);
- $capability->expects($this->once())
- ->method('query')
- ->with('SORT')
- ->willReturn(false);
-
- $strategy = $this->factory->getStrategy(
- $client,
- $mailbox,
- $filter,
- $cursor
- );
-
- $this->assertInstanceOf(FullScanSearchStrategy::class, $strategy);
- }
-
- public function testGetStrategyForSearchWithSort() {
- /** @var MockObject|Horde_Imap_Client_Socket $client */
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $mailbox = 'INBOX';
- $filter = new Horde_Imap_Client_Search_Query();
- $filter->text('test');
- $cursor = null;
- $capability = $this->createMock(Horde_Imap_Client_Data_Capability::class);
- $client->expects($this->once())
- ->method('__get')
- ->with('capability')
- ->willReturn($capability);
- $capability->expects($this->once())
- ->method('query')
- ->with('SORT')
- ->willReturn(true);
-
- $strategy = $this->factory->getStrategy(
- $client,
- $mailbox,
- $filter,
- $cursor
- );
-
- $this->assertInstanceOf(ImapSortSearchStrategy::class, $strategy);
- }
-
- public function testGetStrategyForSearchWithoutSort() {
- /** @var MockObject|Horde_Imap_Client_Socket $client */
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $mailbox = 'INBOX';
- $filter = new Horde_Imap_Client_Search_Query();
- $filter->text('sort');
- $cursor = null;
- $capability = $this->createMock(Horde_Imap_Client_Data_Capability::class);
- $client->expects($this->once())
- ->method('__get')
- ->with('capability')
- ->willReturn($capability);
- $capability->expects($this->once())
- ->method('query')
- ->with('SORT')
- ->willReturn(true);
-
- $strategy = $this->factory->getStrategy(
- $client,
- $mailbox,
- $filter,
- $cursor
- );
-
- $this->assertInstanceOf(ImapSortSearchStrategy::class, $strategy);
- }
-
-}
diff --git a/tests/Unit/IMAP/Sync/ResponseTest.php b/tests/Unit/IMAP/Sync/ResponseTest.php
index 6d2299aa1..892324abb 100644
--- a/tests/Unit/IMAP/Sync/ResponseTest.php
+++ b/tests/Unit/IMAP/Sync/ResponseTest.php
@@ -30,13 +30,11 @@ class ResponseTest extends TestCase {
$newMessages = [];
$changedMessages = [];
$vanishedMessages = [];
- $syncToken = 'bc4564';
- $response = new Response($syncToken, $newMessages, $changedMessages, $vanishedMessages);
+ $response = new Response($newMessages, $changedMessages, $vanishedMessages);
$expected = [
'newMessages' => [],
'changedMessages' => [],
'vanishedMessages' => [],
- 'token' => $syncToken,
];
$json = $response->jsonSerialize();
diff --git a/tests/Unit/IMAP/Sync/SimpleMailboxSyncTest.php b/tests/Unit/IMAP/Sync/SimpleMailboxSyncTest.php
index 59aa4e2ba..455e89324 100644
--- a/tests/Unit/IMAP/Sync/SimpleMailboxSyncTest.php
+++ b/tests/Unit/IMAP/Sync/SimpleMailboxSyncTest.php
@@ -101,7 +101,7 @@ class SimpleMailboxSyncTest extends TestCase {
$this->hordeSync->vanisheduids = $this->createMock(Horde_Imap_Client_Ids::class);
$this->hordeSync->vanisheduids->ids = [23, 24];
- $ids = $this->sync->getVanishedMessages($this->imapClient, $this->syncRequest, $this->hordeSync);
+ $ids = $this->sync->getVanishedMessageUids($this->imapClient, $this->syncRequest, $this->hordeSync);
$this->assertEquals([23, 24], $ids);
}
diff --git a/tests/Unit/IMAP/Sync/SynchronizerTest.php b/tests/Unit/IMAP/Sync/SynchronizerTest.php
index 9c83d654a..b754822e8 100644
--- a/tests/Unit/IMAP/Sync/SynchronizerTest.php
+++ b/tests/Unit/IMAP/Sync/SynchronizerTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
@@ -30,14 +30,14 @@ use OCA\Mail\IMAP\Sync\Request;
use OCA\Mail\IMAP\Sync\Response;
use OCA\Mail\IMAP\Sync\SimpleMailboxSync;
use OCA\Mail\IMAP\Sync\Synchronizer;
-use PHPUnit_Framework_MockObject_MockObject;
+use PHPUnit\Framework\MockObject\MockObject;
class SynchronizerTest extends TestCase {
- /** @var SimpleMailboxSync|PHPUnit_Framework_MockObject_MockObject */
+ /** @var SimpleMailboxSync|MockObject */
private $simpleSync;
- /** @var FavouritesMailboxSync|PHPUnit_Framework_MockObject_MockObject */
+ /** @var FavouritesMailboxSync|MockObject */
private $favSync;
/** @var Synchronizer */
@@ -83,7 +83,7 @@ class SynchronizerTest extends TestCase {
->willReturn($flagged);
$newMessages = [];
$changedMessages = [];
- $vanishedMessages = [4, 5];
+ $vanishedMessageUids = [4, 5];
$sync->expects($this->once())
->method('getNewMessages')
->with($imapClient, $request, $hordeSync)
@@ -93,14 +93,10 @@ class SynchronizerTest extends TestCase {
->with($imapClient, $request, $hordeSync)
->willReturn($changedMessages);
$sync->expects($this->once())
- ->method('getVanishedMessages')
+ ->method('getVanishedMessageUids')
->with($imapClient, $request, $hordeSync)
- ->willReturn($vanishedMessages);
- $imapClient->expects($this->once())
- ->method('getSyncToken')
- ->with($this->equalTo('inbox'))
- ->willReturn('54321');
- $expected = new Response('54321', $newMessages, $changedMessages, $vanishedMessages);
+ ->willReturn($vanishedMessageUids);
+ $expected = new Response($newMessages, $changedMessages, $vanishedMessageUids);
$response = $this->synchronizer->sync($imapClient, $request);
diff --git a/tests/Unit/Service/AccountServiceTest.php b/tests/Unit/Service/AccountServiceTest.php
index a07be412a..9289b70c5 100644
--- a/tests/Unit/Service/AccountServiceTest.php
+++ b/tests/Unit/Service/AccountServiceTest.php
@@ -27,6 +27,7 @@ use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AliasesService;
+use OCP\BackgroundJob\IJobList;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
@@ -53,15 +54,20 @@ class AccountServiceTest extends TestCase {
/** @var MailAccount|MockObject */
private $account2;
+ /** @var IJobList|MockObject */
+ private $jobList;
+
protected function setUp(): void {
parent::setUp();
$this->mapper = $this->createMock(MailAccountMapper::class);
$this->l10n = $this->createMock(IL10N::class);
$this->aliasesService = $this->createMock(AliasesService::class);
+ $this->jobList = $this->createMock(IJobList::class);
$this->accountService = new AccountService(
$this->mapper,
- $this->aliasesService
+ $this->aliasesService,
+ $this->jobList
);
$this->account1 = $this->createMock(MailAccount::class);
@@ -73,9 +79,9 @@ class AccountServiceTest extends TestCase {
->method('findByUserId')
->with($this->user)
->will($this->returnValue([
- $this->account1,
- $this->account2,
- ]));
+ $this->account1,
+ $this->account2,
+ ]));
$expected = [
new Account($this->account1),
diff --git a/tests/Unit/Service/MailManagerTest.php b/tests/Unit/Service/MailManagerTest.php
index f6bd644b2..11a11e698 100644
--- a/tests/Unit/Service/MailManagerTest.php
+++ b/tests/Unit/Service/MailManagerTest.php
@@ -59,9 +59,6 @@ class MailManagerTest extends TestCase {
/** @var MessageMapper|MockObject */
private $messageMapper;
- /** @var Synchronizer|MockObject */
- private $sync;
-
/** @var IEventDispatcher|MockObject */
private $eventDispatcher;
@@ -73,10 +70,9 @@ class MailManagerTest extends TestCase {
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
- $this->mailboxSync = $this->createMock(MailboxSync::class);
$this->folderMapper = $this->createMock(FolderMapper::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
- $this->sync = $this->createMock(Synchronizer::class);
+ $this->mailboxSync = $this->createMock(MailboxSync::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->manager = new MailManager(
@@ -84,7 +80,6 @@ class MailManagerTest extends TestCase {
$this->mailboxMapper,
$this->mailboxSync,
$this->folderMapper,
- $this->sync,
$this->messageMapper,
$this->eventDispatcher
);
@@ -156,22 +151,6 @@ class MailManagerTest extends TestCase {
$this->assertEquals($stats, $actual);
}
- public function testSync() {
- $account = $this->createMock(Account::class);
- $syncRequest = $this->createMock(Request::class);
- $syncResonse = $this->createMock(Response::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
- $this->sync->expects($this->once())
- ->method('sync')
- ->with($client, $syncRequest)
- ->willReturn($syncResonse);
-
- $this->manager->syncMessages($account, $syncRequest);
- }
-
public function testDeleteMessageSourceFolderNotFound(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
diff --git a/tests/Unit/Service/MailSearchTest.php b/tests/Unit/Service/MailSearchTest.php
index fc4483307..2b5006ef4 100644
--- a/tests/Unit/Service/MailSearchTest.php
+++ b/tests/Unit/Service/MailSearchTest.php
@@ -28,23 +28,18 @@ use Horde_Imap_Client_Fetch_Results;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\MessageMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\Search\SearchFilterStringParser;
-use OCA\Mail\IMAP\Search\SearchStrategyFactory;
-use OCA\Mail\Service\MailSearch;
+use OCA\Mail\IMAP\Search\Provider;
+use OCA\Mail\Service\Search\FilterStringParser;
+use OCA\Mail\Service\Search\MailSearch;
use OCP\ILogger;
use PHPUnit\Framework\MockObject\MockObject;
class MailSearchTest extends TestCase {
- /** @var MockObject|IMAPClientFactory */
- private $imapClientFactory;
-
- /** @var MockObject|SearchStrategyFactory */
- private $searchStrategyFactory;
-
- /** @var MockObject|SearchFilterStringParser */
- private $searchStringParser;
+ /** @var FilterStringParser|MockObject */
+ private $filterStringParser;
/** @var MockObject|MailboxMapper */
private $mailboxMapper;
@@ -55,34 +50,32 @@ class MailSearchTest extends TestCase {
/** @var MailSearch */
private $search;
+ /** @var Provider|MockObject */
+ private $imapSearchProvider;
+
+ /** @var MessageMapper|MockObject */
+ private $messageMapper;
+
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
- $this->searchStrategyFactory = $this->createMock(SearchStrategyFactory::class);
- $this->searchStringParser = $this->createMock(SearchFilterStringParser::class);
+ $this->filterStringParser = $this->createMock(FilterStringParser::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
+ $this->imapSearchProvider = $this->createMock(Provider::class);
+ $this->messageMapper = $this->createMock(MessageMapper::class);
$this->logger = $this->createMock(ILogger::class);
$this->search = new MailSearch(
- $this->imapClientFactory,
- $this->searchStrategyFactory,
- $this->searchStringParser,
+ $this->filterStringParser,
$this->mailboxMapper,
+ $this->imapSearchProvider,
+ $this->messageMapper,
$this->logger
);
}
public function testNoFindMessages() {
$account = $this->createMock(Account::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->with($account)
- ->willReturn($client);
- $client->expects($this->once())
- ->method('fetch')
- ->willReturn(new Horde_Imap_Client_Fetch_Results());
$messages = $this->search->findMessages(
$account,