Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor imap search #2058

Merged
merged 1 commit into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/AppInfo/BootstrapSingleton.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCA\Mail\Contracts\IAttachmentService;
use OCA\Mail\Contracts\IAvatarService;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Contracts\IUserPreferences;
use OCA\Mail\Events\BeforeMessageDeletedEvent;
Expand All @@ -44,6 +45,7 @@
use OCA\Mail\Service\Group\IGroupService;
use OCA\Mail\Service\Group\NextcloudGroupService;
use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailSearch;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\UserPreferenceSevice;
use OCP\AppFramework\IAppContainer;
Expand Down Expand Up @@ -92,6 +94,7 @@ private function initializeAppContainer(IAppContainer $container) {
$container->registerAlias(IAvatarService::class, AvatarService::class);
$container->registerAlias(IAttachmentService::class, AttachmentService::class);
$container->registerAlias(IMailManager::class, MailManager::class);
$container->registerAlias(IMailSearch::class, MailSearch::class);
$container->registerAlias(IMailTransmission::class, MailTransmission::class);
$container->registerAlias(IUserPreferences::class, UserPreferenceSevice::class);

Expand Down
43 changes: 43 additions & 0 deletions lib/Contracts/IMailSearch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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\Contracts;

use OCA\Mail\Account;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Model\IMAPMessage;

interface IMailSearch {

/**
* @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;

}
50 changes: 34 additions & 16 deletions lib/Controller/MessagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Exception;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\AttachmentDownloadResponse;
use OCA\Mail\Http\HtmlResponse;
Expand All @@ -52,6 +53,7 @@
use OCP\ILogger;
use OCP\IRequest;
use OCP\IURLGenerator;
use function base64_decode;

class MessagesController extends Controller {

Expand All @@ -61,6 +63,9 @@ class MessagesController extends Controller {
/** @var IMailManager */
private $mailManager;

/** @var IMailSearch */
private $mailSearch;

/** @var string */
private $currentUserId;

Expand Down Expand Up @@ -101,6 +106,7 @@ public function __construct(string $appName,
IRequest $request,
AccountService $accountService,
IMailManager $mailManager,
IMailSearch $mailSearch,
string $UserId,
$userFolder,
ILogger $logger,
Expand All @@ -111,14 +117,15 @@ public function __construct(string $appName,
parent::__construct($appName, $request);

$this->accountService = $accountService;
$this->mailManager = $mailManager;
$this->mailSearch = $mailSearch;
$this->currentUserId = $UserId;
$this->userFolder = $userFolder;
$this->logger = $logger;
$this->l10n = $l10n;
$this->mimeTypeDetector = $mimeTypeDetector;
$this->urlGenerator = $urlGenerator;
$this->timeFactory = $timeFactory;
$this->mailManager = $mailManager;
}

/**
Expand All @@ -129,26 +136,26 @@ public function __construct(string $appName,
* @param string $folderId
* @param int $cursor
* @param string $filter
*
* @return JSONResponse
* @throws ServiceException
*/
public function index(int $accountId, string $folderId, int $cursor = null, string $filter = null): JSONResponse {
$mailBox = $this->getFolder($accountId, $folderId);

$this->logger->debug("loading messages of folder <$folderId>");

if ($cursor === '') {
$cursor = null;
$account = $this->getAccount($accountId);
if ($account === null) {
return new JSONResponse(null, Http::STATUS_FORBIDDEN);
}
$messages = $mailBox->getMessages($filter, $cursor);

$json = array_map(function ($j) use ($mailBox) {
if ($mailBox->getSpecialRole() === 'trash') {
$j['delete'] = (string)$this->l10n->t('Delete permanently');
}
return $j;
}, $messages);
$this->logger->debug("loading messages of folder <$folderId>");

return new JSONResponse($json);
return new JSONResponse(
$this->mailSearch->findMessages(
$account,
base64_decode($folderId),
$filter === '' ? null : $filter,
$cursor
)
);
}

/**
Expand All @@ -158,6 +165,7 @@ public function index(int $accountId, string $folderId, int $cursor = null, stri
* @param int $accountId
* @param string $folderId
* @param int $id
*
* @return JSONResponse
* @throws ServiceException
*/
Expand Down Expand Up @@ -193,6 +201,7 @@ public function show(int $accountId, string $folderId, int $id): JSONResponse {
* @param int $id
* @param int $destAccountId
* @param string $destFolderId
*
* @return JSONResponse
* @throws Exception
*/
Expand Down Expand Up @@ -270,6 +279,7 @@ public function getHtmlBody(int $accountId, string $folderId, int $messageId): R
* @param string $folderId
* @param int $messageId
* @param int $attachmentId
*
* @return AttachmentDownloadResponse
*/
public function downloadAttachment(int $accountId, string $folderId, int $messageId,
Expand All @@ -291,6 +301,7 @@ public function downloadAttachment(int $accountId, string $folderId, int $messag
* @param int $messageId
* @param int $attachmentId
* @param string $targetPath
*
* @return JSONResponse
*/
public function saveAttachment(int $accountId, string $folderId, int $messageId,
Expand Down Expand Up @@ -336,6 +347,7 @@ public function saveAttachment(int $accountId, string $folderId, int $messageId,
* @param string $folderId
* @param string $messageId
* @param array $flags
*
* @return JSONResponse
*/
public function setFlags(int $accountId, string $folderId, int $messageId, array $flags): JSONResponse {
Expand Down Expand Up @@ -375,9 +387,12 @@ public function destroy(int $accountId, string $folderId, int $id): JSONResponse

/**
* @param int $accountId
*
* @return Account|null
* @todo add caching to \OCA\Mail\Service\AccountService
* @deprecated use \OCA\Mail\Service\AccountService::find
*/
private function getAccount($accountId) {
private function getAccount($accountId): ?Account {
if (!array_key_exists($accountId, $this->accounts)) {
$this->accounts[$accountId] = $this->accountService->find(
$this->currentUserId,
Expand All @@ -390,6 +405,7 @@ private function getAccount($accountId) {
/**
* @param int $accountId
* @param string $folderId
*
* @return IMailBox
*/
private function getFolder(int $accountId, string $folderId): IMailBox {
Expand All @@ -402,6 +418,7 @@ private function getFolder(int $accountId, string $folderId): IMailBox {
* @param string $folderId
* @param int $messageId
* @param array $attachment
*
* @return array
*/
private function enrichDownloadUrl(int $accountId, string $folderId, int $messageId,
Expand Down Expand Up @@ -441,6 +458,7 @@ private function attachmentIsImage(array $attachment): bool {

/**
* @param array $attachment
*
* @return boolean
*/
private function attachmentIsCalendarEvent(array $attachment): bool {
Expand Down
33 changes: 32 additions & 1 deletion lib/Db/Mailbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@

namespace OCA\Mail\Db;

use Horde_Imap_Client;
use OCA\Mail\Folder;
use OCP\AppFramework\Db\Entity;
use function in_array;
use function json_decode;
use function ltrim;
use function rtrim;
use function strtolower;

/**
* @method string getName()
Expand Down Expand Up @@ -73,10 +79,35 @@ public function toFolder(): Folder {
$this->delimiter
);
$folder->setSyncToken($this->getSyncToken());
foreach ((json_decode($this->getSpecialUse() ?? '[]', true) ?? []) as $use) {
foreach ($this->getSpecialUseParsed() as $use) {
$folder->addSpecialUse($use);
}
return $folder;
}

private function getSpecialUseParsed(): array {
return json_decode($this->getSpecialUse() ?? '[]', true) ?? [];
}

public function isSpecialUse(string $specialUse): bool {
return in_array(
ltrim(
strtolower($specialUse),
'\\'
),
array_map("strtolower", $this->getSpecialUseParsed()),
true
);
}

public function getMailbox(): string {
// TODO: evaluate if SPECIALUSE_FLAGGED can also be set on a real IMAP mailbox
// because then this could be problematic if they name also ends with /FLAGGED
// though, this sounds very unlikely
if ($this->isSpecialUse(Horde_Imap_Client::SPECIALUSE_FLAGGED)) {
return rtrim($this->getName(), '/FLAGGED');
}
return $this->getName();
}

}
86 changes: 86 additions & 0 deletions lib/IMAP/Search/FullScanSearchStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?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
)
);
}

}
Loading