diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ec54a7cb3d..e886c80c17 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -171,8 +171,15 @@ jobs:
image: stalwartlabs/stalwart:v0.15.5
env:
STALWART_ADMIN_PASSWORD: ${{ env.STALWART_PWD }}
+ # Pin a stable container hostname. Stalwart derives the authority of
+ # its published JMAP discovery URLs (apiUrl/downloadUrl/uploadUrl) from
+ # the container hostname + internal port, which is otherwise a random
+ # container id the runner cannot resolve. With a known hostname we can
+ # map it to loopback via /etc/hosts and publish the internal port 1:1
+ # so the advertised http://stalwart:8080/jmap/ is reachable.
+ options: --hostname stalwart
ports:
- - 10080:8080
+ - 8080:8080
- 10025:25
- 10143:143
- 10993:993
@@ -213,13 +220,15 @@ jobs:
ports:
- 6379:6379
steps:
+ - name: Map Stalwart hostname to loopback
+ run: echo "127.0.0.1 stalwart" | sudo tee -a /etc/hosts
- name: Create domain and account in Stalwart
run: |
- curl -sf -X POST http://localhost:10080/api/principal \
+ curl -sf -X POST http://stalwart:8080/api/principal \
-u "admin:${{ env.STALWART_PWD }}" \
-H 'Content-Type: application/json' \
-d '{"type":"domain","name":"example.com"}'
- curl -sf -X POST http://localhost:10080/api/principal \
+ curl -sf -X POST http://stalwart:8080/api/principal \
-u "admin:${{ env.STALWART_PWD }}" \
-H 'Content-Type: application/json' \
-d '{"type":"individual","name":"user@example.com","secrets":["mypassword"],"emails":["user@example.com"],"roles":["user"]}'
diff --git a/composer.json b/composer.json
index 54f72197db..df6dabe78d 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,7 @@
"phpmailer/dkimvalidator": "^0.3.1",
"rubix/ml": "2.5.5",
"sabberworm/php-css-parser": "^9.4.0",
- "sebastiankrupinski/jmap-client-php": "^2.0.0",
+ "sebastiankrupinski/jmap-client-php": "^2.1.0",
"wamania/php-stemmer": "4.0 as 3.0",
"youthweb/urllinker": "^2.1.0"
},
diff --git a/composer.lock b/composer.lock
index ddfc6823d8..de8926ff86 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b5da21087a50ee288a4a2337ec4fdf11",
+ "content-hash": "09b1b7f1bc1fa81ff19f79ec444faf95",
"packages": [
{
"name": "amphp/amp",
@@ -3064,16 +3064,16 @@
},
{
"name": "sebastiankrupinski/jmap-client-php",
- "version": "v2.0.0",
+ "version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/SebastianKrupinski/jmap-client-php.git",
- "reference": "720ba6ee0579202e5fedab39847680cc5c7ebf58"
+ "reference": "a681cf6f0d79d236e02a3d065b820523000f4577"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SebastianKrupinski/jmap-client-php/zipball/720ba6ee0579202e5fedab39847680cc5c7ebf58",
- "reference": "720ba6ee0579202e5fedab39847680cc5c7ebf58",
+ "url": "https://api.github.com/repos/SebastianKrupinski/jmap-client-php/zipball/a681cf6f0d79d236e02a3d065b820523000f4577",
+ "reference": "a681cf6f0d79d236e02a3d065b820523000f4577",
"shasum": ""
},
"require": {
@@ -3123,7 +3123,7 @@
"issues": "https://github.com/SebastianKrupinski/jmap-client-php/issues",
"source": "https://github.com/SebastianKrupinski/jmap-client-php"
},
- "time": "2026-06-27T23:55:59+00:00"
+ "time": "2026-08-10T21:06:57+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -4373,5 +4373,5 @@
"platform-overrides": {
"php": "8.1"
},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 6af8cf630d..4f7c58ce61 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -17,7 +17,6 @@
use OCA\Mail\Contracts\IAvatarService;
use OCA\Mail\Contracts\IDkimService;
use OCA\Mail\Contracts\IDkimValidator;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Contracts\ITrustedSenderService;
@@ -63,7 +62,6 @@
use OCA\Mail\Service\AvatarService;
use OCA\Mail\Service\DkimService;
use OCA\Mail\Service\DkimValidator;
-use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\Search\MailSearch;
use OCA\Mail\Service\TrustedSenderService;
@@ -121,7 +119,6 @@ public function register(IRegistrationContext $context): void {
$context->registerServiceAlias(IAvatarService::class, AvatarService::class);
$context->registerServiceAlias(IAttachmentService::class, AttachmentService::class);
- $context->registerServiceAlias(IMailManager::class, MailManager::class);
$context->registerServiceAlias(IMailSearch::class, MailSearch::class);
$context->registerServiceAlias(IMailTransmission::class, MailTransmission::class);
$context->registerServiceAlias(ITrustedSenderService::class, TrustedSenderService::class);
diff --git a/lib/BackgroundJob/ContextChat/SubmitContentJob.php b/lib/BackgroundJob/ContextChat/SubmitContentJob.php
index 11619b92fe..698881800a 100644
--- a/lib/BackgroundJob/ContextChat/SubmitContentJob.php
+++ b/lib/BackgroundJob/ContextChat/SubmitContentJob.php
@@ -13,9 +13,9 @@
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\SmimeDecryptException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\ContextChat\TaskService;
use OCA\Mail\Service\MailManager;
@@ -36,7 +36,6 @@ public function __construct(
private AccountService $accountService,
private MailManager $mailManager,
private MessageMapper $messageMapper,
- private IMAPClientFactory $clientFactory,
private ContextChatProvider $contextChatProvider,
private IContentManager $contentManager,
private LoggerInterface $logger,
@@ -108,50 +107,39 @@ protected function run($argument): void {
return;
}
- $client = $this->clientFactory->getClient($account);
$items = [];
- try {
- $startTime = $this->time->getTime();
- foreach ($messages as $message) {
- if ($this->time->getTime() - $startTime > ContextChatProvider::CONTEXT_CHAT_JOB_INTERVAL) {
- break;
- }
- try {
- $imapMessage = $this->mailManager->getImapMessage($client, $account, $mailbox, $message->getUid(), true);
- } catch (ServiceException $e) {
- // couldn't load message, let's skip it. Retrying would be too costly
- continue;
- } catch (SmimeDecryptException $e) {
- // encryption problem, skip this message
- continue;
- }
-
- // Skip encrypted messages
- if ($imapMessage->isEncrypted()) {
- continue;
- }
-
- $fullMessage = $imapMessage->getFullMessage($imapMessage->getUid(), true);
-
- $items[] = new ContentItem(
- "{$mailbox->getId()}:{$message->getId()}",
- $this->contextChatProvider->getId(),
- $imapMessage->getSubject(),
- $fullMessage['body'] ?? '',
- 'E-Mail',
- $imapMessage->getSentDate(),
- [$account->getUserId()],
- );
+ $startTime = $this->time->getTime();
+ foreach ($messages as $message) {
+ if ($this->time->getTime() - $startTime > ContextChatProvider::CONTEXT_CHAT_JOB_INTERVAL) {
+ break;
}
- } catch (\Throwable $e) {
- $this->logger->warning('Exception occurred when trying to fetch messages for context chat', ['exception' => $e]);
- } finally {
try {
- $client->close();
- } catch (\Horde_Imap_Client_Exception $e) {
- $this->logger->debug('Failed to close IMAP client', ['exception' => $e]);
+ $imapMessage = $this->mailManager->getImapMessage($account, $mailbox, $message, true);
+ } catch (ServiceException|ClientException $e) {
+ // couldn't load message, let's skip it. Retrying would be too costly
+ continue;
+ } catch (SmimeDecryptException $e) {
+ // encryption problem, skip this message
+ continue;
+ }
+
+ // Skip encrypted messages
+ if ($imapMessage->isEncrypted()) {
+ continue;
}
+
+ $fullMessage = $imapMessage->getFullMessage($imapMessage->getUid(), true);
+
+ $items[] = new ContentItem(
+ "{$mailbox->getId()}:{$message->getId()}",
+ $this->contextChatProvider->getId(),
+ $imapMessage->getSubject(),
+ $fullMessage['body'] ?? '',
+ 'E-Mail',
+ $imapMessage->getSentDate(),
+ [$account->getUserId()],
+ );
}
if (count($items) > 0) {
diff --git a/lib/BackgroundJob/FollowUpClassifierJob.php b/lib/BackgroundJob/FollowUpClassifierJob.php
index 0169aaf8f9..97fb5086c9 100644
--- a/lib/BackgroundJob/FollowUpClassifierJob.php
+++ b/lib/BackgroundJob/FollowUpClassifierJob.php
@@ -9,13 +9,13 @@
namespace OCA\Mail\BackgroundJob;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\ThreadMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\DB\Exception;
@@ -31,7 +31,7 @@ public function __construct(
ITimeFactory $time,
private LoggerInterface $logger,
private AccountService $accountService,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private AiIntegrationsService $aiService,
private ThreadMapper $threadMapper,
) {
@@ -104,12 +104,12 @@ public function run($argument): void {
$this->logger->debug("Message requires follow-up: {$message->getId()}");
$tag = $this->mailManager->createTag('Follow up', '#d77000', $userId);
- $this->mailManager->tagMessage(
+ $this->mailManager->tagMessages(
$account,
- $mailbox->getName(),
- $message,
+ $mailbox,
$tag,
true,
+ $message,
);
}
}
diff --git a/lib/BackgroundJob/MigrateImportantJob.php b/lib/BackgroundJob/MigrateImportantJob.php
index c2cfaa2c71..59a86aafec 100644
--- a/lib/BackgroundJob/MigrateImportantJob.php
+++ b/lib/BackgroundJob/MigrateImportantJob.php
@@ -13,8 +13,8 @@
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Migration\MigrateImportantFromImapAndDb;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -29,7 +29,7 @@ public function __construct(
private MigrateImportantFromImapAndDb $migration,
private LoggerInterface $logger,
ITimeFactory $timeFactory,
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
) {
parent::__construct($timeFactory);
}
@@ -58,10 +58,10 @@ public function run($argument) {
}
$account = new Account($mailAccount);
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
- if ($this->mailManager->isPermflagsEnabled($client, $account, $mailbox->getName()) === false) {
+ if ($this->mailManager->isPermflagsEnabled($account, $mailbox) === false) {
$this->logger->debug("Permflags not enabled for <{$accountId}>");
return;
}
diff --git a/lib/BackgroundJob/QuotaJob.php b/lib/BackgroundJob/QuotaJob.php
index 7b03ee91f9..69ed0dd6dd 100644
--- a/lib/BackgroundJob/QuotaJob.php
+++ b/lib/BackgroundJob/QuotaJob.php
@@ -8,8 +8,8 @@
namespace OCA\Mail\BackgroundJob;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
@@ -28,7 +28,7 @@ public function __construct(
ITimeFactory $time,
IUserManager $userManager,
private AccountService $accountService,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
IManager $notificationManager,
private LoggerInterface $logger,
IJobList $jobList,
diff --git a/lib/BackgroundJob/RepairSyncJob.php b/lib/BackgroundJob/RepairSyncJob.php
index 4d843af8d3..c7c614d05d 100644
--- a/lib/BackgroundJob/RepairSyncJob.php
+++ b/lib/BackgroundJob/RepairSyncJob.php
@@ -9,8 +9,10 @@
namespace OCA\Mail\BackgroundJob;
+use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Events\SynchronizationEvent;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Sync\SyncService;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -25,6 +27,7 @@ class RepairSyncJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private SyncService $syncService,
+ private ProtocolFactory $protocolFactory,
private AccountService $accountService,
private IUserManager $userManager,
private MailboxMapper $mailboxMapper,
@@ -55,6 +58,15 @@ protected function run($argument): void {
return;
}
+ if ($account->getMailAccount()->getProtocol() !== MailAccount::PROTOCOL_IMAP) {
+ $this->logger->debug(sprintf(
+ 'Account %d uses %s, skipping IMAP repair sync after mailbox refresh',
+ $account->getId(),
+ $account->getMailAccount()->getProtocol(),
+ ));
+ return;
+ }
+
$user = $this->userManager->get($account->getUserId());
if ($user === null || !$user->isEnabled()) {
$this->logger->debug(sprintf(
@@ -65,6 +77,10 @@ protected function run($argument): void {
return;
}
+ $this->protocolFactory
+ ->mailboxConnector($account)
+ ->syncAll($account, true);
+
$rebuildThreads = false;
$trashMailboxId = $account->getMailAccount()->getTrashMailboxId();
$snoozeMailboxId = $account->getMailAccount()->getSnoozeMailboxId();
diff --git a/lib/BackgroundJob/SyncJob.php b/lib/BackgroundJob/SyncJob.php
index 52af3f345f..beeb6624a6 100644
--- a/lib/BackgroundJob/SyncJob.php
+++ b/lib/BackgroundJob/SyncJob.php
@@ -10,11 +10,11 @@
use Horde_Imap_Client_Exception;
use OCA\Mail\AppInfo\Application;
+use OCA\Mail\Db\MailAccount;
use OCA\Mail\Exception\IncompleteSyncException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\MailboxSync;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
-use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
@@ -38,8 +38,7 @@ public function __construct(
ITimeFactory $time,
IUserManager $userManager,
private AccountService $accountService,
- private MailboxSync $mailboxSync,
- private ImapToDbSynchronizer $syncService,
+ private ProtocolFactory $protocolFactory,
private LoggerInterface $logger,
IJobList $jobList,
private readonly IConfig $config,
@@ -87,7 +86,8 @@ protected function run($argument) {
return;
}
- if (!$account->getMailAccount()->canAuthenticateImap()) {
+ if ($account->getMailAccount()->getProtocol() === MailAccount::PROTOCOL_IMAP
+ && !$account->getMailAccount()->canAuthenticateImap()) {
$this->logger->debug('No authentication on IMAP possible, skipping background sync job');
return;
}
@@ -128,8 +128,12 @@ protected function run($argument) {
}
try {
- $this->mailboxSync->sync($account, $this->logger, true);
- $this->syncService->syncAccount($account, $this->logger);
+ $this->protocolFactory
+ ->mailboxConnector($account)
+ ->syncAll($account, true);
+ $this->protocolFactory
+ ->messageConnector($account)
+ ->syncAll($account, false);
} catch (IncompleteSyncException $e) {
$this->logger->warning($e->getMessage(), [
'exception' => $e,
diff --git a/lib/BackgroundJob/TrashRetentionJob.php b/lib/BackgroundJob/TrashRetentionJob.php
index e3e1bd3e0f..0d5fe6abf1 100644
--- a/lib/BackgroundJob/TrashRetentionJob.php
+++ b/lib/BackgroundJob/TrashRetentionJob.php
@@ -10,14 +10,13 @@
namespace OCA\Mail\BackgroundJob;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Db\MessageRetentionMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
@@ -27,12 +26,11 @@ class TrashRetentionJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private LoggerInterface $logger,
- private IMAPClientFactory $clientFactory,
private MessageMapper $messageMapper,
private MessageRetentionMapper $messageRetentionMapper,
private MailAccountMapper $accountMapper,
private MailboxMapper $mailboxMapper,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
) {
parent::__construct($time);
@@ -96,22 +94,16 @@ private function cleanTrash(Account $account, int $retentionSeconds): void {
return;
}
- $client = $this->clientFactory->getClient($account);
- try {
- foreach ($messages as $message) {
- $this->mailManager->deleteMessageWithClient(
- $account,
- $trashMailbox,
- $message->getUid(),
- $client,
- );
- $this->messageRetentionMapper->deleteByMailboxIdAndUid(
- $message->getMailboxId(),
- $message->getUid(),
- );
- }
- } finally {
- $client->logout();
+ foreach ($messages as $message) {
+ $this->mailManager->deleteMessage(
+ $account,
+ $trashMailbox,
+ $message,
+ );
+ $this->messageRetentionMapper->deleteByMailboxIdAndUid(
+ $message->getMailboxId(),
+ $message->getUid(),
+ );
}
}
}
diff --git a/lib/Command/SyncAccount.php b/lib/Command/SyncAccount.php
index cb91217cb9..7246b6a17e 100644
--- a/lib/Command/SyncAccount.php
+++ b/lib/Command/SyncAccount.php
@@ -12,10 +12,8 @@
use OCA\Mail\Account;
use OCA\Mail\Exception\IncompleteSyncException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\MailboxSync;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
-use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCA\Mail\Support\ConsoleLoggerDecorator;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
@@ -33,10 +31,8 @@ final class SyncAccount extends Command {
public function __construct(
private AccountService $accountService,
- private MailboxSync $mailboxSync,
- private ImapToDbSynchronizer $syncService,
+ private ProtocolFactory $protocolFactory,
private LoggerInterface $logger,
- private IMAPClientFactory $clientFactory,
) {
parent::__construct();
}
@@ -46,7 +42,7 @@ public function __construct(
*/
protected function configure() {
$this->setName('mail:account:sync');
- $this->setDescription('Synchronize an IMAP account');
+ $this->setDescription('Synchronize a mail account');
$this->addArgument(self::ARGUMENT_ACCOUNT_ID, InputArgument::REQUIRED);
$this->addOption(self::OPTION_FORCE, 'f', InputOption::VALUE_NONE);
}
@@ -78,8 +74,8 @@ private function sync(Account $account, bool $force, OutputInterface $output): v
);
try {
- $this->mailboxSync->sync($account, $consoleLogger, $force);
- $this->syncService->syncAccount($account, $consoleLogger, $force);
+ $this->protocolFactory->mailboxConnector($account)->syncAll($account, $force);
+ $this->protocolFactory->messageConnector($account)->syncAll($account, $force);
} catch (ServiceException $e) {
if (!($e instanceof IncompleteSyncException)) {
throw $e;
@@ -89,9 +85,5 @@ private function sync(Account $account, bool $force, OutputInterface $output): v
$output->writeln("Batch of new messages sync'ed. " . $mbs . 'MB of memory in use');
$this->sync($account, $force, $output);
}
-
- foreach ($this->clientFactory->getLoginStats() as $host => $count) {
- $consoleLogger->debug(sprintf('%d IMAP connection(s) to %s', $count, $host));
- }
}
}
diff --git a/lib/Command/TestAccount.php b/lib/Command/TestAccount.php
index 32abbba9dd..8f11e7f309 100644
--- a/lib/Command/TestAccount.php
+++ b/lib/Command/TestAccount.php
@@ -9,23 +9,56 @@
namespace OCA\Mail\Command;
+use Horde_Imap_Client;
use Horde_Imap_Client_Exception;
+use Horde_Imap_Client_Ids;
+use OCA\Mail\Account;
+use OCA\Mail\AddressList;
use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\Message;
+use OCA\Mail\IMAP\FolderMapper;
+use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
+use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\JMAP\JmapOperationsService;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
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;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use function array_keys;
+use function array_slice;
+use function array_values;
+use function count;
+use function date;
+use function in_array;
+use function json_decode;
+use function max;
+use function mb_strimwidth;
+use function microtime;
+use function round;
+use function sort;
+use function strtolower;
+use function usort;
final class TestAccount extends Command {
private const ARGUMENT_ACCOUNT_ID = 'account-id';
+ private const OPTION_MAILBOX_LIMIT = 'mailboxes';
+ private const OPTION_MESSAGE_LIMIT = 'messages';
+ private const DEFAULT_MAILBOX_LIMIT = 10;
+ private const DEFAULT_MESSAGE_LIMIT = 5;
public function __construct(
private AccountService $accountService,
private ProtocolFactory $protocolFactory,
+ private FolderMapper $folderMapper,
+ private ImapMessageMapper $imapMessageMapper,
+ private JmapOperationsService $jmapOperationsService,
private LoggerInterface $logger,
) {
parent::__construct();
@@ -36,118 +69,414 @@ protected function configure(): void {
$this->setAliases(['mail:account:diagnose']);
$this->setDescription('Test the connection for a mail account (IMAP or JMAP)');
$this->addArgument(self::ARGUMENT_ACCOUNT_ID, InputArgument::REQUIRED, 'The ID of the mail account');
+ $this->addOption(self::OPTION_MAILBOX_LIMIT, null, InputOption::VALUE_OPTIONAL, 'Number of mailboxes to list', (string)self::DEFAULT_MAILBOX_LIMIT);
+ $this->addOption(self::OPTION_MESSAGE_LIMIT, null, InputOption::VALUE_OPTIONAL, 'Number of recent inbox messages to list', (string)self::DEFAULT_MESSAGE_LIMIT);
+ $this->setHelp(<<<'HELP'
+ The mail:account:test command checks connectivity for a stored
+ mail account and prints protocol-specific diagnostics, mailbox listings,
+ and a short inbox preview.
+
+ Examples:
+
+ Test an account by ID:
+ php occ mail:account:test 42
+
+ Limit the mailbox and inbox preview output:
+ php occ mail:account:test 42 --mailboxes=5 --messages=3
+
+ Use the legacy alias:
+ php occ mail:account:diagnose 42
+
+ The command detects whether the account uses IMAP or JMAP and prints the
+ corresponding endpoint, authentication context, latency, capabilities,
+ mailboxes, and recent inbox messages.
+ HELP);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
+ $io = new SymfonyStyle($input, $output);
$accountId = (int)$input->getArgument(self::ARGUMENT_ACCOUNT_ID);
+ $mailboxLimit = max(1, (int)$input->getOption(self::OPTION_MAILBOX_LIMIT));
+ $messageLimit = max(1, (int)$input->getOption(self::OPTION_MESSAGE_LIMIT));
try {
$account = $this->accountService->findById($accountId);
} catch (DoesNotExistException $e) {
- $output->writeln("Account $accountId does not exist");
- return 1;
+ $io->error("Account $accountId does not exist");
+ return self::FAILURE;
}
$protocol = $account->getMailAccount()->getProtocol();
- $output->writeln("Account $accountId uses protocol: $protocol");
+ $this->renderAccountSummary($account, $io);
return match ($protocol) {
- MailAccount::PROTOCOL_IMAP => $this->testImap($account, $output),
- MailAccount::PROTOCOL_JMAP => $this->testJmap($account, $output),
- default => $this->unsupportedProtocol($protocol, $output),
+ MailAccount::PROTOCOL_IMAP => $this->testImap($account, $io, $mailboxLimit, $messageLimit),
+ MailAccount::PROTOCOL_JMAP => $this->testJmap($account, $io, $mailboxLimit, $messageLimit),
+ default => $this->unsupportedProtocol($protocol, $io),
};
}
- private function testImap(\OCA\Mail\Account $account, OutputInterface $output): int {
- $output->writeln('Testing IMAP connection...');
+ private function renderAccountSummary(Account $account, SymfonyStyle $io): void {
+ $mailAccount = $account->getMailAccount();
+
+ $io->title('Mail Account Connection Test');
+ $io->definitionList(
+ ['Account ID' => (string)$account->getId()],
+ ['Email' => $account->getEmail()],
+ ['Name' => $account->getName()],
+ ['Protocol' => $mailAccount->getProtocol()],
+ ['Authentication' => $mailAccount->getAuthMethod()],
+ );
+ }
+
+ private function testImap(Account $account, SymfonyStyle $io, int $mailboxLimit, int $messageLimit): int {
+ $io->section('IMAP Test');
$mailAccount = $account->getMailAccount();
$sslMode = $mailAccount->getInboundSslMode();
$scheme = ($sslMode === 'none') ? 'imap' : 'imaps';
$host = $mailAccount->getInboundHost() ?? '(not set)';
$port = $mailAccount->getInboundPort();
- $output->writeln('Server: ' . $scheme . '://' . $host . ':' . $port . '');
+ $io->definitionList(
+ ['Server' => $scheme . '://' . $host . ':' . $port],
+ ['Username' => $mailAccount->getInboundUser()],
+ ['Security' => $sslMode],
+ );
if ($account->getMailAccount()->getInboundPassword() === null) {
- $output->writeln('No IMAP password set. The user may need to log in to set it.');
- return 1;
+ $io->error('No IMAP password set. The user may need to log in to set it.');
+ return self::FAILURE;
}
+ $io->text('Opening IMAP connection...');
+
try {
$imapClient = $this->protocolFactory->imapClient($account);
} catch (\Exception $e) {
- $output->writeln('Could not create IMAP client: ' . $e->getMessage() . '');
- return 2;
+ $io->error('Could not create IMAP client: ' . $e->getMessage());
+ return self::FAILURE;
}
+ $startTime = microtime(true);
try {
$imapClient->login();
- $output->writeln('Login successful');
+ $latency = (int)round(max(0, microtime(true) - $startTime) * 1000);
+ $mailboxes = $this->folderMapper->getFolders($account, $imapClient);
+ $this->folderMapper->fetchFolderAcls($mailboxes, $imapClient);
$capabilities = array_keys(
- json_decode($imapClient->capability->serialize(), true)
+ json_decode($imapClient->capability->serialize(), true, 512, JSON_THROW_ON_ERROR)
);
sort($capabilities);
- $output->writeln('Capabilities: ' . implode(', ', $capabilities) . '');
- $output->writeln('IMAP connection test passed');
- return 0;
+ $io->success('IMAP connection test passed.');
+ $io->definitionList(
+ ['Login' => 'Successful'],
+ ['Latency' => $latency . ' ms'],
+ ['Capabilities' => (string)count($capabilities)],
+ );
+
+ if ($capabilities === []) {
+ $io->note('The server returned no CAPABILITY entries.');
+ } else {
+ $io->listing($capabilities);
+ }
+
+ $this->renderImapMailboxPreview($account, $imapClient, $mailboxes, $io, $mailboxLimit, $messageLimit);
+
+ return self::SUCCESS;
} catch (Horde_Imap_Client_Exception $e) {
$this->logger->error('IMAP connection test failed for account ' . $account->getId() . ': ' . $e->getMessage(), [
'exception' => $e,
]);
- $output->writeln('IMAP connection test failed: ' . $e->getMessage() . '');
- return 2;
+ $io->error('IMAP connection test failed: ' . $e->getMessage());
+ return self::FAILURE;
} finally {
$imapClient->logout();
}
}
- private function testJmap(\OCA\Mail\Account $account, OutputInterface $output): int {
- $output->writeln('Testing JMAP connection...');
+ private function testJmap(Account $account, SymfonyStyle $io, int $mailboxLimit, int $messageLimit): int {
+ $io->section('JMAP Test');
$mailAccount = $account->getMailAccount();
$sslMode = $mailAccount->getInboundSslMode();
- $scheme = ($sslMode === 'none') ? 'http' : 'https';
+ $scheme = ($sslMode === 'yes') ? 'https' : 'http';
$host = $mailAccount->getInboundHost() ?? '(not set)';
$port = $mailAccount->getInboundPort();
$path = $mailAccount->getPath() ?? '/.well-known/jmap';
- $output->writeln('Server: ' . $scheme . '://' . $host . ':' . $port . $path . '');
+ $io->definitionList(
+ ['Server' => $scheme . '://' . $host . ':' . $port . $path],
+ ['Username' => $mailAccount->getInboundUser()],
+ ['Security' => $sslMode],
+ );
+
+ if ($mailAccount->getInboundPassword() === null) {
+ $io->error('No JMAP password set. The user may need to log in to set it.');
+ return self::FAILURE;
+ }
+
+ $io->text('Opening JMAP session...');
+ $startTime = microtime(true);
try {
$client = $this->protocolFactory->jmapClient($account);
$session = $client->connect();
+ $this->jmapOperationsService->connect($account);
} catch (\Exception $e) {
$this->logger->error('JMAP connection test failed for account ' . $account->getId() . ': ' . $e->getMessage(), [
'exception' => $e,
]);
- $output->writeln('JMAP connection test failed: ' . $e->getMessage() . '');
- return 2;
+ $io->error('JMAP connection test failed: ' . $e->getMessage());
+ return self::FAILURE;
}
if (!$client->sessionStatus()) {
- $output->writeln('JMAP session discovery failed. Check the server and credentials.');
- return 2;
+ $io->error('JMAP session discovery failed. Check the server and credentials.');
+ return self::FAILURE;
}
- $output->writeln('JMAP session established');
- $output->writeln('Username: ' . $session->username() . '');
- $output->writeln('API URL: ' . $session->commandUrl() . '');
- $output->writeln('State: ' . $session->state() . '');
+ $latency = (int)round(max(0, microtime(true) - $startTime) * 1000);
+
+ $io->success('JMAP connection test passed.');
+ $io->definitionList(
+ ['Session' => 'Established'],
+ ['Username' => $session->username()],
+ ['API URL' => $session->commandUrl()],
+ ['State' => $session->state()],
+ ['Latency' => $latency . ' ms'],
+ );
$capabilities = [];
foreach ($session->capabilities() as $capability) {
$capabilities[] = $capability->id();
}
sort($capabilities);
- $output->writeln('Capabilities: ' . implode(', ', $capabilities) . '');
- $output->writeln('JMAP connection test passed');
- return 0;
+ if ($capabilities === []) {
+ $io->note('The server returned no JMAP capabilities.');
+ } else {
+ $io->listing($capabilities);
+ }
+
+ $this->renderJmapMailboxPreview($io, $mailboxLimit, $messageLimit);
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * @param list<\OCA\Mail\Folder> $folders
+ */
+ private function renderImapMailboxPreview(Account $account, $imapClient, array $folders, SymfonyStyle $io, int $mailboxLimit, int $messageLimit): void {
+ $io->section('Mailboxes');
+
+ usort($folders, static fn ($left, $right) => strcmp($left->getMailbox(), $right->getMailbox()));
+ $rows = [];
+ foreach (array_slice($folders, 0, $mailboxLimit) as $folder) {
+ $status = $this->folderMapper->getFolderStatus($imapClient, $folder->getMailbox());
+ $attributes = array_map(static fn (string $attribute) => strtolower($attribute), $folder->getAttributes());
+ $rows[] = [
+ $folder->getMailbox(),
+ $folder->getDelimiter() ?? 'NIL',
+ in_array('\\noselect', $attributes, true) ? 'no' : 'yes',
+ $status !== null ? (string)$status->getTotal() : 'N/A',
+ $status !== null ? (string)$status->getUnread() : 'N/A',
+ ];
+ }
+
+ if ($rows === []) {
+ $io->note('No mailboxes returned by the IMAP server.');
+ } else {
+ $io->table(['Mailbox', 'Delimiter', 'Selectable', 'Messages', 'Unseen'], $rows);
+ if (count($folders) > $mailboxLimit) {
+ $io->note('Showing the first ' . $mailboxLimit . ' mailboxes. Increase --mailboxes to see more.');
+ }
+ }
+
+ $io->section('Inbox Preview');
+ $inbox = array_values(array_filter($folders, static fn ($folder) => strtolower($folder->getMailbox()) === 'inbox'))[0] ?? null;
+ if ($inbox === null) {
+ $io->note('No INBOX mailbox returned by the IMAP server.');
+ return;
+ }
+
+ try {
+ $messages = $this->loadRecentImapInboxMessages($account, $imapClient, $inbox->getMailbox(), $messageLimit);
+ } catch (\Throwable $e) {
+ $this->logger->warning('Could not load IMAP inbox preview for account ' . $account->getId() . ': ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ $io->warning('Connected successfully, but could not load recent inbox messages: ' . $e->getMessage());
+ return;
+ }
+
+ $this->renderMessageTable($io, $this->buildImapMessageRows($messages));
+ }
+
+ /**
+ * @return list
+ */
+ private function loadRecentImapInboxMessages(Account $account, $imapClient, string $mailbox, int $messageLimit): array {
+ $metaResults = $imapClient->search(
+ $mailbox,
+ null,
+ [
+ 'results' => [
+ Horde_Imap_Client::SEARCH_RESULTS_MIN,
+ Horde_Imap_Client::SEARCH_RESULTS_MAX,
+ Horde_Imap_Client::SEARCH_RESULTS_COUNT,
+ ],
+ ]
+ );
+
+ $total = (int)($metaResults['count'] ?? 0);
+ if ($total === 0) {
+ return [];
+ }
+
+ $maxUid = $metaResults['max'];
+ if ($maxUid === null) {
+ $status = $imapClient->status($mailbox);
+ $maxUid = ((int)($status['uidnext'] ?? 1)) - 1;
+ }
+
+ $lower = max(1, (int)$maxUid - max(50, $messageLimit * 20));
+ $uids = new Horde_Imap_Client_Ids($lower . ':' . (int)$maxUid);
+ $messages = $this->imapMessageMapper->findByIds($imapClient, $mailbox, $uids, $account->getUserId(), false);
+
+ usort($messages, static fn (IMAPMessage $left, IMAPMessage $right) => $right->getSentDate()->getTimestamp() <=> $left->getSentDate()->getTimestamp());
+
+ return array_slice($messages, 0, $messageLimit);
+ }
+
+ private function renderJmapMailboxPreview(SymfonyStyle $io, int $mailboxLimit, int $messageLimit): void {
+ $io->section('Mailboxes');
+ $mailboxes = $this->jmapOperationsService->collectionList(null, [], [
+ ['attribute' => 'order', 'direction' => true],
+ ['attribute' => 'name', 'direction' => true],
+ ]);
+
+ $rows = [];
+ foreach (array_slice($mailboxes, 0, $mailboxLimit) as $mailbox) {
+ $rows[] = [
+ $mailbox->getName(),
+ $mailbox->getDelimiter() ?? 'NIL',
+ $mailbox->getSelectable() ? 'yes' : 'no',
+ (string)$mailbox->getMessages(),
+ (string)$mailbox->getUnseen(),
+ ];
+ }
+
+ if ($rows === []) {
+ $io->note('No mailboxes returned by the JMAP server.');
+ } else {
+ $io->table(['Mailbox', 'Delimiter', 'Selectable', 'Messages', 'Unseen'], $rows);
+ if (count($mailboxes) > $mailboxLimit) {
+ $io->note('Showing the first ' . $mailboxLimit . ' mailboxes. Increase --mailboxes to see more.');
+ }
+ }
+
+ $io->section('Inbox Preview');
+ $inbox = $this->findInboxMailbox($mailboxes);
+ if ($inbox === null || $inbox->getRemoteId() === null) {
+ $io->note('No INBOX mailbox returned by the JMAP server.');
+ return;
+ }
+
+ $messages = $this->jmapOperationsService->entityList(
+ $inbox->getRemoteId(),
+ [],
+ [['attribute' => 'received', 'direction' => true]],
+ ['anchor' => 'absolute', 'position' => 0, 'tally' => $messageLimit]
+ );
+
+ /** @var list $messageList */
+ $messageList = $messages['list'] ?? [];
+ $this->renderMessageTable($io, $this->buildJmapMessageRows($messageList));
+ }
+
+ /**
+ * @param Mailbox[] $mailboxes
+ */
+ private function findInboxMailbox(array $mailboxes): ?Mailbox {
+ foreach ($mailboxes as $mailbox) {
+ if ($mailbox->isSpecialUse('inbox') || $mailbox->isInbox()) {
+ return $mailbox;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param list $messages
+ * @return list>
+ */
+ private function buildImapMessageRows(array $messages): array {
+ $rows = [];
+ foreach ($messages as $message) {
+ $rows[] = [
+ (string)$message->getUid(),
+ date('Y-m-d H:i', $message->getSentDate()->getTimestamp()),
+ $this->formatAddressList($message->getFrom()),
+ $this->truncate($message->getSubject()),
+ '',
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * @param list $messages
+ * @return list>
+ */
+ private function buildJmapMessageRows(array $messages): array {
+ $rows = [];
+ foreach ($messages as $message) {
+ $rows[] = [
+ (string)($message->getRemoteId() ?? $message->getUid()),
+ date('Y-m-d H:i', $message->getSentAt()),
+ $this->formatAddressList($message->getFrom()),
+ $this->truncate($message->getSubject()),
+ $this->truncate($message->getPreviewText() ?? ''),
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * @param list> $rows
+ */
+ private function renderMessageTable(SymfonyStyle $io, array $rows): void {
+ if ($rows === []) {
+ $io->note('No recent messages found in INBOX.');
+ return;
+ }
+
+ $io->table(['UID', 'Date', 'From', 'Subject', 'Preview'], $rows);
+ }
+
+ private function formatAddressList(AddressList $addresses): string {
+ $first = $addresses->first();
+ if ($first === null) {
+ return 'NIL';
+ }
+
+ return $first->getLabel() ?? $first->getEmail() ?? 'NIL';
+ }
+
+ private function truncate(string $value, int $length = 60): string {
+ if ($value === '') {
+ return '';
+ }
+
+ return mb_strimwidth($value, 0, $length, '...');
}
- private function unsupportedProtocol(string $protocol, OutputInterface $output): int {
- $output->writeln("Unsupported protocol: $protocol");
- return 1;
+ private function unsupportedProtocol(string $protocol, SymfonyStyle $io): int {
+ $io->error("Unsupported protocol: $protocol");
+ return self::FAILURE;
}
}
diff --git a/lib/Contracts/IDkimService.php b/lib/Contracts/IDkimService.php
index 024ad1a954..b21ed14fdb 100644
--- a/lib/Contracts/IDkimService.php
+++ b/lib/Contracts/IDkimService.php
@@ -11,8 +11,9 @@
use OCA\Mail\Account;
use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\Message;
interface IDkimService {
- public function validate(Account $account, Mailbox $mailbox, int $id): bool;
+ public function validate(Account $account, Mailbox $mailbox, Message $message): bool;
public function getCached(Account $account, Mailbox $mailbox, int $id): ?bool;
}
diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php
deleted file mode 100644
index c18bf8f4a1..0000000000
--- a/lib/Contracts/IMailManager.php
+++ /dev/null
@@ -1,349 +0,0 @@
-clientFactory->getClient($account);
try {
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(),
+ $message,
true
);
$unsubscribeUrl = $imapMessage->getUnsubscribeUrl();
@@ -86,8 +82,6 @@ public function unsubscribe(int $id): JsonResponse {
'exception' => $e,
]);
return JsonResponse::error('Unknown error');
- } finally {
- $client->logout();
}
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId unsubscribed from mailing list: $id on behalf of $effectiveUserId");
diff --git a/lib/Controller/MailboxesApiController.php b/lib/Controller/MailboxesApiController.php
index cbb14b558f..2b7d3bbb5d 100644
--- a/lib/Controller/MailboxesApiController.php
+++ b/lib/Controller/MailboxesApiController.php
@@ -9,12 +9,12 @@
namespace OCA\Mail\Controller;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\ResponseDefinitions;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
@@ -32,7 +32,7 @@ public function __construct(
string $appName,
IRequest $request,
private readonly ?string $userId,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private readonly AccountService $accountService,
private IMailSearch $mailSearch,
private DelegationService $delegationService,
diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php
index b7dad5a191..a36a3a1b4f 100644
--- a/lib/Controller/MailboxesController.php
+++ b/lib/Controller/MailboxesController.php
@@ -12,8 +12,8 @@
use Horde_Imap_Client;
use OCA\Mail\AppInfo\Application;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
+use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\IncompleteSyncException;
use OCA\Mail\Exception\MailboxNotCachedException;
@@ -22,6 +22,7 @@
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\SyncService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -41,11 +42,12 @@ public function __construct(
IRequest $request,
private AccountService $accountService,
private ?string $userId,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private SyncService $syncService,
private readonly IConfig $config,
private readonly ITimeFactory $timeFactory,
private DelegationService $delegationService,
+ private MailboxMapper $mailboxMapper,
) {
parent::__construct($appName, $request);
}
@@ -127,10 +129,8 @@ public function patch(int $id,
}
if ($syncInBackground !== null) {
- $mailbox = $this->mailManager->enableMailboxBackgroundSync(
- $mailbox,
- $syncInBackground
- );
+ $mailbox->setSyncInBackground($syncInBackground);
+ $this->mailboxMapper->update($mailbox);
$syncVerb = $syncInBackground ? 'enabled' : 'disabled';
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId $syncVerb background sync for mailbox: $id on behalf of $effectiveUserId");
}
diff --git a/lib/Controller/MessageApiController.php b/lib/Controller/MessageApiController.php
index 3306de70cd..0b4f786cf1 100644
--- a/lib/Controller/MessageApiController.php
+++ b/lib/Controller/MessageApiController.php
@@ -15,7 +15,6 @@
use OCA\Mail\Exception\SmimeDecryptException;
use OCA\Mail\Exception\UploadException;
use OCA\Mail\Http\TrapError;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\SmimeData;
use OCA\Mail\ResponseDefinitions;
use OCA\Mail\Service\AccountService;
@@ -59,7 +58,6 @@ public function __construct(
private AttachmentService $attachmentService,
private OutboxService $outboxService,
private MailManager $mailManager,
- private IMAPClientFactory $clientFactory,
private LoggerInterface $logger,
private ITimeFactory $time,
private IURLGenerator $urlGenerator,
@@ -252,13 +250,11 @@ public function get(int $id): DataResponse {
}
$loadBody = true;
- $client = $this->clientFactory->getClient($account);
try {
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(),
+ $message,
true
);
} catch (ServiceException $e) {
@@ -268,13 +264,10 @@ public function get(int $id): DataResponse {
$this->logger->warning('Message could not be decrypted', ['exception' => $e->getMessage()]);
$loadBody = false;
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid()
+ $message
);
- } finally {
- $client->logout();
}
$json = $imapMessage->getFullMessage($id, $loadBody);
@@ -344,19 +337,15 @@ public function getRaw(int $id): DataResponse {
return new DataResponse('Message, Account or Mailbox not found', Http::STATUS_NOT_FOUND);
}
- $client = $this->clientFactory->getClient($account);
try {
$source = $this->mailManager->getSource(
- $client,
$account,
- $mailbox->getName(),
- $message->getUid()
+ $mailbox,
+ $message
);
} catch (ServiceException $e) {
$this->logger->error('Message not found on IMAP, or mail server went away', ['exception' => $e->getMessage()]);
return new DataResponse('Message not found', Http::STATUS_NOT_FOUND);
- } finally {
- $client->logout();
}
return new DataResponse($source, Http::STATUS_OK);
diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php
index 098757c908..c508e26569 100755
--- a/lib/Controller/MessagesController.php
+++ b/lib/Controller/MessagesController.php
@@ -14,7 +14,6 @@
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OCA\Mail\Attachment;
use OCA\Mail\Contracts\IDkimService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Contracts\ITrustedSenderService;
@@ -25,12 +24,12 @@
use OCA\Mail\Http\AttachmentDownloadResponse;
use OCA\Mail\Http\HtmlResponse;
use OCA\Mail\Http\TrapError;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\SmimeData;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
use OCA\Mail\Service\DelegationService;
use OCA\Mail\Service\ItineraryService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\SmimeService;
use OCA\Mail\Service\SnoozeService;
use OCP\AppFramework\Controller;
@@ -67,7 +66,7 @@ public function __construct(
string $appName,
IRequest $request,
private AccountService $accountService,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private IMailSearch $mailSearch,
private ItineraryService $itineraryService,
private ?string $userId,
@@ -81,7 +80,6 @@ public function __construct(
private ITrustedSenderService $trustedSenderService,
private IMailTransmission $mailTransmission,
private SmimeService $smimeService,
- private IMAPClientFactory $clientFactory,
private IDkimService $dkimService,
private IUserPreferences $preferences,
private SnoozeService $snoozeService,
@@ -214,25 +212,20 @@ public function getBody(int $id): JSONResponse {
$cacheInstance = $this->getCacheForAccount($account->getId());
$imapMessageCacheKey = "message_$id";
- $client = $this->clientFactory->getClient($account);
- try {
- $imapMessage = $this->mailManager->getImapMessage(
- $client,
- $account,
- $mailbox,
- $message->getUid(), true
- );
-
- if ($imapMessage->hasHtmlMessage()) {
- $cacheInstance->set($imapMessageCacheKey, $imapMessage->getHtmlBody($id), 600);
- }
+ $imapMessage = $this->mailManager->getImapMessage(
+ $account,
+ $mailbox,
+ $message,
+ true
+ );
- $json = $imapMessage->getFullMessage($id);
- } finally {
- $client->logout();
+ if ($imapMessage->hasHtmlMessage()) {
+ $cacheInstance->set($imapMessageCacheKey, $imapMessage->getHtmlBody($id), 600);
}
- $itineraries = $this->itineraryService->getCached($account, $mailbox, $message->getUid());
+ $json = $imapMessage->getFullMessage($id);
+
+ $itineraries = $this->itineraryService->getCached($account, $mailbox, $message->getId());
if ($itineraries) {
$json['itineraries'] = $itineraries;
}
@@ -251,7 +244,7 @@ public function getBody(int $id): JSONResponse {
}
$json['smime'] = $smimeData;
- $dkimResult = $this->dkimService->getCached($account, $mailbox, $message->getUid());
+ $dkimResult = $this->dkimService->getCached($account, $mailbox, $message->getId());
if (is_bool($dkimResult)) {
$json['dkimValid'] = $dkimResult;
}
@@ -287,7 +280,7 @@ public function getItineraries(int $id): JSONResponse {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $response = new JsonResponse($this->itineraryService->extract($account, $mailbox, $message->getUid()));
+ $response = new JsonResponse($this->itineraryService->extract($account, $mailbox, $message));
$response->cacheFor(24 * 60 * 60, false, true);
return $response;
}
@@ -310,7 +303,7 @@ public function getDkim(int $id): JSONResponse {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $response = new JSONResponse(['valid' => $this->dkimService->validate($account, $mailbox, $message->getUid())]);
+ $response = new JSONResponse(['valid' => $this->dkimService->validate($account, $mailbox, $message)]);
$response->cacheFor(24 * 60 * 60, false, true);
return $response;
}
@@ -395,10 +388,10 @@ public function move(int $id, int $destFolderId): JSONResponse {
$this->mailManager->moveMessage(
$srcAccount,
- $srcMailbox->getName(),
- $message->getUid(),
+ $srcMailbox,
+ $message,
$dstAccount,
- $dstMailbox->getName()
+ $dstMailbox
);
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId moved message <$id> to mailbox <$destFolderId> on behalf of $effectiveUserId");
@@ -498,7 +491,7 @@ public function mdn(int $id): JSONResponse {
try {
$this->mailTransmission->sendMdn($account, $mailbox, $message);
- $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), '$mdnsent', true);
+ $this->mailManager->flagMessages($account, $mailbox, '$mdnsent', true, $message);
} catch (ServiceException $ex) {
$this->logger->error('Sending mdn failed: ' . $ex->getMessage());
throw $ex;
@@ -527,19 +520,13 @@ public function getSource(int $id): JSONResponse {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $client = $this->clientFactory->getClient($account);
- try {
- $response = new JSONResponse([
- 'source' => $this->mailManager->getSource(
- $client,
- $account,
- $mailbox->getName(),
- $message->getUid()
- )
- ]);
- } finally {
- $client->logout();
- }
+ $response = new JSONResponse([
+ 'source' => $this->mailManager->getSource(
+ $account,
+ $mailbox,
+ $message
+ )
+ ]);
// Enable caching
$response->cacheFor(60 * 60, false, true);
@@ -572,17 +559,11 @@ public function export(int $id): Response {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $client = $this->clientFactory->getClient($account);
- try {
- $source = $this->mailManager->getSource(
- $client,
- $account,
- $mailbox->getName(),
- $message->getUid()
- );
- } finally {
- $client->logout();
- }
+ $source = $this->mailManager->getSource(
+ $account,
+ $mailbox,
+ $message
+ );
return new AttachmentDownloadResponse(
$source ?? '',
@@ -630,17 +611,11 @@ public function saveFile(int $id, string $targetPath): Response {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $client = $this->clientFactory->getClient($account);
- try {
- $source = $this->mailManager->getSource(
- $client,
- $account,
- $mailbox->getName(),
- $message->getUid()
- );
- } finally {
- $client->logout();
- }
+ $source = $this->mailManager->getSource(
+ $account,
+ $mailbox,
+ $message
+ );
if ($source === null) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
@@ -704,18 +679,12 @@ public function getHtmlBody(int $id, bool $plain = false): Response {
$html = $cacheInstance->get($imapMessageCacheKey);
if ($html === null) {
- $client = $this->clientFactory->getClient($account);
- try {
- $html = $this->mailManager->getImapMessage(
- $client,
- $account,
- $mailbox,
- $message->getUid(),
- true
- )->getHtmlBody($id);
- } finally {
- $client->logout();
- }
+ $html = $this->mailManager->getImapMessage(
+ $account,
+ $mailbox,
+ $message,
+ true
+ )->getHtmlBody($id);
}
$htmlResponse = $plain
@@ -949,7 +918,7 @@ public function setFlags(int $id, array $flags): JSONResponse {
$flagChanges = [];
foreach ($flags as $flag => $value) {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
- $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), $flag, $value);
+ $this->mailManager->flagMessages($account, $mailbox, $flag, $value, $message);
$flagChanges[] = "$flag=" . ($value ? 'true' : 'false');
}
$flagsSummary = implode(', ', $flagChanges);
@@ -983,12 +952,12 @@ public function setTag(int $id, string $imapLabel): JSONResponse {
}
try {
- $tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->userId);
+ $tag = $this->mailManager->getTagByLabel($imapLabel, $this->userId);
} catch (ClientException $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, true);
+ $this->mailManager->tagMessages($account, $mailbox, $tag, true, $message);
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId added tag <$imapLabel> on message <$id> on behalf of $effectiveUserId");
return new JSONResponse($tag);
}
@@ -1019,12 +988,12 @@ public function removeTag(int $id, string $imapLabel): JSONResponse {
}
try {
- $tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->userId);
+ $tag = $this->mailManager->getTagByLabel($imapLabel, $this->userId);
} catch (ClientException $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
- $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, false);
+ $this->mailManager->tagMessages($account, $mailbox, $tag, false, $message);
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId removed tag <$imapLabel> on message <$id> on behalf of $effectiveUserId");
return new JSONResponse($tag);
}
@@ -1055,8 +1024,8 @@ public function destroy(int $id): JSONResponse {
$this->mailManager->deleteMessage(
$account,
- $mailbox->getName(),
- $message->getUid()
+ $mailbox,
+ $message
);
$this->delegationService->logDelegatedAction($this->userId, $effectiveUserId, "$this->userId deleted message <$id> on behalf of $effectiveUserId");
return new JSONResponse();
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index 00360f58db..4b691b3d64 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -13,7 +13,6 @@
use OCA\Contacts\Event\LoadContactsOcaApiEvent;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\ConfigLexicon;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IUserPreferences;
use OCA\Mail\Db\SmimeCertificate;
use OCA\Mail\Db\TagMapper;
@@ -23,6 +22,7 @@
use OCA\Mail\Service\Classification\ClassificationSettingsService;
use OCA\Mail\Service\ContextChat\ContextChatSettingsService;
use OCA\Mail\Service\InternalAddressService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\OutboxService;
use OCA\Mail\Service\QuickActionsService;
use OCA\Mail\Service\SmimeService;
@@ -75,7 +75,7 @@ public function __construct(
private ?string $userId,
IUserSession $userSession,
private IUserPreferences $preferences,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private TagMapper $tagMapper,
IInitialState $initialStateService,
private LoggerInterface $logger,
diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php
index 52774eb054..1bcf045e0a 100644
--- a/lib/Controller/TagsController.php
+++ b/lib/Controller/TagsController.php
@@ -10,10 +10,10 @@
namespace OCA\Mail\Controller;
use OCA\Mail\AppInfo\Application;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
@@ -26,7 +26,7 @@ class TagsController extends Controller {
public function __construct(
IRequest $request,
private string $userId,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private AccountService $accountService,
) {
parent::__construct(Application::APP_ID, $request);
diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php
index c4f0e15718..af3f3ba883 100755
--- a/lib/Controller/ThreadController.php
+++ b/lib/Controller/ThreadController.php
@@ -9,13 +9,13 @@
namespace OCA\Mail\Controller;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\SnoozeService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -32,7 +32,7 @@ public function __construct(
IRequest $request,
private string $userId,
private AccountService $accountService,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private SnoozeService $snoozeService,
private AiIntegrationsService $aiIntergrationsService,
private LoggerInterface $logger,
diff --git a/lib/Db/Mailbox.php b/lib/Db/Mailbox.php
index 5408bf3fb1..2ca2654e69 100644
--- a/lib/Db/Mailbox.php
+++ b/lib/Db/Mailbox.php
@@ -125,9 +125,18 @@ public function isSpecialUse(string $specialUse): bool {
}
public function isCached(): bool {
- return $this->getSyncNewToken() !== null
+ if ($this->getSyncNewToken() !== null
&& $this->getSyncChangedToken() !== null
- && $this->getSyncVanishedToken() !== null;
+ && $this->getSyncVanishedToken() !== null
+ ) {
+ return true;
+ }
+
+ if ($this->getState() !== null) {
+ return true;
+ }
+
+ return false;
}
private function isLockActive(?int $lock, int $now): bool {
@@ -160,6 +169,14 @@ public function getStats(): MailboxStats {
}
public function getCacheBuster(): string {
+ $state = $this->getState();
+ if ($state !== null) {
+ return hash('md5', implode('|', [
+ (string)$this->getId(),
+ $state,
+ ]));
+ }
+
return hash('md5', implode('|', [
(string)$this->getId(),
$this->getSyncNewToken() ?? 'null',
diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php
index 7ece9a8a1f..11a8f15232 100644
--- a/lib/Db/MailboxMapper.php
+++ b/lib/Db/MailboxMapper.php
@@ -169,6 +169,16 @@ public function findAccountIdForMailbox(int $mailboxId): int {
return (int)$row['account_id'];
}
+ public function findSpecialUseMailbox(Account $account, string $specialUse): ?Mailbox {
+ foreach ($this->findAll($account) as $mailbox) {
+ if ($mailbox->isSpecialUse($specialUse) || ($specialUse === 'inbox' && $mailbox->isInbox())) {
+ return $mailbox;
+ }
+ }
+
+ return null;
+ }
+
/**
* @throws MailboxLockedException
*/
diff --git a/lib/Db/Message.php b/lib/Db/Message.php
index 35d4aa1af3..0241cce66d 100644
--- a/lib/Db/Message.php
+++ b/lib/Db/Message.php
@@ -23,7 +23,7 @@
* @method void setUid(int $uid)
* @method int getUid()
* @method string|null getMessageId()
- * @method void setReferences(string $references)
+ * @method void setReferences(string|null $references)
* @method string|null getReferences()
* @method string|null getInReplyTo()
* @method string|null getThreadRootId()
diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php
index 41bb6dc34f..9b1090f4be 100644
--- a/lib/Db/MessageMapper.php
+++ b/lib/Db/MessageMapper.php
@@ -270,6 +270,7 @@ public function insertBulk(Account $account, Message ...$messages): void {
$qb1 = $this->db->getQueryBuilder();
$qb1->insert($this->getTableName());
$qb1->setValue('uid', $qb1->createParameter('uid'));
+ $qb1->setValue('remote_id', $qb1->createParameter('remote_id'));
$qb1->setValue('message_id', $qb1->createParameter('message_id'));
$qb1->setValue('references', $qb1->createParameter('references'));
$qb1->setValue('in_reply_to', $qb1->createParameter('in_reply_to'));
@@ -297,6 +298,7 @@ public function insertBulk(Account $account, Message ...$messages): void {
foreach ($messages as $message) {
$qb1->setParameter('uid', $message->getUid(), IQueryBuilder::PARAM_INT);
+ $qb1->setParameter('remote_id', $message->getRemoteId(), $message->getRemoteId() === null ? IQueryBuilder::PARAM_NULL : IQueryBuilder::PARAM_STR);
$qb1->setParameter('message_id', $message->getMessageId(), IQueryBuilder::PARAM_STR);
$inReplyTo = self::filterMessageIdLength($message->getInReplyTo());
$qb1->setParameter('in_reply_to', $inReplyTo, $inReplyTo === null ? IQueryBuilder::PARAM_NULL : IQueryBuilder::PARAM_STR);
@@ -751,6 +753,74 @@ public function deleteByUid(Mailbox $mailbox, int ...$uids): void {
}
}
+ /**
+ * @param Mailbox $mailbox
+ * @param string[] $rids
+ *
+ * @return Message[]
+ */
+ public function findByRemoteIds(Mailbox $mailbox, array $rids): array {
+ if ($rids === []) {
+ return [];
+ }
+
+ $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('remote_id', $qb->createNamedParameter($rids, IQueryBuilder::PARAM_STR_ARRAY))
+ )
+ ->orderBy('sent_at', 'desc');
+
+ return $this->findRecipients($this->findEntities($select));
+ }
+
+ /**
+ * @param Mailbox $mailbox
+ * @param string[] $rids
+ */
+ public function deleteByRemoteIds(Mailbox $mailbox, string ...$rids): void {
+ $selectMessageIdsQuery = $this->db->getQueryBuilder();
+ $deleteRecipientsQuery = $this->db->getQueryBuilder();
+ $deleteMessagesQuery = $this->db->getQueryBuilder();
+
+ $selectMessageIdsQuery->select('id')
+ ->from($this->getTableName())
+ ->where(
+ $selectMessageIdsQuery->expr()->eq('mailbox_id', $selectMessageIdsQuery->createNamedParameter($mailbox->getId())),
+ $selectMessageIdsQuery->expr()->in('remote_id', $deleteMessagesQuery->createParameter('remote_ids')),
+ );
+ $deleteRecipientsQuery->delete('mail_recipients')
+ ->where(
+ $deleteRecipientsQuery->expr()->in('message_id', $deleteRecipientsQuery->createParameter('ids')),
+ );
+ $deleteMessagesQuery->delete('mail_messages')
+ ->where(
+ $deleteMessagesQuery->expr()->in('id', $deleteMessagesQuery->createParameter('ids')),
+ );
+
+ foreach (array_chunk($rids, 1000) as $chunk) {
+ $this->atomic(function () use ($selectMessageIdsQuery, $deleteRecipientsQuery, $deleteMessagesQuery, $chunk) {
+ $selectMessageIdsQuery->setParameter('remote_ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
+ $selectResult = $selectMessageIdsQuery->executeQuery();
+ $ids = array_map('intval', $selectResult->fetchAll(\PDO::FETCH_COLUMN));
+ $selectResult->closeCursor();
+ if (empty($ids)) {
+ return;
+ }
+
+ $deleteRecipientsQuery->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY);
+ $deleteRecipientsQuery->executeStatement();
+
+ $deleteMessagesQuery->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY);
+ $deleteMessagesQuery->executeStatement();
+ }, $this->db);
+ }
+ }
+
/**
* @param Account $account
* @param string $threadRootId
diff --git a/lib/IMAP/ImapMailboxConnector.php b/lib/IMAP/ImapMailboxConnector.php
new file mode 100644
index 0000000000..7be26dffc6
--- /dev/null
+++ b/lib/IMAP/ImapMailboxConnector.php
@@ -0,0 +1,121 @@
+mailboxSync->sync($account, $this->logger, $force);
+ }
+
+ #[\Override]
+ public function syncOne(Account $account, Mailbox $mailbox): void {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $this->mailboxSync->syncStats($client, $mailbox);
+ } finally {
+ $client->logout();
+ }
+ }
+
+ #[\Override]
+ public function create(Account $account, string $name, array $specialUse = []): Mailbox {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $folder = $this->folderMapper->createFolder($client, $name, $specialUse);
+ $this->folderMapper->fetchFolderAcls([$folder], $client);
+ $this->folderMapper->detectFolderSpecialUse([$folder]);
+ $this->mailboxSync->sync($account, $this->logger, true, $client);
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not get mailbox status: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ );
+ } finally {
+ $client->logout();
+ }
+
+ return $this->mailboxMapper->find($account, $name);
+ }
+
+ #[\Override]
+ public function rename(Account $account, Mailbox $mailbox, string $newName): Mailbox {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $this->folderMapper->renameFolder($client, $mailbox->getName(), $newName);
+ $this->mailboxSync->sync($account, $this->logger, true, $client);
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not rename mailbox ' . $mailbox->getId() . ' on IMAP: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ );
+ } finally {
+ $client->logout();
+ }
+
+ try {
+ return $this->mailboxMapper->find($account, $newName);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException("The renamed mailbox $newName does not exist", 0, $e);
+ }
+ }
+
+ #[\Override]
+ public function delete(Account $account, Mailbox $mailbox): void {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $this->folderMapper->delete($client, $mailbox->getName());
+ } finally {
+ $client->logout();
+ }
+
+ $this->mailboxMapper->delete($mailbox);
+ }
+
+ #[\Override]
+ public function subscribe(Account $account, Mailbox $mailbox, bool $subscribed): Mailbox {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $client->subscribeMailbox($mailbox->getName(), $subscribed);
+ $this->mailboxSync->sync($account, $this->logger, true, $client);
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not set subscription status for mailbox ' . $mailbox->getId() . ' on IMAP: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ );
+ } finally {
+ $client->logout();
+ }
+
+ return $this->mailboxMapper->find($account, $mailbox->getName());
+ }
+}
diff --git a/lib/IMAP/ImapMessageConnector.php b/lib/IMAP/ImapMessageConnector.php
new file mode 100644
index 0000000000..27380a2ddb
--- /dev/null
+++ b/lib/IMAP/ImapMessageConnector.php
@@ -0,0 +1,464 @@
+synchronizer->syncAccount($account, $this->logger, $force);
+ }
+
+ #[\Override]
+ public function syncMailbox(Account $account, Mailbox $mailbox, LoggerInterface $logger, int $criteria, ?array $knownUids = null, bool $force = false): SyncResult {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $rebuildThreads = $this->synchronizer->sync(
+ $account,
+ $client,
+ $mailbox,
+ $logger,
+ $criteria,
+ $knownUids,
+ $force,
+ );
+ } finally {
+ $client->logout();
+ }
+
+ return new SyncResult(
+ state: $mailbox->getSyncChangedToken(),
+ stats: [
+ 'rebuildThreads' => $rebuildThreads,
+ ],
+ );
+ }
+
+ /**
+ * @throws ServiceException
+ * @throws SmimeDecryptException
+ */
+ #[\Override]
+ public function fetchMessages(Account $account, Mailbox $mailbox, bool $loadBody = false, Message ...$messages): array {
+ $client = $this->protocolFactory->imapClient($account);
+ $uids = array_map(static fn ($message) => $message->getUid(), $messages);
+ try {
+ return $this->imapMessageMapper->findByIds(
+ $client,
+ $mailbox->getName(),
+ $uids,
+ $account->getUserId(),
+ true
+ );
+ } catch (DoesNotExistException|Horde_Mime_Exception|Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not load messages: ' . $e->getMessage(), $e->getCode(), $e);
+ } finally {
+ $client->logout();
+ }
+ }
+
+ #[\Override]
+ public function findMessages(Account $account, Mailbox $mailbox, SearchQuery $searchQuery): array {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $fetchResult = $client->search(
+ $mailbox->getName(),
+ $this->convertMailQueryToHordeQuery($searchQuery),
+ );
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not get message IDs: ' . $e->getMessage(), 0, $e);
+ } finally {
+ $client->logout();
+ }
+
+ return $fetchResult['match']->ids;
+ }
+
+ #[\Override]
+ public function fetchMessageRaw(Account $account, Mailbox $mailbox, Message $message): ?string {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ return $this->imapMessageMapper->getFullText(
+ $client,
+ $mailbox->getName(),
+ $message->getUid(),
+ $account->getUserId(),
+ false,
+ );
+ } finally {
+ $client->logout();
+ }
+ }
+
+ /**
+ * @return Attachment[]
+ * @throws DoesNotExistException
+ * @throws ServiceException
+ */
+ #[\Override]
+ public function fetchAttachments(Account $account, Mailbox $mailbox, Message $message): array {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ return $this->imapMessageMapper->getAttachments(
+ $client,
+ $mailbox->getName(),
+ $message->getUid(),
+ $account->getUserId(),
+ );
+ } catch (Horde_Imap_Client_Exception_NoSupportExtension|Horde_Imap_Client_Exception|Horde_Mime_Exception $e) {
+ throw new ServiceException('Could not load attachments from IMAP: ' . $e->getMessage(), $e->getCode(), $e);
+ } finally {
+ $client->logout();
+ }
+ }
+
+ /**
+ * @throws DoesNotExistException
+ * @throws ServiceException
+ */
+ #[\Override]
+ public function fetchAttachment(Account $account, Mailbox $mailbox, Message $message, string $attachmentId): Attachment {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ return $this->imapMessageMapper->getAttachment(
+ $client,
+ $mailbox->getName(),
+ $message->getUid(),
+ $attachmentId,
+ $account->getUserId(),
+ );
+ } catch (Horde_Imap_Client_Exception|Horde_Mime_Exception $e) {
+ throw new ServiceException('Could not load attachment from IMAP: ' . $e->getMessage(), $e->getCode(), $e);
+ } finally {
+ $client->logout();
+ }
+ }
+
+ #[\Override]
+ public function moveMessages(Account $account, Mailbox $targetMailbox, Mailbox $sourceMailbox, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $client = $this->protocolFactory->imapClient($account);
+
+ $mutatedMessages = [];
+ try {
+ foreach ($messages as $message) {
+ try {
+ $newUid = $this->imapMessageMapper->move($client, $sourceMailbox->getName(), $message->getUid(), $targetMailbox->getName());
+ if ($newUid === null) {
+ // The IMAP server does not support UIDPLUS and the message has no Message-ID
+ // header, so the new UID is unknown. It will be reconciled on the next sync.
+ $this->logger->debug('Moved message but could not determine its new UID', [
+ 'userId' => $account->getUserId(),
+ 'accountId' => $account->getId(),
+ 'sourceMailboxId' => $sourceMailbox->getId(),
+ 'targetMailboxId' => $targetMailbox->getId(),
+ 'messageUid' => $message->getUid(),
+ ]);
+ continue;
+ }
+ $message->setUid($newUid);
+ $message->setMailboxId($targetMailbox->getId());
+ $mutatedMessages[] = $message;
+ } catch (Horde_Imap_Client_Exception $e) {
+ $this->logger->error('Could not move message on remote IMAP server', [
+ 'exception' => $e,
+ 'userId' => $account->getUserId(),
+ 'accountId' => $account->getId(),
+ 'sourceMailboxId' => $sourceMailbox->getId(),
+ 'targetMailboxId' => $targetMailbox->getId(),
+ 'messageUid' => $message->getUid(),
+ ]);
+ }
+ }
+ } finally {
+ $client->logout();
+ }
+
+ return $mutatedMessages;
+ }
+
+ #[\Override]
+ public function deleteMessages(Account $account, Mailbox $mailbox, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $client = $this->protocolFactory->imapClient($account);
+
+ $mutatedMessages = [];
+ try {
+ foreach ($messages as $message) {
+ try {
+ $this->imapMessageMapper->expunge($client, $mailbox->getName(), $message->getUid());
+ $mutatedMessages[] = $message;
+ } catch (Horde_Imap_Client_Exception $e) {
+ $this->logger->error('Could not delete message on remote IMAP server', [
+ 'exception' => $e,
+ 'userId' => $account->getUserId(),
+ 'accountId' => $account->getId(),
+ 'mailboxId' => $mailbox->getId(),
+ 'messageUid' => $message->getUid(),
+ ]);
+ }
+ }
+ } finally {
+ $client->logout();
+ }
+
+ return $mutatedMessages;
+ }
+
+ #[\Override]
+ public function flagMessages(Account $account, Mailbox $mailbox, string $flag, bool $value, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $client = $this->protocolFactory->imapClient($account);
+
+ $uids = array_map(static fn (Message $message): int => $message->getUid(), $messages);
+ try {
+ $imapFlags = $this->filterFlags($client, $flag, $mailbox->getName());
+ foreach ($imapFlags as $imapFlag) {
+ if ($imapFlag === '') {
+ continue;
+ }
+ // modify remote messages
+ if ($value) {
+ $this->imapMessageMapper->addFlag($client, $mailbox, $uids, $imapFlag);
+ } else {
+ $this->imapMessageMapper->removeFlag($client, $mailbox, $uids, $imapFlag);
+ }
+ // update local messages
+ foreach ($messages as $message) {
+ $message->setFlag($flag, $value);
+ }
+ }
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not set message flag on remote IMAP server: ' . $e->getMessage(), $e->getCode(), $e);
+ } finally {
+ $client->logout();
+ }
+
+ return $messages;
+ }
+
+ #[\Override]
+ public function tagMessages(Account $account, Mailbox $mailbox, Tag $tag, bool $value, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $client = $this->protocolFactory->imapClient($account);
+
+ try {
+ if ($this->isPermflagsEnabledWithClient($client, $mailbox->getName()) === false) {
+ $this->logger->error('Cannot set message keyword, server does not support permanent flags', ['tag' => $tag->getDisplayName()]);
+ return [];
+ }
+
+ $uids = array_map(static fn (Message $message) => $message->getUid(), $messages);
+ try {
+ if ($value) {
+ $this->imapMessageMapper->addFlag($client, $mailbox, $uids, $tag->getImapLabel());
+ } else {
+ $this->imapMessageMapper->removeFlag($client, $mailbox, $uids, $tag->getImapLabel());
+ }
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not set message keyword on remote IMAP server: ' . $e->getMessage(), $e->getCode(), $e);
+ }
+ } finally {
+ $client->logout();
+ }
+
+ foreach ($messages as $message) {
+ $this->applyTagValue($message, $tag, $value);
+ }
+
+ return $messages;
+ }
+
+ #[\Override]
+ public function getQuota(Account $account): ?Quota {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ $quotas = array_map(
+ static fn (Folder $mailbox) => $client->getQuotaRoot($mailbox->getMailbox()),
+ $this->imapMailboxMapper->getFolders($account, $client),
+ );
+ } catch (Horde_Imap_Client_Exception_NoSupportExtension) {
+ return null;
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException('Could not get quota from IMAP: ' . $e->getMessage(), $e->getCode(), $e);
+ } finally {
+ $client->logout();
+ }
+
+ $storageQuotas = array_map(static fn (array $root) => $root['storage'] ?? [
+ 'usage' => 0,
+ 'limit' => 0,
+ ], array_merge(...array_values($quotas)));
+
+ if ($storageQuotas === []) {
+ return null;
+ }
+
+ $storage = array_merge(...array_values($storageQuotas));
+
+ return new Quota(
+ 1024 * (int)($storage['usage'] ?? 0),
+ 1024 * (int)($storage['limit'] ?? 0),
+ );
+ }
+
+ #[\Override]
+ public function clearCache(Account $account, Mailbox $mailbox): void {
+ $this->synchronizer->clearCache($account, $mailbox);
+ }
+
+ #[\Override]
+ public function repairSync(Account $account, Mailbox $mailbox): void {
+ $this->synchronizer->repairSync($account, $mailbox, $this->logger);
+ }
+
+ #[\Override]
+ public function isPermflagsEnabled(Account $account, Mailbox $mailbox): bool {
+ $client = $this->protocolFactory->imapClient($account);
+ try {
+ return $this->isPermflagsEnabledWithClient($client, $mailbox->getName());
+ } finally {
+ $client->logout();
+ }
+ }
+
+ private function isPermflagsEnabledWithClient($client, string $mailbox): bool {
+ try {
+ $capabilities = $client->status($mailbox, Horde_Imap_Client::STATUS_PERMFLAGS);
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not get message flag options from IMAP: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ );
+ }
+
+ return is_array($capabilities)
+ && in_array('\\*', $capabilities['permflags'] ?? [], true);
+ }
+
+ private function convertMailQueryToHordeQuery(SearchQuery $searchQuery): Horde_Imap_Client_Search_Query {
+ $query = new Horde_Imap_Client_Search_Query();
+ foreach ($searchQuery->getBodies() as $textToken) {
+ $query->text($textToken, true);
+ }
+
+ return $query;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function filterFlags($client, string $flag, string $mailbox): array {
+ $systemFlags = [
+ 'seen' => [Horde_Imap_Client::FLAG_SEEN],
+ 'answered' => [Horde_Imap_Client::FLAG_ANSWERED],
+ 'flagged' => [Horde_Imap_Client::FLAG_FLAGGED],
+ 'deleted' => [Horde_Imap_Client::FLAG_DELETED],
+ 'draft' => [Horde_Imap_Client::FLAG_DRAFT],
+ 'recent' => [Horde_Imap_Client::FLAG_RECENT],
+ ];
+
+ if (isset($systemFlags[$flag])) {
+ return $systemFlags[$flag];
+ }
+
+ try {
+ $capabilities = $client->status($mailbox, Horde_Imap_Client::STATUS_PERMFLAGS);
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not get message flag options from IMAP: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ );
+ }
+
+ if (!isset($capabilities['permflags'])) {
+ return [];
+ }
+
+ if (in_array('\\*', $capabilities['permflags'], true) || in_array($flag, $capabilities['permflags'], true)) {
+ return [$flag];
+ }
+
+ return [];
+ }
+
+ private function applyTagValue(Message $message, Tag $tag, bool $value): void {
+ $tags = $message->getTags();
+
+ if ($value) {
+ foreach ($tags as $existingTag) {
+ if ($existingTag->getImapLabel() === $tag->getImapLabel()) {
+ return;
+ }
+ }
+
+ $new = new Tag();
+ $new->setImapLabel($tag->getImapLabel());
+ $new->setDisplayName($tag->getDisplayName());
+ $new->setColor($tag->getColor());
+ $new->setIsDefaultTag($tag->getIsDefaultTag() ?? false);
+ $tags[] = $new;
+ } else {
+ $tags = array_values(array_filter(
+ $tags,
+ static fn (Tag $existingTag): bool => $existingTag->getImapLabel() !== $tag->getImapLabel(),
+ ));
+ }
+
+ $message->setTags($tags);
+ }
+}
diff --git a/lib/IMAP/MailboxSync.php b/lib/IMAP/MailboxSync.php
index c47d04cfcf..90ed10398d 100644
--- a/lib/IMAP/MailboxSync.php
+++ b/lib/IMAP/MailboxSync.php
@@ -21,6 +21,7 @@
use OCA\Mail\Events\MailboxesSynchronizedEvent;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Folder;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
@@ -55,7 +56,7 @@ public function __construct(
private MailboxMapper $mailboxMapper,
private FolderMapper $folderMapper,
private MailAccountMapper $mailAccountMapper,
- private IMAPClientFactory $imapClientFactory,
+ private readonly ProtocolFactory $protocolFactory,
ITimeFactory $timeFactory,
IEventDispatcher $dispatcher,
IDBConnection $dbConnection,
@@ -72,13 +73,13 @@ public function sync(Account $account,
LoggerInterface $logger,
bool $force = false,
?Horde_Imap_Client_Socket $client = null): void {
- if (!$force && $account->getMailAccount()->getLastMailboxSync() >= ($this->timeFactory->getTime() - 7200)) {
+ if (!$force && $account->getMailAccount()->getLastMailboxSync() >= ($this->timeFactory->getTime() - 900)) {
$logger->debug('account is up to date, skipping mailbox sync');
return;
}
if ($client === null) {
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
$ownClient = true;
} else {
$ownClient = false;
diff --git a/lib/IMAP/PreviewEnhancer.php b/lib/IMAP/PreviewEnhancer.php
index 3f67545379..929a85a946 100644
--- a/lib/IMAP/PreviewEnhancer.php
+++ b/lib/IMAP/PreviewEnhancer.php
@@ -11,10 +11,12 @@
use Horde_Imap_Client_Exception;
use OCA\Mail\Account;
+use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMapper;
use OCA\Mail\IMAP\MessageMapper as ImapMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Avatar\Avatar;
use OCA\Mail\Service\AvatarService;
@@ -26,7 +28,7 @@
class PreviewEnhancer {
public function __construct(
- private IMAPClientFactory $clientFactory,
+ private readonly ProtocolFactory $protocolFactory,
private ImapMapper $imapMapper,
private DbMapper $mapper,
private LoggerInterface $logger,
@@ -49,10 +51,9 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo
return array_merge($carry, [$message->getUid()]);
}, []);
- $client = $this->clientFactory->getClient($account);
foreach ($messages as $message) {
- $attachments = $this->attachmentService->getAttachmentNames($account, $mailbox, $message, $client);
+ $attachments = $this->attachmentService->getAttachmentNames($account, $mailbox, $message);
$message->setAttachments($attachments);
}
@@ -77,6 +78,19 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo
return $messages;
}
+ if ($account->getMailAccount()->getProtocol() === MailAccount::PROTOCOL_JMAP) {
+ foreach ($messages as $message) {
+ if ($message->getStructureAnalyzed()) {
+ continue;
+ }
+
+ $message->setStructureAnalyzed(true);
+ }
+
+ return $this->mapper->updatePreviewDataBulk(...$messages);
+ }
+
+ $client = $this->protocolFactory->imapClient($account);
try {
$data = $this->imapMapper->getBodyStructureData(
$client,
diff --git a/lib/IMAP/Search/Provider.php b/lib/IMAP/Search/Provider.php
deleted file mode 100644
index aecfa50d2e..0000000000
--- a/lib/IMAP/Search/Provider.php
+++ /dev/null
@@ -1,66 +0,0 @@
-clientFactory->getClient($account);
- try {
- $fetchResult = $client->search(
- $mailbox->getName(),
- $this->convertMailQueryToHordeQuery($searchQuery)
- );
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException('Could not get message IDs: ' . $e->getMessage(), 0, $e);
- } finally {
- $client->logout();
- }
-
- return $fetchResult['match']->ids;
- }
-
- /**
- * @param SearchQuery $searchQuery
- *
- * @todo possible optimization: filter flags here as well as it might speed up IMAP search
- *
- * @return Horde_Imap_Client_Search_Query
- */
- private function convertMailQueryToHordeQuery(SearchQuery $searchQuery): Horde_Imap_Client_Search_Query {
- return array_reduce(
- $searchQuery->getBodies(),
- static function (Horde_Imap_Client_Search_Query $query, string $textToken) {
- $query->text($textToken, true);
- return $query;
- },
- new Horde_Imap_Client_Search_Query()
- );
- }
-}
diff --git a/lib/JMAP/Exception/JmapUnknownMethod.php b/lib/JMAP/Exception/JmapUnknownMethod.php
new file mode 100644
index 0000000000..a784f9f025
--- /dev/null
+++ b/lib/JMAP/Exception/JmapUnknownMethod.php
@@ -0,0 +1,18 @@
+configureTransportMode($secure ? 'https' : 'http');
- $client->setHost($host . ':' . (string)$port);
+ $client->configureTransportMode($secure ? JmapClient::TRANSPORT_MODE_SECURE : JmapClient::TRANSPORT_MODE_STANDARD);
+ $client->setHost($host . ':' . $port);
if ($path !== '/.well-known/jmap') {
$client->setDiscoveryPath($path);
}
diff --git a/lib/JMAP/JmapMailboxAdapter.php b/lib/JMAP/JmapMailboxAdapter.php
new file mode 100644
index 0000000000..3883188f33
--- /dev/null
+++ b/lib/JMAP/JmapMailboxAdapter.php
@@ -0,0 +1,169 @@
+setName($response->label() ?? $response->id() ?? '');
+ $mailbox->setNameHash(md5($response->id() ?? ''));
+ $mailbox->setRemoteParentId($response->in());
+ $mailbox->setRemoteId($response->id());
+ $mailbox->setState(null);
+ $mailbox->setAttributes(json_encode($this->convertToAttributes($response), JSON_THROW_ON_ERROR));
+ $mailbox->setDelimiter(self::DELIMITER);
+ $mailbox->setMessages($response->objectsTotal() ?? 0);
+ $mailbox->setUnseen($response->objectsUnseen() ?? 0);
+ $mailbox->setSelectable($response->rights()?->readItems() === true);
+ $mailbox->setSpecialUse(json_encode($this->convertToSpecialUse($response), JSON_THROW_ON_ERROR));
+ $mailbox->setMyAcls($this->convertToAcl($response));
+ $mailbox->setShared(false);
+
+ return $mailbox;
+ }
+
+ /**
+ * @throws JsonException
+ */
+ public function convertFromMailbox(Mailbox $mailbox, array $patch = []): MailboxParametersRequest {
+ $properties = ['location', 'name', 'subscribed', 'role'];
+ if (!empty($patch)) {
+ $properties = array_intersect($properties, $patch);
+ }
+
+ $request = new MailboxParametersRequest();
+
+ if (in_array('location', $properties, true)) {
+ $request->in($mailbox->getRemoteParentId());
+ }
+ if (in_array('name', $properties, true)) {
+ $request->label($mailbox->getName());
+ }
+ if (in_array('subscribed', $properties, true)) {
+ $request->subscribed(str_contains($mailbox->getAttributes() ?? '', '\\subscribed'));
+ }
+ if (in_array('role', $properties, true)) {
+ $specialUse = json_decode($mailbox->getSpecialUse() ?? '[]', true) ?? [];
+ $role = $this->convertFromSpecialUse($specialUse);
+ $request->role($role);
+ }
+
+ return $request;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function convertToAttributes(MailboxParametersResponse $response): array {
+ $attributes = [];
+
+ if ($response->subscribed() !== false) {
+ $attributes[] = '\\subscribed';
+ }
+
+ $role = $response->role();
+ if ($role !== null && $role !== '') {
+ $attributes[] = '\\' . $this->normalizeSpecialUse($role);
+ }
+
+ if ($response->rights()?->readItems() !== true) {
+ $attributes[] = '\\noselect';
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function convertToSpecialUse(MailboxParametersResponse $response): array {
+ $role = $response->role();
+ if ($role === null || $role === '') {
+ return [];
+ }
+
+ return [$this->normalizeSpecialUse($role)];
+ }
+
+ /**
+ * @param string[] $specialUse
+ */
+ private function convertFromSpecialUse(array $specialUse): ?string {
+ $role = $specialUse[0] ?? null;
+ if ($role === null) {
+ return null;
+ }
+
+ $role = strtolower(trim($role, '\\'));
+ if ($role === 'flagged') {
+ return 'important';
+ }
+
+ $allowed = ['all', 'archive', 'drafts', 'important', 'inbox', 'junk', 'sent', 'trash'];
+
+ return in_array($role, $allowed, true) ? $role : null;
+ }
+
+ private function normalizeSpecialUse(string $role): string {
+ $role = strtolower($role);
+
+ return $role === 'important' ? 'flagged' : $role;
+ }
+
+ private function convertToAcl(MailboxParametersResponse $response): ?string {
+ $rights = $response->rights();
+ if ($rights === null) {
+ return null;
+ }
+
+ /** @var string $acls */
+ $acls = '';
+ if ($rights->readItems()) {
+ $acls .= 'lr';
+ }
+ if ($rights->addItems()) {
+ $acls .= 'i';
+ }
+ if ($rights->removeItems()) {
+ $acls .= 'te';
+ }
+ if ($rights->setSeen()) {
+ $acls .= 's';
+ }
+ if ($rights->setKeywords()) {
+ $acls .= 'w';
+ }
+ if ($rights->createChild()) {
+ $acls .= 'k';
+ }
+ if ($rights->rename() || $rights->delete()) {
+ $acls .= 'x';
+ }
+ if ($rights->submit()) {
+ $acls .= 'p';
+ }
+ if ($rights->createChild() && $rights->rename() && $rights->delete()) {
+ $acls .= 'a';
+ }
+
+ return $acls === '' ? null : $acls;
+ }
+
+}
diff --git a/lib/JMAP/JmapMailboxConnector.php b/lib/JMAP/JmapMailboxConnector.php
new file mode 100644
index 0000000000..ef6d4e5b12
--- /dev/null
+++ b/lib/JMAP/JmapMailboxConnector.php
@@ -0,0 +1,318 @@
+getMailAccount()->getLastMailboxSync() >= ($this->timeFactory->getTime() - self::MAILBOX_SYNC_TTL)) {
+ $this->logger->debug('account is up to date, skipping mailbox sync');
+ return;
+ }
+
+ $this->jmapOperationsService->connect($account);
+ $remoteMailboxes = $this->jmapOperationsService->collectionList();
+ $localMailboxes = $this->mailboxMapper->findAll($account);
+ $remoteMailboxNames = $this->constructMailboxSyncNameLookup($remoteMailboxes);
+
+ // create or update mailboxes locally that are present remotely
+ foreach ($remoteMailboxes as $remoteMailbox) {
+ $remoteId = $remoteMailbox->getRemoteId();
+ $remoteMailboxName = ($remoteId !== null ? $remoteMailboxNames[$remoteId] ?? null : null) ?? $remoteMailbox->getName();
+ $remoteMailbox->setName($remoteMailboxName);
+ $remoteMailbox->setNameHash(md5($remoteMailboxName));
+
+ $localMailboxIdx = null;
+ $localMailboxData = null;
+ foreach ($localMailboxes as $key => $candidate) {
+ if ($candidate->getRemoteId() === $remoteMailbox->getRemoteId()) {
+ $localMailboxIdx = $key;
+ $localMailboxData = $candidate;
+ break;
+ }
+ }
+
+ if ($localMailboxData === null) {
+ $remoteMailbox->setAccountId($account->getId());
+ $this->mailboxMapper->insert($remoteMailbox);
+ } elseif ($localMailboxIdx !== null) {
+ $localMailbox = $this->mergeMailbox($localMailboxData, $remoteMailbox);
+ $this->mailboxMapper->update($localMailbox);
+ unset($localMailboxes[$localMailboxIdx]);
+ }
+ }
+ // delete local mailboxes that are not present remotely
+ if (count($localMailboxes) > 0) {
+ foreach ($localMailboxes as $mailbox) {
+ $this->mailboxMapper->delete($mailbox);
+ }
+ }
+
+ $this->dispatcher->dispatchTyped(new MailboxesSynchronizedEvent($account));
+ }
+
+ #[\Override]
+ public function syncOne(Account $account, Mailbox $mailbox): void {
+ $remoteId = $mailbox->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException('JMAP mailbox is missing a remote id');
+ }
+
+ $this->jmapOperationsService->connect($account);
+
+ $remoteMailbox = $this->jmapOperationsService->collectionFetch($remoteId);
+ if ($remoteMailbox === null) {
+ throw new ServiceException('JMAP mailbox could not be fetched from the remote');
+ }
+ $this->mailboxMapper->update($this->mergeMailbox($mailbox, $remoteMailbox, ['name', 'nameHash']));
+ }
+
+ #[\Override]
+ public function create(Account $account, string $name, array $specialUse = []): Mailbox {
+ $this->jmapOperationsService->connect($account);
+
+ // extract the mailbox name and parent name from the full path for remote operation
+ $pathParts = explode(self::DELIMITER, $name);
+ if (count($pathParts) === 1) {
+ $mailboxName = $name;
+ $parentName = null;
+ } else {
+ $mailboxName = array_pop($pathParts);
+ $parentName = implode(self::DELIMITER, $pathParts);
+ }
+ // find the parent mailbox to retrieve remote mailbox id for remote operation
+ if ($parentName !== null) {
+ try {
+ $location = $this->mailboxMapper->find($account, $parentName);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException('JMAP parent mailbox does not exist: ' . $parentName);
+ }
+
+ if ($location->getRemoteId() === null) {
+ throw new ServiceException('JMAP parent mailbox is missing a remote id: ' . $parentName);
+ }
+ } else {
+ $location = null;
+ }
+ // construct the mailbox for the remote and local creation
+ $mailbox = new Mailbox();
+ $mailbox->setAccountId($account->getId());
+ $mailbox->setDelimiter(self::DELIMITER);
+ $mailbox->setMessages(0);
+ $mailbox->setUnseen(0);
+ $mailbox->setSelectable(true);
+ $mailbox->setAttributes(json_encode(['\\subscribed'], JSON_THROW_ON_ERROR));
+ $mailbox->setSpecialUse(json_encode($specialUse, JSON_THROW_ON_ERROR));
+ // create in remote store, using only the mailbox
+ $mailbox->setName($mailboxName);
+ $mailbox = $this->jmapOperationsService->collectionCreate($location, $mailbox);
+ if ($mailbox === null) {
+ throw new ServiceException('JMAP mailbox creation failed');
+ }
+ // create in local store, using the full path name
+ $mailbox->setName($name);
+ $mailbox->setNameHash(md5($name));
+ $mailbox = $this->mailboxMapper->insert($mailbox);
+
+ return $mailbox;
+ }
+
+ #[\Override]
+ public function rename(Account $account, Mailbox $mailbox, string $newName): Mailbox {
+ $remoteId = $mailbox->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException('JMAP mailbox is missing a remote id');
+ }
+
+ $this->jmapOperationsService->connect($account);
+
+ // extract the mailbox name from the full path for remote operation
+ $pathParts = explode(self::DELIMITER, $newName);
+ if (count($pathParts) === 1) {
+ $mailboxName = $newName;
+ } else {
+ $mailboxName = array_pop($pathParts);
+ }
+ // update remote store, using only the mailbox name
+ $mailbox->setName($mailboxName);
+ $mailbox = $this->jmapOperationsService->collectionModify($remoteId, $mailbox, ['name']);
+ if ($mailbox === null) {
+ throw new ServiceException('JMAP mailbox rename failed');
+ }
+ // update local store, with the full path name
+ try {
+ $mailbox->setName($newName);
+ $mailbox->setNameHash(md5($newName));
+ return $this->mailboxMapper->update($mailbox);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException("The renamed mailbox $newName does not exist", 0, $e);
+ }
+ }
+
+ #[\Override]
+ public function delete(Account $account, Mailbox $mailbox): void {
+ $remoteId = $mailbox->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException('JMAP mailbox is missing a remote id');
+ }
+
+ $this->jmapOperationsService->connect($account);
+
+ // delete from remote store
+ $result = $this->jmapOperationsService->collectionDestroy($remoteId);
+ if ($result === null) {
+ throw new ServiceException('JMAP mailbox deletion failed');
+ }
+ // delete from local store
+ $this->mailboxMapper->delete($mailbox);
+ }
+
+ #[\Override]
+ public function subscribe(Account $account, Mailbox $mailbox, bool $subscribed): Mailbox {
+ $remoteId = $mailbox->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException('JMAP mailbox is missing a remote id');
+ }
+
+ $this->jmapOperationsService->connect($account);
+
+ // update subscription attribute
+ $attributes = json_decode($mailbox->getAttributes() ?? '[]', true);
+ if (!is_array($attributes)) {
+ $attributes = [];
+ }
+ if ($subscribed) {
+ $attributes[] = '\\subscribed';
+ } else {
+ $attributes = array_filter($attributes, static function ($attribute) {
+ return $attribute !== '\\subscribed';
+ });
+ }
+ $mailbox->setAttributes(json_encode(array_values(array_unique($attributes))));
+ // update remote store
+ $mailbox = $this->jmapOperationsService->collectionModify($remoteId, $mailbox, ['subscribed']);
+ if ($mailbox === null) {
+ throw new ServiceException('JMAP mailbox subscription update failed');
+ }
+ // update local store
+ try {
+ return $this->mailboxMapper->update($mailbox);
+ } catch (DoesNotExistException $e) {
+ throw new ServiceException('The updated mailbox does not exist', 0, $e);
+ }
+ }
+
+ /**
+ * @param Mailbox[] $remoteMailboxes
+ * @return array
+ */
+ private function constructMailboxSyncNameLookup(array $remoteMailboxes): array {
+ $mailboxesByRid = [];
+ foreach ($remoteMailboxes as $remoteMailbox) {
+ $rid = $remoteMailbox->getRemoteId();
+ if ($rid === null) {
+ continue;
+ }
+
+ $mailboxesByRid[$rid] = $remoteMailbox;
+ }
+
+ $lookup = [];
+ $visiting = [];
+ $resolveMailboxPath = function (string $rid) use (&$resolveMailboxPath, $mailboxesByRid, &$lookup, &$visiting): string {
+ if (isset($lookup[$rid])) {
+ return $lookup[$rid];
+ }
+
+ $mailbox = $mailboxesByRid[$rid];
+ if (isset($visiting[$rid])) {
+ $this->logger->warning('Detected cyclic JMAP mailbox parent relationship', [
+ 'rid' => $rid,
+ ]);
+
+ return $mailbox->getName();
+ }
+
+ $visiting[$rid] = true;
+ $path = $mailbox->getName();
+ $parentRid = $mailbox->getRemoteParentId();
+
+ if ($parentRid !== null) {
+ if (isset($mailboxesByRid[$parentRid])) {
+ $path = $resolveMailboxPath($parentRid) . self::DELIMITER . $path;
+ } else {
+ $this->logger->warning('JMAP mailbox parent missing from sync payload', [
+ 'rid' => $rid,
+ 'parentRid' => $parentRid,
+ ]);
+ }
+ }
+
+ unset($visiting[$rid]);
+ $lookup[$rid] = $path;
+
+ return $path;
+ };
+
+ foreach (array_keys($mailboxesByRid) as $rid) {
+ $resolveMailboxPath($rid);
+ }
+
+ return $lookup;
+ }
+
+ private function mergeMailbox(Mailbox $target, Mailbox $source, array $omit = []): Mailbox {
+ if (!in_array('name', $omit, true)) {
+ $target->setName($source->getName());
+ }
+ if (!in_array('nameHash', $omit, true)) {
+ $target->setNameHash($source->getNameHash());
+ }
+ $target->setRemoteId($source->getRemoteId());
+ $target->setAttributes($source->getAttributes());
+ $target->setDelimiter($source->getDelimiter());
+ $target->setMessages($source->getMessages());
+ $target->setUnseen($source->getUnseen());
+ $target->setSelectable($source->getSelectable() === true);
+ $target->setSpecialUse($source->getSpecialUse());
+ $target->setMyAcls($source->getMyAcls());
+ $target->setShared($source->isShared() === true);
+
+ return $target;
+ }
+
+}
diff --git a/lib/JMAP/JmapMessageAdapter.php b/lib/JMAP/JmapMessageAdapter.php
new file mode 100644
index 0000000000..55f225c429
--- /dev/null
+++ b/lib/JMAP/JmapMessageAdapter.php
@@ -0,0 +1,379 @@
+keywords();
+ $updatedAt = $source->parameter('updatedAt');
+
+ $message = new Message();
+ $message->setRemoteId($source->id());
+ $message->setMessageId($this->firstString($source->messageId()));
+ $message->setInReplyTo($this->firstString($source->inReplyTo()));
+ $message->setReferences($this->normalizeReferenceValue($source->references()));
+ $message->setThreadRootId($source->thread());
+ $message->setSubject($source->subject() ?? '');
+ $message->setSentAt($this->parseTimestamp($source->sent() ?? $source->received() ?? null));
+ $message->setFlagAnswered($source->answered() ?? false);
+ $message->setFlagDeleted($source->keyword('$deleted') ?? false);
+ $message->setFlagDraft($source->draft() ?? false);
+ $message->setFlagFlagged($source->flagged() ?? false);
+ $message->setFlagSeen($source->seen() ?? false);
+ $message->setFlagForwarded($source->forwarded() ?? false);
+ $message->setFlagJunk($source->junk() ?? false);
+ $message->setFlagNotjunk($source->notjunk() ?? false);
+ $message->setFlagImportant(($source->keyword('$label1') ?? false) || ($source->keyword(Tag::LABEL_IMPORTANT) ?? false));
+ $message->setFlagMdnsent($source->keyword('$mdnsent') ?? false);
+ $message->setPreviewText($source->bodyTextPreview());
+ $message->setFlagAttachments($source->hasAttachment() ?? false);
+ $message->setStructureAnalyzed(true);
+ $message->setUpdatedAt($this->parseTimestamp($updatedAt ?? $source->sent() ?? null));
+
+ $message->setFrom($this->convertAddressList($source->from() ?? $source->sender()));
+ $message->setTo($this->convertAddressList($source->to() ?? []));
+ $message->setCc($this->convertAddressList($source->cc() ?? []));
+ $message->setBcc($this->convertAddressList($source->bcc() ?? []));
+ $message->setTags($this->convertTags($keywords));
+
+ return $message;
+ }
+
+ public function convertToModelMessage(MailParametersResponse $source, int $uid, bool $loadBody): IMAPMessage {
+ // extract body, attachments and other related properties from the structure
+ [
+ 'plainBody' => $plainBody,
+ 'htmlBody' => $htmlBody,
+ 'attachments' => $attachments,
+ 'inlineAttachments' => $inlineAttachments,
+ 'isEncrypted' => $isEncrypted,
+ 'isSigned' => $isSigned,
+ 'isPgpMimeEncrypted' => $isPgpMimeEncrypted,
+ 'scheduling' => $scheduling,
+ ] = $this->extractStructureData($source, $uid, $loadBody);
+ /** @var list $inlineAttachments */
+ $dispositionNotificationTo = $this->firstHeaderValue($source, 'Disposition-Notification-To') ?? '';
+ $hasDkimSignature = $this->firstHeaderValue($source, 'DKIM-Signature') !== null;
+ [$unsubscribeUrl, $unsubscribeMailto] = $this->extractUnsubscribeTargets($source);
+ $isOneClickUnsubscribe = $unsubscribeUrl !== null
+ && str_contains(strtolower($this->firstHeaderValue($source, 'List-Unsubscribe-Post') ?? ''), 'one-click');
+ $flags = array_keys($source->keywords());
+
+ return new IMAPMessage(
+ $uid,
+ $this->firstString($source->messageId()) ?? '',
+ $flags,
+ $this->convertAddressList($source->from() ?? $source->sender()),
+ $this->convertAddressList($source->to() ?? []),
+ $this->convertAddressList($source->cc() ?? []),
+ $this->convertAddressList($source->bcc() ?? []),
+ $this->convertAddressList($source->replyTo() ?? []),
+ $source->subject() ?? '',
+ $plainBody,
+ $htmlBody,
+ $htmlBody !== '',
+ $attachments,
+ $inlineAttachments,
+ $attachments !== [] || $inlineAttachments !== [],
+ $scheduling,
+ new Horde_Imap_Client_DateTime('@' . $this->parseTimestamp($source->received() ?? $source->sent() ?? null)),
+ $this->normalizeRawMessageIdList($source->references()),
+ $dispositionNotificationTo,
+ $hasDkimSignature,
+ [],
+ $unsubscribeUrl,
+ $isOneClickUnsubscribe,
+ $unsubscribeMailto,
+ $this->firstString($source->inReplyTo()) ?? '',
+ $isEncrypted,
+ $isSigned,
+ false,
+ $this->htmlService,
+ $isPgpMimeEncrypted,
+ );
+ }
+
+ /**
+ * @param array> $messages
+ */
+ public function countUnreadMessages(array $messages): int {
+ $count = 0;
+ foreach ($messages as $message) {
+ if (($message['keywords']['$seen'] ?? false) !== true) {
+ $count++;
+ }
+ }
+
+ return $count;
+ }
+
+ private function convertAddressList(array $entries): AddressList {
+ $addresses = [];
+ foreach ($entries as $entry) {
+ $email = is_array($entry) ? ($entry['email'] ?? null) : null;
+ if (!is_string($email) || $email === '') {
+ continue;
+ }
+ $addresses[] = Address::fromRaw((string)($entry['name'] ?? $email), $email);
+ }
+
+ return new AddressList($addresses);
+ }
+
+ /**
+ * @param array $keywords
+ * @return Tag[]
+ */
+ private function convertTags(array $keywords): array {
+ $tags = [];
+ foreach ($keywords as $keyword => $value) {
+ if (!is_string($keyword) || $keyword === '' || $value !== true || $this->isReservedKeyword($keyword)) {
+ continue;
+ }
+
+ $tag = new Tag();
+ $tag->setImapLabel($keyword);
+ $tag->setDisplayName($keyword);
+ $tag->setColor('');
+ $tag->setIsDefaultTag(false);
+ $tags[] = $tag;
+ }
+
+ return $tags;
+ }
+
+ private function isReservedKeyword(string $keyword): bool {
+ return in_array($keyword, self::RESERVED_KEYWORDS, true);
+ }
+
+ private function parseTimestamp(mixed $value): int {
+ if (is_string($value) && $value !== '') {
+ $timestamp = strtotime($value);
+ if ($timestamp !== false) {
+ return $timestamp;
+ }
+ }
+
+ return time();
+ }
+
+ private function firstString(mixed $value): ?string {
+ if (is_string($value) && $value !== '') {
+ return $value;
+ }
+ if (is_array($value)) {
+ foreach ($value as $entry) {
+ if (is_string($entry) && $entry !== '') {
+ return $entry;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private function normalizeReferenceValue(mixed $references): ?string {
+ if ($references === null) {
+ return null;
+ }
+ if (is_string($references)) {
+ return $references;
+ }
+ if (is_array($references)) {
+ return json_encode(array_values(array_filter($references, static fn (mixed $value): bool => is_string($value) && $value !== '')));
+ }
+
+ return null;
+ }
+
+ private function normalizeRawMessageIdList(mixed $references): string {
+ if (!is_array($references)) {
+ return '';
+ }
+
+ return implode(' ', array_filter($references, static fn (mixed $value): bool => is_string($value) && $value !== ''));
+ }
+
+ private function firstHeaderValue(MailParametersResponse $source, string $name): ?string {
+ $value = $source->header($name, 'asText');
+ return $this->firstString($value);
+ }
+
+ /**
+ * @return array{0:?string,1:?string}
+ */
+ private function extractUnsubscribeTargets(MailParametersResponse $source): array {
+ $headerValue = $this->firstHeaderValue($source, 'List-Unsubscribe');
+ if ($headerValue === null) {
+ return [null, null];
+ }
+
+ $unsubscribeUrl = null;
+ $unsubscribeMailto = null;
+ foreach (preg_split('/\s*,\s*/', $headerValue) ?: [] as $entry) {
+ $target = trim($entry, " \t\n\r\0\x0B<>");
+ if ($target === '') {
+ continue;
+ }
+
+ $normalizedTarget = strtolower($target);
+ if ($unsubscribeMailto === null && str_starts_with($normalizedTarget, 'mailto:')) {
+ $unsubscribeMailto = $target;
+ continue;
+ }
+ if ($unsubscribeUrl === null && (str_starts_with($normalizedTarget, 'https://') || str_starts_with($normalizedTarget, 'http://'))) {
+ $unsubscribeUrl = $target;
+ }
+ }
+
+ return [$unsubscribeUrl, $unsubscribeMailto];
+ }
+
+ /**
+ * @return array{
+ * plainBody:string,
+ * htmlBody:string,
+ * attachments:array>,
+ * inlineAttachments:array>,
+ * isEncrypted:bool,
+ * isSigned:bool,
+ * isPgpMimeEncrypted:bool,
+ * scheduling:array>
+ * }
+ */
+ private function extractStructureData(MailParametersResponse $source, int $uid, bool $loadBody): array {
+ $state = [
+ 'plainBody' => '',
+ 'htmlBody' => '',
+ 'attachments' => [],
+ 'inlineAttachments' => [],
+ 'isEncrypted' => false,
+ 'isSigned' => false,
+ 'isPgpMimeEncrypted' => false,
+ 'scheduling' => [],
+ ];
+
+ $walk = function (MailPartResponse $part) use (&$walk, &$state, $source, $uid, $loadBody): void {
+ $partId = $part->id();
+ $blobId = $part->blob();
+ $type = strtolower($part->type() ?? '');
+ $disposition = strtolower($part->disposition() ?? '');
+ $content = is_string($partId) ? ($source->bodyPartValue($partId) ?? '') : '';
+
+ if ($type === 'multipart/encrypted' || $type === 'application/pkcs7-mime' || $type === 'application/x-pkcs7-mime') {
+ $state['isEncrypted'] = true;
+ }
+ if ($type === 'multipart/signed' || $type === 'application/pkcs7-signature' || $type === 'application/x-pkcs7-signature' || $type === 'application/pgp-signature') {
+ $state['isSigned'] = true;
+ }
+ if ($type === 'application/pgp-encrypted') {
+ $state['isEncrypted'] = true;
+ $state['isPgpMimeEncrypted'] = true;
+ }
+ if ($type === 'text/calendar') {
+ $state['scheduling'][] = [
+ 'id' => $partId,
+ 'mime' => $type,
+ 'fileName' => $part->name(),
+ 'method' => $this->extractCalendarMethod($content),
+ ];
+ }
+
+ if ($loadBody && $type === 'text/plain' && $state['plainBody'] === '') {
+ $state['plainBody'] = $content;
+ }
+ if ($loadBody && $type === 'text/html' && $state['htmlBody'] === '') {
+ $state['htmlBody'] = $content;
+ }
+
+ if ($loadBody && ($disposition === 'attachment' || $disposition === 'inline' || $type === 'text/calendar' || $type === 'application/ics')) {
+ $entry = [
+ 'id' => $blobId,
+ 'messageId' => $uid,
+ 'fileName' => $part->name(),
+ 'mime' => $type !== '' ? $type : 'application/octet-stream',
+ 'size' => $part->size() ?? 0,
+ 'cid' => $part->cid(),
+ 'disposition' => $part->disposition() ?? '',
+ ];
+ if ($disposition === 'inline') {
+ $state['inlineAttachments'][] = $entry;
+ } else {
+ $state['attachments'][] = $entry;
+ }
+ }
+
+ foreach ($part->parts() ?? [] as $subPart) {
+ if ($subPart instanceof MailPartResponse) {
+ $walk($subPart);
+ }
+ }
+ };
+
+ $bodyStructure = $source->bodyPartStructure();
+ if ($bodyStructure instanceof MailPartResponse) {
+ $walk($bodyStructure);
+ }
+
+ $preview = $source->bodyTextPreview();
+ if ($loadBody && $state['plainBody'] === '' && is_string($preview)) {
+ $state['plainBody'] = $preview;
+ }
+
+ return $state;
+ }
+
+ private function extractCalendarMethod(string $content): ?string {
+ if ($content === '') {
+ return null;
+ }
+
+ if (preg_match('/^METHOD:([^\r\n;]+)/mi', $content, $matches) !== 1) {
+ return null;
+ }
+
+ $method = trim($matches[1]);
+
+ return $method !== '' ? $method : null;
+ }
+}
diff --git a/lib/JMAP/JmapMessageConnector.php b/lib/JMAP/JmapMessageConnector.php
new file mode 100644
index 0000000000..d32c80b6d5
--- /dev/null
+++ b/lib/JMAP/JmapMessageConnector.php
@@ -0,0 +1,443 @@
+mailboxMapper->findAll($account) as $mailbox) {
+ $syncSent = $account->getMailAccount()->getSentMailboxId() === $mailbox->getId() || $mailbox->isSpecialUse('sent');
+ if (!$mailbox->isInbox() && !$mailbox->getSyncInBackground() && !$syncSent) {
+ $this->logger->debug('Skipping mailbox sync for ' . $mailbox->getId());
+ continue;
+ }
+
+ $this->logger->debug('Syncing ' . $mailbox->getId());
+ $this->syncMailbox($account, $mailbox, $this->logger, 0, null, $force);
+ $rebuildThreads = true;
+ }
+
+ $this->eventDispatcher->dispatchTyped(new SynchronizationEvent($account, $this->logger, $rebuildThreads));
+ }
+
+ #[\Override]
+ public function syncMailbox(Account $account, Mailbox $mailbox, LoggerInterface $logger, int $criteria, ?array $knownUids = null, bool $force = false): SyncResult {
+ if ($mailbox->getRemoteId() === null || $mailbox->getSelectable() === false) {
+ return new SyncResult(state: $mailbox->getState());
+ }
+
+ // fetch delta from remote store
+ $this->jmapOperationsService->connect($account);
+ $delta = $this->jmapOperationsService->entityDelta($mailbox->getRemoteId(), $mailbox->getState() ?? '');
+ if ($delta['state'] === $mailbox->getState()) {
+ return new SyncResult(state: $mailbox->getState());
+ }
+
+ $addedUids = [];
+ $addedMessages = [];
+ $modifiedUids = [];
+ $modifiedMessages = [];
+ $deletedUids = [];
+
+ // update local store - deletions
+ if (isset($delta['deletions']) && $delta['deletions'] !== []) {
+ $deletedMessages = $this->dbMessageMapper->findByRemoteIds($mailbox, $delta['deletions']);
+ foreach ($deletedMessages as $key => $message) {
+ $deletedUids[] = $message->getUid();
+ unset($deletedMessages[$key]);
+ }
+ $this->dbMessageMapper->deleteByRemoteIds($mailbox, ...$delta['deletions']);
+ }
+
+ $deltaIds = array_values(array_unique(array_merge($delta['additions'] ?? [], $delta['modifications'] ?? [])));
+ $remoteMessages = $deltaIds === [] ? [] : $this->jmapOperationsService->entityFetchMessage(...$deltaIds);
+ $localMessages = $this->dbMessageMapper->findByRemoteIds($mailbox, $deltaIds);
+ $localMessages = $this->mapMessagesByRemoteId(...$localMessages);
+
+ $nextUid = ($this->dbMessageMapper->findHighestUid($mailbox) ?? 0) + 1;
+
+ foreach (array_keys($remoteMessages) as $remoteId) {
+ $remoteMessage = $remoteMessages[$remoteId];
+ $localMessage = $localMessages[$remoteId] ?? null;
+ $uid = $localMessage?->getUid() ?? $nextUid++;
+ if ($localMessage !== null) {
+ $modifiedUids[] = $uid;
+ $modifiedMessages[] = $this->mergeMessage($localMessage, $remoteMessage);
+ } else {
+ $remoteMessage->setMailboxId($mailbox->getId());
+ $remoteMessage->setUid($uid);
+ $addedUids[] = $uid;
+ $addedMessages[] = $remoteMessage;
+ }
+ unset($remoteMessages[$remoteId]);
+ unset($localMessages[$remoteId]);
+ }
+
+ if ($addedMessages !== []) {
+ $this->dbMessageMapper->insertBulk($account, ...$addedMessages);
+ }
+ if ($modifiedMessages !== []) {
+ $this->dbMessageMapper->updateBulk($account, true, ...$modifiedMessages);
+ }
+
+ $mailbox->setState($delta['state']);
+ $this->mailboxMapper->update($mailbox);
+
+ return new SyncResult(
+ new: $addedUids,
+ modified: $modifiedUids,
+ deleted: $deletedUids,
+ state: $mailbox->getState(),
+ stats: ['rebuildThreads' => true],
+ );
+ }
+
+ #[\Override]
+ public function fetchMessages(Account $account, Mailbox $mailbox, bool $loadBody = false, Message ...$messages): array {
+ $messages = $this->mapMessagesByRemoteId(...$messages);
+ // retrieve message details from remote store
+ $this->jmapOperationsService->connect($account);
+ $remoteMessages = $this->jmapOperationsService->entityFetchNative(...array_keys($messages));
+ // convert to model messages and preserve UIDs from local store
+ $modelMessages = [];
+ foreach ($remoteMessages as $remoteId => $remoteMessage) {
+ $modelMessages[$remoteId] = $this->jmapMessageAdapter->convertToModelMessage($remoteMessage, $messages[$remoteId]->getUid(), $loadBody);
+ }
+ return $modelMessages;
+ }
+
+ #[\Override]
+ public function findMessages(Account $account, Mailbox $mailbox, SearchQuery $searchQuery): array {
+ if ($mailbox->getRemoteId() === null) {
+ return [];
+ }
+
+ $this->jmapOperationsService->connect($account);
+ $results = $this->jmapOperationsService->entityList(
+ $mailbox->getRemoteId(),
+ $this->convertSearchQueryToFilters($searchQuery),
+ [],
+ null,
+ 'basic',
+ );
+ $messages = $this->dbMessageMapper->findByRemoteIds($mailbox, array_keys($results['list']));
+
+ return array_map(
+ static fn (Message $message): int => $message->getUid(),
+ $messages,
+ );
+ }
+
+ #[\Override]
+ public function fetchMessageRaw(Account $account, Mailbox $mailbox, Message $message): ?string {
+ $remoteId = $message->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException("Message {$message->getId()} does not have a remote id");
+ }
+ // retrieve from remote store
+ $this->jmapOperationsService->connect($account);
+ return $this->jmapOperationsService->entityFetchRaw($remoteId);
+ }
+
+ /**
+ * @return Attachment[]
+ */
+ #[\Override]
+ public function fetchAttachments(Account $account, Mailbox $mailbox, Message $message): array {
+ $remoteId = $message->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException("Message {$message->getId()} does not have a remote id");
+ }
+ // retrieve from remote store
+ $this->jmapOperationsService->connect($account);
+ return $this->jmapOperationsService->attachmentFetch($remoteId);
+ }
+
+ #[\Override]
+ public function fetchAttachment(Account $account, Mailbox $mailbox, Message $message, string $attachmentId): Attachment {
+ $remoteId = $message->getRemoteId();
+ if ($remoteId === null) {
+ throw new ServiceException("Message {$message->getId()} does not have a remote id");
+ }
+ // retrieve from remote store
+ $this->jmapOperationsService->connect($account);
+ $attachment = $this->jmapOperationsService->attachmentFetch($remoteId, $attachmentId)[0] ?? null;
+
+ if ($attachment === null) {
+ throw new ServiceException("Attachment $attachmentId for message {$message->getId()} could not be retrieved from server");
+ }
+ return $attachment;
+ }
+
+ #[\Override]
+ public function moveMessages(Account $account, Mailbox $targetMailbox, Mailbox $sourceMailbox, Message ...$messages): array {
+ $targetRemoteId = $targetMailbox->getRemoteId();
+ if ($targetRemoteId === null) {
+ throw new ServiceException("Destination mailbox {$targetMailbox->getId()} does not have a remote id");
+ }
+ $messages = $this->mapMessagesByRemoteId(...$messages);
+ // update remote store
+ $this->jmapOperationsService->connect($account);
+ $results = $this->jmapOperationsService->entityMove($targetRemoteId, ...array_keys($messages));
+ // compute mutated messages with new mailbox id and uid if move was successful
+ $mutatedMessages = [];
+ $nextUid = ($this->dbMessageMapper->findHighestUid($targetMailbox) ?? 0) + 1;
+ foreach ($results as $remoteId => $status) {
+ if (!isset($messages[$remoteId]) || $status !== true) {
+ continue;
+ }
+ $messages[$remoteId]->setMailboxId($targetMailbox->getId());
+ $messages[$remoteId]->setUid($nextUid++);
+ $mutatedMessages[] = $messages[$remoteId];
+ }
+
+ return $mutatedMessages;
+ }
+
+ #[\Override]
+ public function deleteMessages(Account $account, Mailbox $mailbox, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $messages = $this->mapMessagesByRemoteId(...$messages);
+ // update remote store
+ $this->jmapOperationsService->connect($account);
+ $results = $this->jmapOperationsService->entityDelete(...array_keys($messages));
+ // collect the messages that were successfully deleted on the remote
+ $mutatedMessages = [];
+ foreach ($results as $remoteId => $status) {
+ if (!isset($messages[$remoteId]) || $status !== true) {
+ continue;
+ }
+ $mutatedMessages[] = $messages[$remoteId];
+ }
+
+ return $mutatedMessages;
+ }
+
+ #[\Override]
+ public function flagMessages(Account $account, Mailbox $mailbox, string $flag, bool $value, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $messages = $this->mapMessagesByRemoteId(...$messages);
+ $flag = $this->normalizeFlagForRemote($flag);
+ // update remote store
+ $this->jmapOperationsService->connect($account);
+ $results = $this->jmapOperationsService->entityModifyFlags([$flag => $value], ...array_keys($messages));
+
+ $mutatedMessages = [];
+ foreach ($messages as $remoteId => $message) {
+ if (($results[$remoteId] ?? false) !== true) {
+ throw new ServiceException("Message {$message->getUid()} could not be flagged on remote");
+ }
+ $this->applyFlagValue($message, $flag, $value);
+ $mutatedMessages[] = $message;
+ }
+
+ return $mutatedMessages;
+ }
+
+ #[\Override]
+ public function tagMessages(Account $account, Mailbox $mailbox, Tag $tag, bool $value, Message ...$messages): array {
+ return $this->flagMessages($account, $mailbox, $tag->getImapLabel(), $value, ...$messages);
+ }
+
+ #[\Override]
+ public function getQuota(Account $account): ?Quota {
+ return null;
+ }
+
+ #[\Override]
+ public function clearCache(Account $account, Mailbox $mailbox): void {
+ $this->dbMessageMapper->deleteAll($mailbox);
+ $mailbox->setState(null);
+ $this->mailboxMapper->update($mailbox);
+ }
+
+ #[\Override]
+ public function repairSync(Account $account, Mailbox $mailbox): void {
+ $this->clearCache($account, $mailbox);
+ $this->logger->debug('Repairing JMAP mailbox cache for ' . $mailbox->getId());
+ $this->syncMailbox($account, $mailbox, $this->logger, 0, null, true);
+ }
+
+ #[\Override]
+ public function isPermflagsEnabled(Account $account, Mailbox $mailbox): bool {
+ return true;
+ }
+
+ /**
+ * @return list
+ */
+ private function convertSearchQueryToFilters(SearchQuery $searchQuery): array {
+ $filters = [];
+ foreach ($searchQuery->getBodies() as $textToken) {
+ $filters[] = [
+ 'attribute' => 'body',
+ 'value' => $textToken,
+ ];
+ }
+
+ return $filters;
+ }
+
+ /**
+ * @return array
+ */
+ private function mapMessagesByRemoteId(Message ...$messages): array {
+ $mapped = [];
+ foreach ($messages as $message) {
+ $rid = $message->getRemoteId();
+ if ($rid === null) {
+ throw new ServiceException("Message {$message->getId()} does not have a remote id");
+ }
+ $mapped[$rid] = $message;
+ }
+ return $mapped;
+ }
+
+ private function mergeMessage(Message $target, Message $source): Message {
+ $target->setRemoteId($source->getRemoteId());
+ $target->setMessageId($source->getMessageId());
+ $target->setInReplyTo($source->getInReplyTo());
+ $target->setReferences($source->getReferences());
+ $target->setThreadRootId($source->getThreadRootId());
+ $target->setSubject($source->getSubject());
+ $target->setSentAt($source->getSentAt());
+ $target->setFlagAnswered($source->getFlagAnswered() === true);
+ $target->setFlagDeleted($source->getFlagDeleted() === true);
+ $target->setFlagDraft($source->getFlagDraft() === true);
+ $target->setFlagFlagged($source->getFlagFlagged() === true);
+ $target->setFlagSeen($source->getFlagSeen() === true);
+ $target->setFlagForwarded($source->getFlagForwarded() === true);
+ $target->setFlagJunk($source->getFlagJunk() === true);
+ $target->setFlagNotjunk($source->getFlagNotjunk() === true);
+ $target->setFlagImportant($source->getFlagImportant() === true);
+ $target->setFlagMdnsent($source->getFlagMdnsent() === true);
+ $target->setPreviewText($source->getPreviewText());
+ $target->setFlagAttachments($source->getFlagAttachments());
+ $target->setStructureAnalyzed($source->getStructureAnalyzed() === true);
+ $target->setUpdatedAt($source->getUpdatedAt());
+ $target->setFrom($source->getFrom());
+ $target->setTo($source->getTo());
+ $target->setCc($source->getCc());
+ $target->setBcc($source->getBcc());
+ $target->setTags($source->getTags());
+
+ return $target;
+ }
+
+ private function findLocalMessageByUid(Mailbox $mailbox, int $uid): Message {
+ $messages = $this->dbMessageMapper->findByUids($mailbox, [$uid]);
+ if ($messages === []) {
+ throw new ServiceException("Message $uid does not exist locally");
+ }
+
+ return $messages[0];
+ }
+
+ private function normalizeFlagForRemote(string $flag): string {
+ return match ($flag) {
+ 'seen' => '$seen',
+ 'flagged' => '$flagged',
+ 'deleted' => '$deleted',
+ 'draft' => '$draft',
+ 'answered' => '$answered',
+ 'forwarded' => '$forwarded',
+ 'junk' => '$junk',
+ 'notjunk' => '$notjunk',
+ 'mdnsent' => '$mdnsent',
+ 'important' => Tag::LABEL_IMPORTANT,
+ default => $flag,
+ };
+ }
+
+ private function applyFlagValue(Message $message, string $flag, bool $value): void {
+ switch ($flag) {
+ case '$seen':
+ case '$flagged':
+ case '$deleted':
+ case '$draft':
+ case '$answered':
+ case '$forwarded':
+ $message->setFlag(ltrim($flag, '$'), $value);
+ break;
+ case '$junk':
+ case '$notjunk':
+ case '$phishing':
+ case '$mdnsent':
+ case Tag::LABEL_IMPORTANT:
+ $message->setFlag($flag, $value);
+ break;
+ default:
+ $this->applyTagValue($message, $flag, $value);
+ break;
+ }
+ }
+
+ private function applyTagValue(Message $message, string $flag, bool $value): void {
+ $tags = $message->getTags();
+
+ if ($value) {
+ foreach ($tags as $tag) {
+ if ($tag->getImapLabel() === $flag) {
+ return;
+ }
+ }
+
+ $tag = new Tag();
+ $tag->setImapLabel($flag);
+ $tag->setDisplayName($flag);
+ $tag->setColor('');
+ $tag->setIsDefaultTag(false);
+ $tags[] = $tag;
+ } else {
+ $tags = array_values(array_filter(
+ $tags,
+ static fn (Tag $tag): bool => $tag->getImapLabel() !== $flag,
+ ));
+ }
+
+ $message->setTags($tags);
+ }
+
+}
diff --git a/lib/Listener/DeleteDraftListener.php b/lib/Listener/DeleteDraftListener.php
index c858495c9a..a15cd4d9a2 100644
--- a/lib/Listener/DeleteDraftListener.php
+++ b/lib/Listener/DeleteDraftListener.php
@@ -19,8 +19,8 @@
use OCA\Mail\Events\DraftSavedEvent;
use OCA\Mail\Events\MessageDeletedEvent;
use OCA\Mail\Events\OutboxMessageCreatedEvent;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
@@ -35,7 +35,7 @@ class DeleteDraftListener implements IEventListener {
private $eventDispatcher;
public function __construct(
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
private MailboxMapper $mailboxMapper,
private MessageMapper $messageMapper,
private LoggerInterface $logger,
@@ -56,7 +56,7 @@ public function handle(Event $event): void {
* @param Message $draft
*/
private function deleteDraft(Account $account, Message $draft): void {
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$draftsMailbox = $this->getDraftsMailbox($account);
} catch (DoesNotExistException $e) {
diff --git a/lib/Listener/MoveJunkListener.php b/lib/Listener/MoveJunkListener.php
index ddcb224219..0cf7514ca6 100644
--- a/lib/Listener/MoveJunkListener.php
+++ b/lib/Listener/MoveJunkListener.php
@@ -9,10 +9,12 @@
namespace OCA\Mail\Listener;
-use OCA\Mail\Contracts\IMailManager;
+use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Events\MessageFlaggedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Service\MailManager;
+use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
@@ -22,7 +24,8 @@
*/
class MoveJunkListener implements IEventListener {
public function __construct(
- private IMailManager $mailManager,
+ private MailManager $mailManager,
+ private MailboxMapper $mailboxMapper,
private LoggerInterface $logger,
) {
}
@@ -42,6 +45,16 @@ public function handle(Event $event): void {
}
$mailbox = $event->getMailbox();
+ $messageId = $this->mailManager->getMessageIdForUid($mailbox, $event->getUid());
+ if ($messageId === null) {
+ return;
+ }
+
+ try {
+ $message = $this->mailManager->getMessage($account->getUserId(), $messageId);
+ } catch (DoesNotExistException) {
+ return;
+ }
if ($event->isSet() && $junkMailboxId !== $mailbox->getId()) {
try {
@@ -57,10 +70,10 @@ public function handle(Event $event): void {
try {
$this->mailManager->moveMessage(
$account,
- $mailbox->getName(),
- $event->getUid(),
+ $mailbox,
+ $message,
$account,
- $junkMailbox->getName(),
+ $junkMailbox,
);
} catch (ServiceException $e) {
$this->logger->error('move message to junk mailbox failed. account_id: {account_id}', [
@@ -68,14 +81,19 @@ public function handle(Event $event): void {
'account_id' => $account->getId(),
]);
}
- } elseif (!$event->isSet() && $mailbox->getName() !== 'INBOX') {
+ } elseif (!$event->isSet() && !$mailbox->isInbox()) {
+ $inboxMailbox = $this->mailboxMapper->findSpecialUseMailbox($account, 'inbox');
+ if ($inboxMailbox === null) {
+ return;
+ }
+
try {
$this->mailManager->moveMessage(
$account,
- $mailbox->getName(),
- $event->getUid(),
+ $mailbox,
+ $message,
$account,
- 'INBOX',
+ $inboxMailbox,
);
} catch (ServiceException $e) {
$this->logger->error('move message to inbox failed. account_id: {account_id}', [
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index f428c4db6e..cd2d373ecb 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -433,6 +433,10 @@ public function isSignatureValid(): bool {
return $this->signatureIsValid;
}
+ public function hasDkimSignature(): bool {
+ return $this->hasDkimSignature;
+ }
+
public function getUnsubscribeUrl(): ?string {
return $this->unsubscribeUrl;
}
diff --git a/lib/Protocol/ProtocolFactory.php b/lib/Protocol/ProtocolFactory.php
index b226f4bb0e..e973172565 100644
--- a/lib/Protocol/ProtocolFactory.php
+++ b/lib/Protocol/ProtocolFactory.php
@@ -9,6 +9,7 @@
namespace OCA\Mail\Protocol;
+use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Socket;
use JmapClient\Client as JmapClient;
use OCA\Mail\Account;
@@ -18,7 +19,13 @@
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\IMAP\ImapMailboxConnector;
+use OCA\Mail\IMAP\ImapMessageConnector;
+use OCA\Mail\IMAP\ImapTransmissionConnector;
+use OCA\Mail\JMAP\Exception\JmapTransportException;
use OCA\Mail\JMAP\JmapClientFactory;
+use OCA\Mail\JMAP\JmapMailboxConnector;
+use OCA\Mail\JMAP\JmapMessageConnector;
use Psr\Container\ContainerInterface;
class ProtocolFactory {
@@ -27,16 +34,16 @@ class ProtocolFactory {
* Maps protocol => connector interface => class name
*/
private const CONNECTOR_MAP = [
- // MailAccount::PROTOCOL_IMAP => [
- // IMailboxConnector::class => ImapMailboxConnector::class,
- // IMessageConnector::class => ImapMessageConnector::class,
- // ITransmissionConnector::class => ImapTransmissionConnector::class,
- // ],
- // MailAccount::PROTOCOL_JMAP => [
- // IMailboxConnector::class => JmapMailboxConnector::class,
- // IMessageConnector::class => JmapMessageConnector::class,
- // ITransmissionConnector::class => JmapTransmissionConnector::class,
- // ],
+ MailAccount::PROTOCOL_IMAP => [
+ IMailboxConnector::class => ImapMailboxConnector::class,
+ IMessageConnector::class => ImapMessageConnector::class,
+ //ITransmissionConnector::class => ImapTransmissionConnector::class,
+ ],
+ MailAccount::PROTOCOL_JMAP => [
+ IMailboxConnector::class => JmapMailboxConnector::class,
+ IMessageConnector::class => JmapMessageConnector::class,
+ //ITransmissionConnector::class => JmapTransmissionConnector::class,
+ ],
];
public function __construct(
@@ -62,6 +69,30 @@ public function jmapClient(Account $account): JmapClient {
return $this->jmapClientFactory->getClient($account);
}
+ /**
+ * @throws ServiceException
+ * @throws Horde_Imap_Client_Exception
+ * @throws JmapTransportException
+ */
+ public function testConnection(Account $account): void {
+ $protocol = $account->getMailAccount()->getProtocol();
+
+ if ($protocol === MailAccount::PROTOCOL_IMAP) {
+ $this->imapClient($account)->close();
+ return;
+ }
+
+ if ($protocol === MailAccount::PROTOCOL_JMAP) {
+ $client = $this->jmapClient($account);
+ if (!$client->sessionStatus()) {
+ $client->connect();
+ }
+ return;
+ }
+
+ throw new ServiceException("Unsupported protocol $protocol");
+ }
+
/**
* @throws ServiceException
*/
@@ -102,7 +133,9 @@ public function transmissionConnector(Account $account): ITransmissionConnector
*/
private function resolveConnector(Account $account, string $interface): mixed {
$protocol = $account->getMailAccount()->getProtocol();
- $class = self::CONNECTOR_MAP[$protocol][$interface] ?? null;
+ /** @var array> $map */
+ $map = self::CONNECTOR_MAP;
+ $class = $map[$protocol][$interface] ?? null;
if ($class === null) {
throw new ServiceException("No $interface implementation for protocol $protocol");
diff --git a/lib/Send/Chain.php b/lib/Send/Chain.php
index 5e7d60c204..bc485d1948 100644
--- a/lib/Send/Chain.php
+++ b/lib/Send/Chain.php
@@ -12,7 +12,7 @@
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\DB\Exception;
@@ -25,7 +25,7 @@ public function __construct(
private FlagRepliedMessageHandler $flagRepliedMessageHandler,
private AttachmentService $attachmentService,
private LocalMessageMapper $localMessageMapper,
- private IMAPClientFactory $clientFactory,
+ private ProtocolFactory $protocolFactory,
) {
}
@@ -50,7 +50,7 @@ public function process(Account $account, LocalMessage $localMessage): LocalMess
throw new ServiceException('Could not send message because a previous send operation produced an unclear sent state.');
}
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$result = $handlers->process($account, $localMessage, $client);
} finally {
diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php
index 256b546e98..900f3db43a 100644
--- a/lib/Service/AccountService.php
+++ b/lib/Service/AccountService.php
@@ -22,7 +22,7 @@
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
@@ -45,7 +45,7 @@ public function __construct(
private MailAccountMapper $mapper,
private AliasesService $aliasesService,
IJobList $jobList,
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
private readonly IConfig $config,
private readonly ITimeFactory $timeFactory,
private DelegationMapper $delegationMapper,
@@ -234,8 +234,7 @@ public function getAllAcounts(): array {
public function testAccountConnection(string $currentUserId, int $accountId) :bool {
$account = $this->find($currentUserId, $accountId);
try {
- $client = $this->imapClientFactory->getClient($account);
- $client->close();
+ $this->protocolFactory->testConnection($account);
return true;
} catch (\Throwable $e) {
return false;
diff --git a/lib/Service/AiIntegrations/AiIntegrationsService.php b/lib/Service/AiIntegrations/AiIntegrationsService.php
index e482144385..eb5d3334ac 100644
--- a/lib/Service/AiIntegrations/AiIntegrationsService.php
+++ b/lib/Service/AiIntegrations/AiIntegrationsService.php
@@ -13,14 +13,13 @@
use OCA\Mail\Account;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\ConfigLexicon;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
use OCA\Mail\Exception\PotentialPromptInjectionException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\EventData;
use OCA\Mail\Model\IMAPMessage;
+use OCA\Mail\Service\MailManager;
use OCP\IAppConfig;
use OCP\IUserManager;
use OCP\L10N\IFactory;
@@ -42,8 +41,7 @@ class AiIntegrationsService {
public function __construct(
private LoggerInterface $logger,
private Cache $cache,
- private IMAPClientFactory $clientFactory,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private TaskProcessingManager $taskProcessingManager,
private IFactory $l10nFactory,
private IUserManager $userManager,
@@ -79,52 +77,45 @@ public function summarizeMessages(Account $account, array $messages): void {
}
$user = $this->userManager->get($account->getUserId());
$language = explode('_', $this->l10nFactory->getUserLanguage($user))[0];
- $client = $this->clientFactory->getClient($account);
- try {
- foreach ($messages as $entry) {
- if (mb_strlen((string)$entry->getSummary()) !== 0) {
- continue;
- }
- // retrieve full message from server
- $userId = $account->getUserId();
- $mailboxId = $entry->getMailboxId();
- $messageLocalId = $entry->getId();
- $messageRemoteId = $entry->getUid();
- $mailbox = $this->mailManager->getMailbox($userId, $mailboxId);
- $message = $this->mailManager->getImapMessage(
- $client,
- $account,
- $mailbox,
- $messageRemoteId,
- true
- );
- // skip message if it is encrypted or empty
- if ($message->isEncrypted() || empty(trim($message->getPlainBody()))) {
- continue;
- }
- $messageBody = $message->getPlainBody();
- try {
- self::assertNoPromptInjection($messageBody);
- } catch (PotentialPromptInjectionException $e) {
- $this->logger->warning('Skipped message summary: potential prompt injection', ['exception' => $e, 'messageId' => $messageLocalId]);
- continue;
- }
- // construct prompt and task
- $prompt = sprintf(DefaultPrompts::SUMMARIZE_MESSAGE, $language, $messageBody);
- $task = new TaskProcessingTask(
- TextToText::ID,
- [
- 'max_tokens' => 1024,
- 'input' => $prompt,
- ],
- Application::APP_ID,
- $userId,
- 'message:' . (string)$messageLocalId
- );
- $this->taskProcessingManager->scheduleTask($task);
+ foreach ($messages as $entry) {
+ if (mb_strlen((string)$entry->getSummary()) !== 0) {
+ continue;
+ }
+ // retrieve full message from server
+ $userId = $account->getUserId();
+ $mailboxId = $entry->getMailboxId();
+ $messageLocalId = $entry->getId();
+ $mailbox = $this->mailManager->getMailbox($userId, $mailboxId);
+ $message = $this->mailManager->getImapMessage(
+ $account,
+ $mailbox,
+ $entry,
+ true
+ );
+ // skip message if it is encrypted or empty
+ if ($message->isEncrypted() || empty(trim($message->getPlainBody()))) {
+ continue;
}
- } finally {
- $client->logout();
+ $messageBody = $message->getPlainBody();
+ try {
+ self::assertNoPromptInjection($messageBody);
+ } catch (PotentialPromptInjectionException $e) {
+ $this->logger->warning('Skipped message summary: potential prompt injection', ['exception' => $e, 'messageId' => $messageLocalId]);
+ continue;
+ }
+ // construct prompt and task
+ $prompt = sprintf(DefaultPrompts::SUMMARIZE_MESSAGE, $language, $messageBody);
+ $task = new TaskProcessingTask(
+ TextToText::ID,
+ [
+ 'max_tokens' => 1024,
+ 'input' => $prompt,
+ ],
+ Application::APP_ID,
+ $userId,
+ 'message:' . (string)$messageLocalId
+ );
+ $this->taskProcessingManager->scheduleTask($task);
}
}
@@ -145,15 +136,14 @@ public function summarizeThread(Account $account, string $threadId, array $messa
if ($cachedSummary) {
return $cachedSummary;
}
- $client = $this->clientFactory->getClient($account);
try {
- $messagesBodies = array_map(function ($message) use ($client, $account, $currentUserId) {
+ $messagesBodies = array_map(function ($message) use ($account, $currentUserId) {
$mailbox = $this->mailManager->getMailbox($currentUserId, $message->getMailboxId());
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(), true
+ $message,
+ true
);
$body = $imapMessage->getPlainBody();
self::assertNoPromptInjection($body);
@@ -162,8 +152,6 @@ public function summarizeThread(Account $account, string $threadId, array $messa
} catch (PotentialPromptInjectionException $e) {
$this->logger->warning('Skipped thread summary: potential prompt injection', ['exception' => $e, 'threadId' => $threadId]);
return null;
- } finally {
- $client->logout();
}
$taskPrompt = implode("\n", $messagesBodies);
@@ -196,15 +184,14 @@ public function generateEventData(Account $account, string $threadId, array $mes
if (!isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToText::ID])) {
return null;
}
- $client = $this->clientFactory->getClient($account);
try {
- $messageBodies = array_map(function ($message) use ($client, $account, $currentUserId) {
+ $messageBodies = array_map(function ($message) use ($account, $currentUserId) {
$mailbox = $this->mailManager->getMailbox($currentUserId, $message->getMailboxId());
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(), true
+ $message,
+ true
);
$body = $imapMessage->getPlainBody();
self::assertNoPromptInjection($body);
@@ -213,8 +200,6 @@ public function generateEventData(Account $account, string $threadId, array $mes
} catch (PotentialPromptInjectionException $e) {
$this->logger->warning('Skipped event data generation: potential prompt injection', ['exception' => $e, 'threadId' => $threadId]);
return null;
- } finally {
- $client->logout();
}
$task = new TaskProcessingTask(
@@ -258,13 +243,12 @@ public function getSmartReply(Account $account, Mailbox $mailbox, Message $messa
throw new ServiceException('Failed to decode smart replies JSON output', previous: $e);
}
}
- $client = $this->clientFactory->getClient($account);
try {
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(), true
+ $message,
+ true
);
if (!$this->isPersonalEmail($imapMessage)) {
return [];
@@ -274,8 +258,6 @@ public function getSmartReply(Account $account, Mailbox $mailbox, Message $messa
} catch (PotentialPromptInjectionException $e) {
$this->logger->warning('Skipped smart replies: potential prompt injection', ['exception' => $e, 'messageId' => $message->getId()]);
return [];
- } finally {
- $client->logout();
}
$prompt = sprintf(DefaultPrompts::SMART_REPLY, $messageBody);
$task = new TaskProcessingTask(TextToText::ID, ['input' => $prompt], Application::APP_ID, $currentUserId);
@@ -321,18 +303,12 @@ public function requiresFollowUp(
throw new ServiceException('No language model available for smart replies');
}
- $client = $this->clientFactory->getClient($account);
- try {
- $imapMessage = $this->mailManager->getImapMessage(
- $client,
- $account,
- $mailbox,
- $message->getUid(),
- true,
- );
- } finally {
- $client->logout();
- }
+ $imapMessage = $this->mailManager->getImapMessage(
+ $account,
+ $mailbox,
+ $message,
+ true,
+ );
if (!$this->isPersonalEmail($imapMessage)) {
return false;
diff --git a/lib/Service/AntiSpamService.php b/lib/Service/AntiSpamService.php
index efb1d6b8ec..c0ec727249 100644
--- a/lib/Service/AntiSpamService.php
+++ b/lib/Service/AntiSpamService.php
@@ -21,9 +21,9 @@
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Model\Message;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\DataUri\DataUriParser;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -36,7 +36,7 @@ class AntiSpamService {
public function __construct(
private MessageMapper $dbMessageMapper,
private MailManager $mailManager,
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
private SmtpClientFactory $smtpClientFactory,
private ImapMessageMapper $messageMapper,
private LoggerInterface $logger,
@@ -120,7 +120,7 @@ public function sendReportEmail(Account $account, Mailbox $mailbox, int $uid, st
$mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId());
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$fullText = $this->messageMapper->getFullText(
$client,
@@ -198,7 +198,7 @@ public function sendReportEmail(Account $account, Mailbox $mailbox, int $uid, st
return;
}
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$this->messageMapper->save(
$client,
diff --git a/lib/Service/Attachment/AttachmentService.php b/lib/Service/Attachment/AttachmentService.php
index 8f718d007c..149a1ef1d1 100644
--- a/lib/Service/Attachment/AttachmentService.php
+++ b/lib/Service/Attachment/AttachmentService.php
@@ -14,7 +14,6 @@
use OCA\Files_Sharing\SharedStorage;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalAttachmentMapper;
use OCA\Mail\Db\LocalMessage;
@@ -25,6 +24,7 @@
use OCA\Mail\Exception\SmimeDecryptException;
use OCA\Mail\Exception\UploadException;
use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\File;
@@ -50,7 +50,7 @@ public function __construct(
$userFolder,
private LocalAttachmentMapper $mapper,
private AttachmentStorage $storage,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private MessageMapper $messageMapper,
private ICacheFactory $cacheFactory,
private IURLGenerator $urlGenerator,
@@ -252,7 +252,7 @@ public function handleAttachments(Account $account, array $attachments, \Horde_I
/**
* @return list
*/
- public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $message, \Horde_Imap_Client_Socket $client): array {
+ public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $message): array {
if ($message->getStructureAnalyzed() === true && $message->getFlagAttachments() === false) {
// Structure analysis already confirmed no attachments, nothing to fetch.
return [];
@@ -276,10 +276,9 @@ public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $
$attachments = [];
try {
$imapMessage = $this->mailManager->getImapMessage(
- $client,
$account,
$mailbox,
- $message->getUid(),
+ $message,
true
);
$attachments = $imapMessage->getAttachments();
diff --git a/lib/Service/Classification/NewMessagesClassifier.php b/lib/Service/Classification/NewMessagesClassifier.php
index 449157531f..79cf7903a5 100644
--- a/lib/Service/Classification/NewMessagesClassifier.php
+++ b/lib/Service/Classification/NewMessagesClassifier.php
@@ -11,13 +11,13 @@
use Horde_Imap_Client;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\Tag;
use OCA\Mail\Db\TagMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Service\MailManager;
use Psr\Log\LoggerInterface;
class NewMessagesClassifier {
@@ -33,7 +33,7 @@ public function __construct(
private ImportanceClassifier $classifier,
private TagMapper $tagMapper,
private LoggerInterface $logger,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
) {
}
@@ -88,8 +88,8 @@ public function classifyNewMessages(
$this->logger->info("Message {$message->getUid()} ({$message->getPreviewText()}) is " . ($prediction ? 'important' : 'not important'));
if ($prediction) {
$message->setFlagImportant(true);
- $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), Tag::LABEL_IMPORTANT, true);
- $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $importantTag, true);
+ $this->mailManager->flagMessages($account, $mailbox, Tag::LABEL_IMPORTANT, true, $message);
+ $this->mailManager->tagMessages($account, $mailbox, $importantTag, true, $message);
}
}
} catch (ServiceException $e) {
diff --git a/lib/Service/DkimService.php b/lib/Service/DkimService.php
index a498004774..a15ed8fd81 100644
--- a/lib/Service/DkimService.php
+++ b/lib/Service/DkimService.php
@@ -13,9 +13,9 @@
use OCA\Mail\Contracts\IDkimService;
use OCA\Mail\Contracts\IDkimValidator;
use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\Message;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -27,8 +27,7 @@ class DkimService implements IDkimService {
private $cache;
public function __construct(
- private IMAPClientFactory $clientFactory,
- private MessageMapper $messageMapper,
+ private ProtocolFactory $protocolFactory,
ICacheFactory $cacheFactory,
private IDkimValidator $dkimValidator,
) {
@@ -36,32 +35,23 @@ public function __construct(
}
#[\Override]
- public function validate(Account $account, Mailbox $mailbox, int $id): bool {
- $cached = $this->getCached($account, $mailbox, $id);
+ public function validate(Account $account, Mailbox $mailbox, Message $message): bool {
+ $cached = $this->getCached($account, $mailbox, $message->getId());
if (is_bool($cached)) {
return $cached;
}
- $client = $this->clientFactory->getClient($account);
- try {
- $fullText = $this->messageMapper->getFullText(
- $client,
- $mailbox->getName(),
- $id,
- $account->getUserId(),
- false,
- );
+ $fullText = $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchMessageRaw($account, $mailbox, $message);
- if ($fullText === null) {
- throw new ServiceException("Could not fetch message source for uid $id");
- }
- } finally {
- $client->logout();
+ if ($fullText === null) {
+ throw new ServiceException('Could not fetch message source for uid ' . $message->getUid());
}
$result = $this->dkimValidator->validate($fullText);
- $cache_key = $this->buildCacheKey($account, $mailbox, $id);
+ $cache_key = $this->buildCacheKey($account, $mailbox, $message->getId());
$this->cache->set($cache_key, $result, self::CACHE_TTL);
return $result;
diff --git a/lib/Service/DraftsService.php b/lib/Service/DraftsService.php
index 1224cb8499..de8053d7c1 100644
--- a/lib/Service/DraftsService.php
+++ b/lib/Service/DraftsService.php
@@ -10,7 +10,6 @@
namespace OCA\Mail\Service;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
@@ -18,7 +17,7 @@
use OCA\Mail\Events\DraftMessageCreatedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -35,8 +34,8 @@ public function __construct(
private LocalMessageMapper $mapper,
private AttachmentService $attachmentService,
IEventDispatcher $eventDispatcher,
- private IMAPClientFactory $clientFactory,
- private IMailManager $mailManager,
+ private ProtocolFactory $protocolFactory,
+ private MailManager $mailManager,
private LoggerInterface $logger,
private AccountService $accountService,
ITimeFactory $time,
@@ -101,7 +100,7 @@ public function saveMessage(Account $account, LocalMessage $message, array $to,
return $message;
}
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
@@ -133,7 +132,7 @@ public function updateMessage(Account $account, LocalMessage $message, array $to
return $message;
}
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
diff --git a/lib/Service/IMipService.php b/lib/Service/IMipService.php
index 146f2c9446..739e62cd0b 100644
--- a/lib/Service/IMipService.php
+++ b/lib/Service/IMipService.php
@@ -100,7 +100,7 @@ public function process(): void {
}
try {
- $imapMessages = $this->mailManager->getImapMessagesForScheduleProcessing($account, $mailbox, array_map(static fn ($message) => $message->getUid(), $filteredMessages));
+ $imapMessages = $this->mailManager->getImapMessages($account, $mailbox, true, ...$filteredMessages);
} catch (ServiceException $e) {
$this->logger->error('Could not get IMAP messages form IMAP server', ['exception' => $e]);
continue;
diff --git a/lib/Service/ItineraryService.php b/lib/Service/ItineraryService.php
index ef285b7f71..1a656fbe73 100644
--- a/lib/Service/ItineraryService.php
+++ b/lib/Service/ItineraryService.php
@@ -11,9 +11,9 @@
use Nextcloud\KItinerary\Itinerary;
use OCA\Mail\Account;
+use OCA\Mail\Attachment;
use OCA\Mail\Db\Mailbox;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Db\Message;
use OCA\Mail\Integration\KItinerary\ItineraryExtractor;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -30,8 +30,7 @@ class ItineraryService {
private $cache;
public function __construct(
- private IMAPClientFactory $clientFactory,
- private MessageMapper $messageMapper,
+ private readonly MailManager $mailManager,
private ItineraryExtractor $extractor,
ICacheFactory $cacheFactory,
private LoggerInterface $logger,
@@ -51,30 +50,28 @@ public function getCached(Account $account, Mailbox $mailbox, int $id): ?Itinera
return null;
}
- public function extract(Account $account, Mailbox $mailbox, int $id): Itinerary {
- if ($cached = ($this->getCached($account, $mailbox, $id))) {
+ public function extract(Account $account, Mailbox $mailbox, Message $message): Itinerary {
+ if ($cached = ($this->getCached($account, $mailbox, $message->getId()))) {
return $cached;
}
- $client = $this->clientFactory->getClient($account);
- try {
- $itinerary = new Itinerary();
- $htmlBody = $this->messageMapper->getHtmlBody($client, $mailbox->getName(), $id, $account->getUserId());
- if ($htmlBody !== null) {
- $itinerary = $itinerary->merge(
- $this->extractor->extract($htmlBody)
- );
- $nItinerary = count($itinerary);
- $this->logger->debug("Extracted $nItinerary itinerary entries from the message HTML body");
- } else {
- $this->logger->debug('Message does not have an HTML body, can\'t extract itinerary info');
- }
- $attachments = $this->messageMapper->getRawAttachments($client, $mailbox->getName(), $id, $account->getUserId());
- } finally {
- $client->logout();
+ $imapMessage = $this->mailManager->getImapMessage($account, $mailbox, $message, true);
+
+ $itinerary = new Itinerary();
+ $htmlBody = $imapMessage->htmlMessage;
+ if ($htmlBody !== '') {
+ $itinerary = $itinerary->merge(
+ $this->extractor->extract($htmlBody)
+ );
+ $nItinerary = count($itinerary);
+ $this->logger->debug("Extracted $nItinerary itinerary entries from the message HTML body");
+ } else {
+ $this->logger->debug('Message does not have an HTML body, can\'t extract itinerary info');
}
- $itinerary = array_reduce($attachments, function (Itinerary $combined, string $attachment) {
- $extracted = $this->extractor->extract($attachment);
+
+ $attachments = $this->mailManager->getMailAttachments($account, $mailbox, $message);
+ $itinerary = array_reduce($attachments, function (Itinerary $combined, Attachment $attachment) {
+ $extracted = $this->extractor->extract($attachment->getContent());
$nExtracted = count($extracted);
$this->logger->debug("Extracted $nExtracted itinerary entries from an attachment");
return $combined->merge($extracted);
@@ -87,7 +84,7 @@ public function extract(Account $account, Mailbox $mailbox, int $id): Itinerary
$nFinal = count($final);
$this->logger->debug("Reduced $nItinerary itinerary entries to $nFinal entries");
- $cache_key = $this->buildCacheKey($account, $mailbox, $id);
+ $cache_key = $this->buildCacheKey($account, $mailbox, $message->getId());
$this->cache->set($cache_key, json_encode($final), self::CACHE_TTL);
return $final;
diff --git a/lib/Service/JMAP/JmapOperationsService.php b/lib/Service/JMAP/JmapOperationsService.php
new file mode 100644
index 0000000000..913923e83d
--- /dev/null
+++ b/lib/Service/JMAP/JmapOperationsService.php
@@ -0,0 +1,1038 @@
+dataStore = $this->protocolFactory->jmapClient($account);
+ try {
+ // evaluate if client was already connected
+ if (!$this->dataStore->sessionStatus()) {
+ $this->dataStore->connect();
+ }
+ } catch (Throwable $e) {
+ throw new ServiceException('Could not connect to JMAP server: ' . $e->getMessage(), 0, $e);
+ }
+ if ($this->sessionAccountId !== $account->getId()) {
+ $sessionAccount = $this->dataStore->sessionAccountDefault('mail');
+ if ($sessionAccount === null) {
+ throw new ServiceException('JMAP session does not provide a default mail account', 1);
+ }
+ $this->sessionAccountId = $account->getId();
+ $this->dataAccount = $sessionAccount->id();
+ $this->supportsBlob = $this->dataStore->sessionCapable('blob');
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the connected data account identifier or fails if not connected.
+ */
+ private function account(): string {
+ if ($this->dataAccount === null) {
+ throw new ServiceException('JMAP data store is not connected', 1);
+ }
+ return $this->dataAccount;
+ }
+
+ /**
+ * Transmits one or more JMAP commands and returns the response bundle.
+ *
+ * @throws ServiceException
+ */
+ private function transceive(array $commands): ResponseBundle {
+ try {
+ return $this->dataStore->perform($commands);
+ } catch (Throwable $e) {
+ throw new ServiceException('JMAP request failed: ' . $e->getMessage(), 0, $e);
+ }
+ }
+
+ /**
+ * Downloads a blob from remote storage into $data.
+ *
+ * @throws ServiceException
+ */
+ private function download(string $account, string $identifier, &$data, string $type = 'application/octet-stream', string $name = 'file.bin'): void {
+ try {
+ $this->dataStore->download($account, $identifier, $data, $type, $name);
+ } catch (Throwable $e) {
+ throw new ServiceException('JMAP download failed: ' . $e->getMessage(), 0, $e);
+ }
+ }
+
+ /**
+ * List of collections in remote storage
+ *
+ * @param string|null $location optional location constraint (e.g. parent collection id)
+ * @param list $filter optional filter conditions
+ * @param list $sort optional sort conditions
+ *
+ * @return Mailbox[]
+ */
+ public function collectionList(?string $location = null, array $filter = [], array $sort = []): array {
+ // construct request
+ $r0 = new MailboxQuery($this->account());
+ // define location
+ if (!empty($location)) {
+ $r0->filter()->in($location);
+ }
+ // define filter
+ foreach ($filter as $condition) {
+ $value = $condition['value'];
+ match($condition['attribute']) {
+ 'in' => $r0->filter()->in($value),
+ 'name' => $r0->filter()->name($value),
+ 'role' => $r0->filter()->role($value),
+ 'hasRoles' => $r0->filter()->hasRoles($value),
+ 'subscribed' => $r0->filter()->isSubscribed($value),
+ default => null
+ };
+ }
+ // define order
+ foreach ($sort as $condition) {
+ $ascending = $condition['direction'];
+ match($condition['attribute']) {
+ 'name' => $r0->sort()->name($ascending),
+ 'order' => $r0->sort()->order($ascending),
+ default => null
+ };
+ }
+ // construct request
+ $r1 = new MailboxGet($this->account());
+ $r1->targetFromRequest($r0, '/ids');
+ // transceive
+ $bundle = $this->transceive([$r0, $r1]);
+ // extract response
+ $response = $bundle->response(1);
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // convert collection objects
+ $list = [];
+ foreach ($response->objects() as $so) {
+ if (!$so instanceof MailboxParametersResponse) {
+ continue;
+ }
+ $list[] = $this->jmapMailboxAdapter->convertToMailbox($so);
+ }
+ // return collection of collections
+ return $list;
+ }
+
+ /**
+ * Check existence of collections in remote storage
+ *
+ * @param string ...$identifiers remote identifiers
+ *
+ * @return array map of remote identifiers to existence status
+ */
+ public function collectionExtant(string ...$identifiers): array {
+ $extant = [];
+ // construct request
+ $r0 = new MailboxGet($this->account());
+ $r0->target(...$identifiers);
+ $r0->property('id');
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // construct map of extant collection identifiers
+ foreach ($response->objects() as $so) {
+ if (!$so instanceof MailboxParametersResponse) {
+ continue;
+ }
+ $id = $so->id();
+ if ($id === null) {
+ continue;
+ }
+ $extant[$id] = true;
+ }
+ return $extant;
+ }
+
+ /**
+ * Retrieve details for a specific collection in remote storage
+ *
+ * @param string $identifier remote identifier
+ *
+ * @return Mailbox|null collection object if retrieval was successful, null otherwise
+ */
+ public function collectionFetch(string $identifier): ?Mailbox {
+ // construct request
+ $r0 = new MailboxGet($this->account());
+ $r0->target($identifier);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // convert collection objects
+ $collection = $response->object(0);
+ if ($collection instanceof MailboxParametersResponse) {
+ return $this->jmapMailboxAdapter->convertToMailbox($collection);
+ }
+ return null;
+ }
+
+ /**
+ * Create collection in remote storage
+ *
+ * @param Mailbox|null $location optional parent collection
+ * @param Mailbox $mailbox collection to create
+ *
+ * @return Mailbox|null created collection or null if creation failed
+ */
+ public function collectionCreate(?Mailbox $location, Mailbox $mailbox): ?Mailbox {
+ // convert entity
+ $to = $this->jmapMailboxAdapter->convertFromMailbox($mailbox);
+ // define location
+ if (!empty($location)) {
+ $to->in($location->getRemoteId());
+ }
+ $id = uniqid();
+ // construct request
+ $r0 = new MailboxSet($this->account());
+ $r0->create($id, $to);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // check for success
+ $result = $response->createSuccess($id);
+ if ($result !== null) {
+ $mailbox->setRemoteId($result['id']);
+ $mailbox->setNameHash(md5($result['id']));
+ return $mailbox;
+ }
+ // check for failure
+ $result = $response->createFailure($id);
+ if ($result !== null) {
+ $type = $result['type'] ?? 'unknownError';
+ $description = $result['description'] ?? 'An unknown error occurred during collection creation.';
+ throw new ServiceException("$type: $description", 1);
+ }
+ // return null if creation failed without failure reason
+ return null;
+ }
+
+ /**
+ * Modify collection in remote storage
+ *
+ * @param string $identifier remote identifier
+ * @param Mailbox $mailbox collection with modifications to apply
+ *
+ * @return Mailbox|null modified collection or null if modification failed
+ */
+ public function collectionModify(string $identifier, Mailbox $mailbox, array $patch = []): ?Mailbox {
+ // convert entity, optionally limited to the patched properties
+ $to = $this->jmapMailboxAdapter->convertFromMailbox($mailbox, $patch);
+ // construct request
+ $r0 = new MailboxSet($this->account());
+ $r0->update($identifier, $to);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // check for success
+ $result = $response->updateSuccess($identifier);
+ if ($result !== null) {
+ return $mailbox;
+ }
+ // check for failure
+ $result = $response->updateFailure($identifier);
+ if ($result !== null) {
+ $type = $result['type'] ?? 'unknownError';
+ $description = $result['description'] ?? 'An unknown error occurred during collection modification.';
+ throw new ServiceException("$type: $description", 1);
+ }
+ // return null if modification failed without failure reason
+ return null;
+ }
+
+ /**
+ * Delete collection in remote storage
+ *
+ * @param string $identifier remote identifier
+ * @param bool $force whether to force deletion even if collection is not empty
+ *
+ * @return string|null deleted collection identifier or null if deletion failed
+ */
+ public function collectionDestroy(string $identifier, bool $force = false): ?string {
+ // construct request
+ $r0 = new MailboxSet($this->account());
+ $r0->delete($identifier);
+ if ($force) {
+ $r0->destroyContents(true);
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // check for success
+ $result = $response->deleteSuccess($identifier);
+ if ($result !== null) {
+ return (string)$result['id'];
+ }
+ // check for failure
+ $result = $response->deleteFailure($identifier);
+ if ($result !== null) {
+ $type = $result['type'] ?? 'unknownError';
+ $description = $result['description'] ?? 'An unknown error occurred during collection deletion.';
+ throw new ServiceException("$type: $description", 1);
+ }
+ // return null if deletion failed without failure reason
+ return null;
+ }
+
+ /**
+ * Retrieve entities from remote storage
+ *
+ * @param string|null $location optional location constraint
+ * @param list $filter optional filter conditions
+ * @param list $sort optional sort conditions
+ * @param JmapEntityListRange|null $range optional range conditions
+ * @param JmapEntityListGranularity|null $granularity optional granularity level; 'basic' returns entityPropertiesBasic, 'default' returns entityPropertiesDefault with full body
+ *
+ * @return array{state:string, list:array}
+ */
+ public function entityList(?string $location = null, array $filter = [], array $sort = [], ?array $range = null, ?string $granularity = null): array {
+ // construct first request
+ $r0 = new MailQuery($this->account());
+ // define location
+ if (!empty($location)) {
+ $r0->filter()->in($location);
+ }
+ // define filter
+ foreach ($filter as $condition) {
+ $value = $condition['value'];
+ match($condition['attribute']) {
+ '*' => $r0->filter()->text($value),
+ 'in' => $r0->filter()->in($value),
+ 'inOmit' => $r0->filter()->inOmit($value),
+ 'from' => $r0->filter()->from($value),
+ 'to' => $r0->filter()->to($value),
+ 'cc' => $r0->filter()->cc($value),
+ 'bcc' => $r0->filter()->bcc($value),
+ 'subject' => $r0->filter()->subject($value),
+ 'body' => $r0->filter()->body($value),
+ 'attachmentPresent' => $r0->filter()->hasAttachment($value),
+ 'tagPresent' => $r0->filter()->keywordPresent($value),
+ 'tagAbsent' => $r0->filter()->keywordAbsent($value),
+ 'before' => $r0->filter()->receivedBefore($value),
+ 'after' => $r0->filter()->receivedAfter($value),
+ 'min' => $r0->filter()->sizeMin((int)$value),
+ 'max' => $r0->filter()->sizeMax((int)$value),
+ default => null
+ };
+ }
+ // define order
+ foreach ($sort as $condition) {
+ $direction = $condition['direction'];
+ match($condition['attribute']) {
+ 'from' => $r0->sort()->from($direction),
+ 'to' => $r0->sort()->to($direction),
+ 'subject' => $r0->sort()->subject($direction),
+ 'received' => $r0->sort()->received($direction),
+ 'sent' => $r0->sort()->sent($direction),
+ 'size' => $r0->sort()->size($direction),
+ 'tag' => $r0->sort()->keyword($direction),
+ default => null
+ };
+ }
+ // define range
+ if ($range !== null) {
+ $anchor = $range['anchor'] ?? null;
+ $position = $range['position'] ?? null;
+ $tally = $range['tally'] ?? null;
+ if ($anchor === 'absolute' && $position !== null && $tally !== null) {
+ $r0->limitAbsolute((int)$position, (int)$tally);
+ }
+ if ($anchor === 'relative' && $position !== null && $tally !== null) {
+ $r0->limitRelative((int)$position, (int)$tally);
+ }
+ }
+ // construct second request
+ $r1 = new MailGet($this->account());
+ $r1->targetFromRequest($r0, '/ids');
+ // select properties to return
+ if ($granularity === 'basic') {
+ $r1->property(...$this->entityPropertiesBasic);
+ } else {
+ $r1->property(...$this->entityPropertiesDefault);
+ $r1->bodyAll(true);
+ }
+ // transceive
+ $bundle = $this->transceive([$r0, $r1]);
+ // extract response
+ $response = $bundle->response(1);
+ // convert json objects to message objects
+ $state = $response->state();
+ $list = $response->objects();
+ foreach ($list as $id => $entry) {
+ if (!$entry instanceof MailParametersResponse) {
+ continue;
+ }
+ $list[$id] = $this->jmapMessageAdapter->convertToDatabaseMessage($entry);
+ }
+ // return message collection
+ return ['list' => $list, 'state' => $state];
+ }
+
+ /**
+ * Check existence of entities in remote storage
+ *
+ * @param string ...$identifiers remote identifiers
+ *
+ * @return array array of remote identifiers and their existence status
+ */
+ public function entityExtant(string ...$identifiers): array {
+ $extant = [];
+ // construct request
+ $r0 = new MailGet($this->account());
+ $r0->target(...$identifiers);
+ $r0->property('id');
+ // transmit request and receive response
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // construct map of extant collection identifiers
+ foreach ($response->objects() as $so) {
+ if (!$so instanceof MailParametersResponse) {
+ continue;
+ }
+ $id = $so->id();
+ if ($id === null) {
+ continue;
+ }
+ $extant[$id] = true;
+ }
+ return $extant;
+ }
+
+ /**
+ * Delta for entities in remote storage
+ *
+ * @param string|null $location optional remote location constraint (e.g. remote collection identifier)
+ * @param string $state state identifier to compare against
+ *
+ * @return array{state:string, additions:array, modifications:array, deletions:array}
+ */
+ public function entityDelta(?string $location, string $state): array {
+ // if no state is given, return all entities as additions
+ if (empty($state)) {
+ $results = $this->entityList($location, [], [], null, 'basic');
+ $delta = [
+ 'state' => $results['state'],
+ 'additions' => [],
+ 'modifications' => [],
+ 'deletions' => [],
+ ];
+ foreach ($results['list'] as $entry) {
+ $remoteId = $entry->getRemoteId();
+ if ($remoteId === null) {
+ continue;
+ }
+ $delta['additions'][] = $remoteId;
+ }
+ return $delta;
+ }
+ // if location is given, perform delta for specific collection, otherwise perform delta for all collections
+ if (empty($location)) {
+ return $this->entityDeltaDefault($state);
+ } else {
+ return $this->entityDeltaSpecific($location, $state);
+ }
+ }
+
+ /**
+ * Delta of changes for specific collection in remote storage
+ *
+ * @param string|null $location optional remote location constraint (e.g. remote collection identifier)
+ * @param string $state state identifier to compare against
+ *
+ * @return array{state:string, additions:array, modifications:array, deletions:array}
+ */
+ public function entityDeltaSpecific(?string $location, string $state): array {
+ // construct set request
+ $r0 = new MailQueryChanges($this->account());
+ // set location constraint
+ if (!empty($location)) {
+ $r0->filter()->in($location);
+ }
+ // set state constraint
+ if (!empty($state)) {
+ $r0->state($state);
+ } else {
+ $r0->state('0');
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ return $this->constructDeltaResult(
+ $response->stateNew(),
+ $response->added(),
+ $response->removed(),
+ );
+ }
+
+ /**
+ * Delta of changes for all collections in remote storage
+ *
+ * @param string $state state identifier to compare against
+ *
+ * @return array{state:string, additions:array, modifications:array, deletions:array}
+ */
+ public function entityDeltaDefault(string $state): array {
+ // construct set request
+ $r0 = new MailChanges($this->account());
+ if (!empty($state)) {
+ $r0->state($state);
+ } else {
+ $r0->state('');
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+
+ return $this->constructDeltaResult(
+ $response->stateNew(),
+ $response->added(),
+ $response->removed(),
+ );
+ }
+
+ /**
+ * Construct delta result from added and removed entries
+ *
+ * @param string $state state identifier to return in result
+ * @param array $added entries that were added
+ * @param array $removed entries that were removed
+ *
+ * @return array{state:string, additions:array, modifications:array, deletions:array}
+ */
+ private function constructDeltaResult(string $state, array $added, array $removed): array {
+ // extract/flatten ids from added and removed entries
+ $extractIds = static function (array $entries): array {
+ $ids = [];
+ foreach ($entries as $entry) {
+ if (is_string($entry) && $entry !== '') {
+ $ids[] = $entry;
+ continue;
+ }
+
+ $id = is_array($entry) ? ($entry['id'] ?? null) : null;
+ if (is_string($id) && $id !== '') {
+ $ids[] = $id;
+ }
+ }
+
+ return array_values(array_unique($ids));
+ };
+ $addedIds = $extractIds($added);
+ $removedIds = $extractIds($removed);
+ // entries that are both in added and removed are considered modified
+ $modifiedIds = array_values(array_intersect($addedIds, $removedIds));
+ $modifiedIdMap = array_fill_keys($modifiedIds, true);
+ // entries that are only in added are considered additions, entries that are only in removed are considered deletions
+ $additionIds = array_values(array_filter(
+ $addedIds,
+ static fn (string $id): bool => !isset($modifiedIdMap[$id]),
+ ));
+ $deletionIds = array_values(array_filter(
+ $removedIds,
+ static fn (string $id): bool => !isset($modifiedIdMap[$id]),
+ ));
+
+ return [
+ 'state' => $state,
+ 'additions' => $additionIds,
+ 'modifications' => $modifiedIds,
+ 'deletions' => $deletionIds,
+ ];
+ }
+
+ /**
+ * Retrieve entities from remote storage
+ *
+ * @param string ...$identifiers remote identifiers
+ *
+ * @return Message[]
+ */
+ public function entityFetchMessage(string ...$identifiers): array {
+ $responses = $this->entityFetchNative(...$identifiers);
+ $list = [];
+ foreach ($responses as $id => $entry) {
+ $list[$id] = $this->jmapMessageAdapter->convertToDatabaseMessage($entry);
+ }
+ return $list;
+ }
+
+ /**
+ * Retrieve entities from remote storage
+ *
+ * @param string ...$identifiers remote identifiers
+ *
+ * @return MailParametersResponse[]
+ */
+ public function entityFetchNative(string ...$identifiers): array {
+ // construct request
+ $r0 = new MailGet($this->account());
+ $r0->target(...$identifiers);
+ // select properties to return
+ $r0->property(...$this->entityPropertiesDefault);
+ $r0->bodyAll(true);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // convert json objects to message objects
+ $list = [];
+ foreach ($response->objects() as $so) {
+ if (!$so instanceof MailParametersResponse) {
+ continue;
+ }
+ $id = $so->id();
+ if ($id === null) {
+ continue;
+ }
+ $list[$id] = $so;
+ }
+ // return message collection
+ return $list;
+ }
+
+ /**
+ * Retrieve raw message source from remote storage
+ *
+ * @param string $identifier remote identifier
+ *
+ * @return string|null raw message source if retrieval was successful, null otherwise
+ */
+ public function entityFetchRaw(string $identifier): ?string {
+ $entities = $this->entityFetchNative($identifier);
+ $entity = $entities[$identifier] ?? null;
+ if (!$entity instanceof MailParametersResponse) {
+ return null;
+ }
+
+ $blobId = $entity->blob();
+ if ($blobId === null || $blobId === '') {
+ return null;
+ }
+
+ $rawMessage = null;
+ $this->download($this->account(), $blobId, $rawMessage, 'message/rfc822', 'message.eml');
+
+ return is_string($rawMessage) ? $rawMessage : null;
+ }
+
+ /**
+ * Create entity in remote storage
+ */
+ public function entityCreate(string $location, array $so): ?array {
+ // convert entity
+ $to = new MailParametersRequest();
+ $to->parametersRaw($so);
+ $to->in($location);
+ $id = uniqid();
+ // construct request
+ $r0 = new MailSet($this->account());
+ $r0->create($id, $to);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ // check for success
+ $result = $response->createSuccess($id);
+ if ($result !== null) {
+ return array_merge($so, $result);
+ }
+ // check for failure
+ $result = $response->createFailure($id);
+ if ($result !== null) {
+ $type = $result['type'] ?? 'unknownError';
+ $description = $result['description'] ?? 'An unknown error occurred during collection creation.';
+ throw new ServiceException("$type: $description", 1);
+ }
+ // return null if creation failed without failure reason
+ return null;
+ }
+
+ /**
+ * Update entity in remote storage
+ */
+ public function entityModify(array $so): ?array {
+ // extract entity id
+ $id = $so['id'];
+ // convert entity
+ $to = new MailParametersRequest();
+ $to->parametersRaw($so);
+ // construct request
+ $r0 = new MailSet($this->account());
+ $r0->update($id, $to);
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ $results = [];
+ // check for success
+ foreach ($response->updateSuccesses() as $id) {
+ $results[$id] = true;
+ }
+ // check for failure
+ foreach ($response->updateFailures() as $id => $data) {
+ $results[$id] = $data['type'] ?? 'unknownError';
+ }
+
+ return $results;
+ }
+
+ /**
+ * Partially update entity in remote storage
+ */
+ public function entityModifyPatch(MailParametersRequest $patch, string ...$identifiers): ?array {
+ // construct request
+ $r0 = new MailSet($this->account());
+ foreach ($identifiers as $id) {
+ $r0->patch($id, $patch);
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+ $results = [];
+ // check for success
+ foreach ($response->updateSuccesses() as $id => $data) {
+ $results[$id] = true;
+ }
+ // check for failure
+ foreach ($response->updateFailures() as $id => $data) {
+ $results[$id] = $data['type'] ?? 'unknownError';
+ }
+
+ return $results;
+ }
+
+ /**
+ * Modify entity flags in remote storage
+ *
+ * @param array $flags list of flags to set on entity (e.g. ['seen' => true, 'flagged' => false])
+ * @param string ...$identifiers remote identifiers to apply flag modifications to
+ *
+ * @return array map of remote identifiers to modification result (true for success, error type for failure)
+ */
+ public function entityModifyFlags(array $flags, string ...$identifiers): ?array {
+ // construct patch request with flag modifications
+ $patch = new MailParametersRequest();
+ foreach ($flags as $flag => $value) {
+ $patch->keyword($flag, $value);
+ }
+ // execute command
+ $result = $this->entityModifyPatch($patch, ...$identifiers);
+ return $result;
+ }
+
+ /**
+ * Delete entity in remote storage
+ *
+ * @param string ...$identifiers remote identifiers to delete
+ *
+ * @return array map of remote identifiers to deletion result (true for success, error type for failure)
+ */
+ public function entityDelete(string ...$identifiers): array {
+ // construct set request
+ $r0 = new MailSet($this->account());
+ foreach ($identifiers as $id) {
+ $r0->delete($id);
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+
+ $results = [];
+ // map successful and failed deletions to their identifiers
+ foreach ($response->deleteSuccesses() as $id) {
+ $results[$id] = true;
+ }
+ foreach ($response->deleteFailures() as $id => $data) {
+ $results[$id] = $data['type'] ?? 'unknownError';
+ }
+
+ return $results;
+ }
+
+ /**
+ * Move entity to another collection in remote storage
+ *
+ * @param string $target remote identifier of target collection to move entities to
+ * @param string ...$identifiers remote identifiers of entities to move
+ *
+ * @return array map of remote identifiers to move result (true for success, error type for failure)
+ */
+ public function entityMove(string $target, string ...$identifiers): array {
+ // construct set request
+ $r0 = new MailSet($this->account());
+ foreach ($identifiers as $id) {
+ $r0->update($id)->in($target);
+ }
+ // transceive
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // check for command error
+ if ($response instanceof ResponseException) {
+ if ($response->type() === 'unknownMethod') {
+ throw new JmapUnknownMethod($response->description(), 1);
+ } else {
+ throw new ServiceException($response->type() . ': ' . $response->description(), 1);
+ }
+ }
+
+ $results = [];
+ // map successful and failed moves to their identifiers
+ foreach ($response->updateSuccesses() as $id => $data) {
+ $results[$id] = true;
+ }
+ foreach ($response->updateFailures() as $id => $data) {
+ $results[$id] = $data['type'] ?? 'unknownError';
+ }
+
+ return $results;
+ }
+
+ public function attachmentFetch(string $entityId, string ...$blobId): array {
+ $entities = $this->entityFetchNative($entityId);
+ $entity = $entities[$entityId] ?? null;
+ if (!$entity instanceof MailParametersResponse) {
+ return [];
+ }
+
+ $attachments = [];
+ foreach ($entity->attachments() as $attachment) {
+ if ($blobId === [] || in_array($attachment->blob(), $blobId, true)) {
+ $content = '';
+ $this->download($this->account(), $attachment->blob(), $content);
+
+ $attachments[] = new Attachment(
+ $attachment->blob(),
+ $attachment->name() ?? 'unknown.file',
+ $attachment->type() ?? 'application/octet-stream',
+ $content,
+ $attachment->size() ?? 0,
+ $attachment->cid(),
+ $attachment->disposition(),
+ );
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * retrieve identity from remote storage
+ *
+ *
+ */
+ public function identityFetch(?string $account = null): array {
+ if ($account === null) {
+ $account = $this->account();
+ }
+ // construct set request
+ $r0 = new MailIdentityGet($this->account());
+ // transmit request and receive response
+ $bundle = $this->transceive([$r0]);
+ // extract response
+ $response = $bundle->first();
+ // convert json object to message object and return
+ return $response->objects();
+ }
+
+}
diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php
index dfcb6e9c91..fe26d948f2 100644
--- a/lib/Service/MailManager.php
+++ b/lib/Service/MailManager.php
@@ -9,14 +9,8 @@
namespace OCA\Mail\Service;
-use Horde_Imap_Client;
-use Horde_Imap_Client_Exception;
-use Horde_Imap_Client_Exception_NoSupportExtension;
-use Horde_Imap_Client_Socket;
-use Horde_Mime_Exception;
use OCA\Mail\Account;
use OCA\Mail\Attachment;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
@@ -31,54 +25,38 @@
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ImapFlagEncodingException;
use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Exception\SmimeDecryptException;
use OCA\Mail\Exception\TrashMailboxNotSetException;
-use OCA\Mail\Folder;
-use OCA\Mail\IMAP\FolderMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\ImapFlag;
-use OCA\Mail\IMAP\MailboxSync;
-use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Model\IMAPMessage;
+use OCA\Mail\Protocol\ProtocolFactory;
+use OCA\Mail\Service\Search\SearchQuery;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
use Psr\Log\LoggerInterface;
use function array_map;
use function array_values;
-class MailManager implements IMailManager {
- /**
- * https://datatracker.ietf.org/doc/html/rfc9051#name-flags-message-attribute
- */
- private const SYSTEM_FLAGS = [
- 'seen' => [Horde_Imap_Client::FLAG_SEEN],
- 'answered' => [Horde_Imap_Client::FLAG_ANSWERED],
- 'flagged' => [Horde_Imap_Client::FLAG_FLAGGED],
- 'deleted' => [Horde_Imap_Client::FLAG_DELETED],
- 'draft' => [Horde_Imap_Client::FLAG_DRAFT],
- 'recent' => [Horde_Imap_Client::FLAG_RECENT],
- ];
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
+class MailManager {
public function __construct(
- private IMAPClientFactory $imapClientFactory,
- private MailboxMapper $mailboxMapper,
- private MailboxSync $mailboxSync,
- private FolderMapper $folderMapper,
- private ImapMessageMapper $imapMessageMapper,
- private DbMessageMapper $dbMessageMapper,
- IEventDispatcher $eventDispatcher,
- private LoggerInterface $logger,
- private TagMapper $tagMapper,
- private MessageTagsMapper $messageTagsMapper,
- private ThreadMapper $threadMapper,
- private ImapFlag $imapFlag,
+ private readonly MailboxMapper $mailboxMapper,
+ private readonly DbMessageMapper $dbMessageMapper,
+ private readonly IEventDispatcher $eventDispatcher,
+ private readonly LoggerInterface $logger,
+ private readonly TagMapper $tagMapper,
+ private readonly MessageTagsMapper $messageTagsMapper,
+ private readonly ProtocolFactory $protocolFactory,
+ private readonly ImapFlag $imapFlag,
+ private readonly ThreadMapper $threadMapper,
) {
- $this->eventDispatcher = $eventDispatcher;
}
- #[\Override]
+ //** ============================ Mailbox Operations ============================ */
+
+ /**
+ * @throws ClientException
+ */
public function getMailbox(string $uid, int $id): Mailbox {
try {
return $this->mailboxMapper->findByUid($id, $uid);
@@ -94,213 +72,217 @@ public function getMailbox(string $uid, int $id): Mailbox {
* @return Mailbox[]
* @throws ServiceException
*/
- #[\Override]
public function getMailboxes(Account $account, bool $forceSync = false): array {
- $this->mailboxSync->sync($account, $this->logger, $forceSync);
+ $this->protocolFactory
+ ->mailboxConnector($account)
+ ->syncAll($account, $forceSync);
return $this->mailboxMapper->findAll($account);
}
- #[\Override]
+ /**
+ * @throws ServiceException
+ * @throws DoesNotExistException the mailbox could not be re-read after being created on the remote (IMAP-backed accounts only)
+ */
public function createMailbox(Account $account, string $name, array $specialUse = []): Mailbox {
- $client = $this->imapClientFactory->getClient($account);
- try {
- $folder = $this->folderMapper->createFolder($client, $name, $specialUse);
- $this->folderMapper->fetchFolderAcls([$folder], $client);
- $this->folderMapper->detectFolderSpecialUse([$folder]);
- $this->mailboxSync->sync($account, $this->logger, true, $client);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not get mailbox status: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- } finally {
- $client->logout();
+ return $this->protocolFactory
+ ->mailboxConnector($account)
+ ->create($account, $name, $specialUse);
+ }
+
+ /**
+ * @throws ServiceException
+ */
+ public function renameMailbox(Account $account, Mailbox $mailbox, string $name): Mailbox {
+ return $this->protocolFactory
+ ->mailboxConnector($account)
+ ->rename($account, $mailbox, $name);
+ }
+
+ /**
+ * @throws ServiceException
+ */
+ public function deleteMailbox(Account $account, Mailbox $mailbox): void {
+ $this->protocolFactory
+ ->mailboxConnector($account)
+ ->delete($account, $mailbox);
+ }
+
+ /**
+ * @throws ServiceException
+ * @throws DoesNotExistException the mailbox could not be re-read after being updated on the remote (IMAP-backed accounts only)
+ */
+ public function updateSubscription(Account $account, Mailbox $mailbox, bool $subscribed): Mailbox {
+ return $this->protocolFactory
+ ->mailboxConnector($account)
+ ->subscribe($account, $mailbox, $subscribed);
+ }
+
+ /**
+ * @throws ClientException
+ * @throws ServiceException
+ */
+ public function clearMailbox(Account $account, Mailbox $mailbox): void {
+ $uids = $this->dbMessageMapper->findAllUids($mailbox);
+ if ($uids === []) {
+ return;
}
+ // deleteMessages already moves to trash, or expunges if the mailbox is the trash
+ $messages = $this->dbMessageMapper->findByUids($mailbox, $uids);
+ $this->deleteMessages($account, $mailbox, ...$messages);
+ }
+
+ //** ============================ Message Operations ============================ */
- return $this->mailboxMapper->find($account, $name);
+ /**
+ * @throws DoesNotExistException
+ */
+ public function getMessage(string $uid, int $id): Message {
+ return $this->dbMessageMapper->findByUserId($uid, $id);
}
- #[\Override]
- public function getImapMessage(Horde_Imap_Client_Socket $client,
- Account $account,
- Mailbox $mailbox,
- int $uid,
- bool $loadBody = false): IMAPMessage {
- try {
- return $this->imapMessageMapper->find(
- $client,
- $mailbox->getName(),
- $uid,
- $account->getUserId(),
- $loadBody
- );
- } catch (DoesNotExistException|Horde_Mime_Exception|Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not load message',
- $e->getCode(),
- $e
- );
+ /**
+ * @throws ClientException
+ * @throws ServiceException
+ * @throws SmimeDecryptException
+ */
+ public function getImapMessage(Account $account, Mailbox $mailbox, Message $message, bool $loadBody = false): IMAPMessage {
+ $messages = $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchMessages($account, $mailbox, $loadBody, $message);
+
+ if ($messages === []) {
+ throw new ClientException('Message not found on remote server');
}
+ return reset($messages);
}
/**
* @param Account $account
* @param Mailbox $mailbox
- * @param int[] $uids
+ * @param bool $loadBody
+ * @param Message ...$messages
* @return IMAPMessage[]
* @throws ServiceException
+ * @throws SmimeDecryptException
*/
- public function getImapMessagesForScheduleProcessing(Account $account,
- Mailbox $mailbox,
- array $uids): array {
- $client = $this->imapClientFactory->getClient($account);
- try {
- return $this->imapMessageMapper->findByIds(
- $client,
- $mailbox->getName(),
- $uids,
- $account->getUserId(),
- true
- );
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not load messages: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- } finally {
- $client->logout();
- }
+ public function getImapMessages(Account $account, Mailbox $mailbox, bool $loadBody = false, Message ...$messages): array {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchMessages($account, $mailbox, $loadBody, ...$messages);
}
- #[\Override]
- public function getThread(Account $account, string $threadRootId): array {
- return $this->dbMessageMapper->findThread($account, $threadRootId);
+ /**
+ * @param Account $account
+ * @param Mailbox $mailbox
+ * @param Message $message
+ *
+ * @return string|null
+ *
+ * @throws ClientException
+ * @throws ServiceException
+ */
+ public function getSource(Account $account, Mailbox $mailbox, Message $message): ?string {
+ $raw = $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchMessageRaw($account, $mailbox, $message);
+ if ($raw === null) {
+ throw new ClientException('Message not found on remote server');
+ }
+ return $raw;
}
- #[\Override]
public function getMessageIdForUid(Mailbox $mailbox, $uid): ?int {
return $this->dbMessageMapper->getIdForUid($mailbox, $uid);
}
- #[\Override]
- public function getMessage(string $uid, int $id): Message {
- return $this->dbMessageMapper->findByUserId($uid, $id);
+ /**
+ * @return Message[]
+ */
+ public function getByMessageId(Account $account, string $messageId): array {
+ return $this->dbMessageMapper->findByMessageId($account, $messageId);
}
/**
- * @param Horde_Imap_Client_Socket $client
- * @param Account $account
- * @param string $mailbox
- * @param int $uid
- *
- * @return string
- *
+ * @return int[]
* @throws ServiceException
*/
- #[\Override]
- public function getSource(Horde_Imap_Client_Socket $client,
- Account $account,
- string $mailbox,
- int $uid): ?string {
- try {
- return $this->imapMessageMapper->getFullText(
- $client,
- $mailbox,
- $uid,
- $account->getUserId(),
- false,
- );
- } catch (Horde_Imap_Client_Exception|DoesNotExistException $e) {
- throw new ServiceException('Could not load message', 0, $e);
- }
+ public function findMessages(Account $account, Mailbox $mailbox, SearchQuery $searchQuery): array {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->findMessages($account, $mailbox, $searchQuery);
}
/**
* @param Account $sourceAccount
- * @param string $sourceFolderId
- * @param int $uid
+ * @param Mailbox $sourceMailbox
+ * @param Message $message
* @param Account $destinationAccount
- * @param string $destFolderId
+ * @param Mailbox $destinationMailbox
*
* @return ?int the new UID (or null if couldn't be determined)
* @throws ServiceException
*/
- #[\Override]
- public function moveMessage(Account $sourceAccount,
- string $sourceFolderId,
- int $uid,
- Account $destinationAccount,
- string $destFolderId): ?int {
- if ($sourceAccount->getId() === $destinationAccount->getId()) {
- try {
- $sourceMailbox = $this->mailboxMapper->find($sourceAccount, $sourceFolderId);
- } catch (DoesNotExistException $e) {
- throw new ServiceException("Source mailbox $sourceFolderId does not exist", 0, $e);
- }
-
- $newUid = $this->moveMessageOnSameAccount(
- $sourceAccount,
- $sourceFolderId,
- $destFolderId,
- $uid
- );
-
- // Delete cached source message (the source imap message is copied and deleted)
- $this->eventDispatcher->dispatch(
- MessageDeletedEvent::class,
- new MessageDeletedEvent($sourceAccount, $sourceMailbox, $uid)
- );
-
- return $newUid;
- } else {
+ public function moveMessage(Account $sourceAccount, Mailbox $sourceMailbox, Message $message, Account $destinationAccount, Mailbox $destinationMailbox): ?int {
+ if ($sourceAccount->getId() !== $destinationAccount->getId()) {
throw new ServiceException('It is not possible to move across accounts yet');
}
+
+ $mutatedUids = $this->moveMessages($sourceAccount, $destinationMailbox, $sourceMailbox, $message);
+
+ return $mutatedUids[0] ?? null;
}
/**
- * @throws ClientException
* @throws ServiceException
- * @todo evaluate if we should sync mailboxes first
*/
- #[\Override]
- public function deleteMessage(Account $account,
- string $mailboxId,
- int $messageUid): void {
- try {
- $sourceMailbox = $this->mailboxMapper->find($account, $mailboxId);
- } catch (DoesNotExistException $e) {
- throw new ServiceException("Source mailbox $mailboxId does not exist", 0, $e);
+ public function moveMessages(Account $account, Mailbox $targetMailbox, Mailbox $sourceMailbox, Message ...$messages): array {
+ if ($messages === []) {
+ return [];
+ }
+ $sourceUids = [];
+ foreach ($messages as $message) {
+ $sourceUids[spl_object_id($message)] = $message->getUid();
}
- $client = $this->imapClientFactory->getClient($account);
- try {
- $this->deleteMessageWithClient($account, $sourceMailbox, $messageUid, $client);
- } finally {
- $client->logout();
+ // update remote store
+ $mutatedMessages = $this->protocolFactory
+ ->messageConnector($account)
+ ->moveMessages($account, $targetMailbox, $sourceMailbox, ...$messages);
+
+ // update local store
+ $mutatedUids = [];
+ foreach ($mutatedMessages as $mutatedMessage) {
+ $this->dbMessageMapper->update($mutatedMessage);
+ $mutatedUids[] = $mutatedMessage->getUid();
+
+ $this->eventDispatcher->dispatchTyped(new MessageDeletedEvent(
+ $account,
+ $sourceMailbox,
+ $sourceUids[spl_object_id($mutatedMessage)],
+ ));
}
+
+ return $mutatedUids;
}
/**
- * @throws ServiceException
* @throws ClientException
- * @throws TrashMailboxNotSetException
- *
+ * @throws ServiceException
* @todo evaluate if we should sync mailboxes first
*/
- #[\Override]
- public function deleteMessageWithClient(
- Account $account,
- Mailbox $mailbox,
- int $messageUid,
- Horde_Imap_Client_Socket $client,
- ): void {
- $this->eventDispatcher->dispatchTyped(
- new BeforeMessageDeletedEvent($account, $mailbox, $messageUid)
- );
+ public function deleteMessage(Account $account, Mailbox $mailbox, Message $message): void {
+ $this->deleteMessages($account, $mailbox, $message);
+ }
+ /**
+ * @throws ClientException
+ * @throws ServiceException
+ */
+ public function deleteMessages(Account $account, Mailbox $sourceMailbox, Message ...$messages): void {
+ if ($messages === []) {
+ return;
+ }
try {
$trashMailboxId = $account->getMailAccount()->getTrashMailboxId();
if ($trashMailboxId === null) {
@@ -310,403 +292,223 @@ public function deleteMessageWithClient(
} catch (DoesNotExistException $e) {
throw new ServiceException('No trash folder', 0, $e);
}
+ $operation = $sourceMailbox->getId() === $trashMailbox->getId() ? 'delete' : 'move';
+ $mappedUids = [];
- if ($mailbox->getName() === $trashMailbox->getName()) {
- // Delete inside trash -> expunge
- $this->imapMessageMapper->expunge(
- $client,
- $mailbox->getName(),
- $messageUid
- );
- } else {
- $this->imapMessageMapper->move(
- $client,
- $mailbox->getName(),
- $messageUid,
- $trashMailbox->getName()
- );
+ // dispatch events and map objects to their original UIDs before mutation
+ foreach ($messages as $message) {
+ $this->eventDispatcher->dispatchTyped(new BeforeMessageDeletedEvent($account, $sourceMailbox, $message->getUid()));
+ $this->logger->debug("$operation message", ['messageId' => $message->getUid(), 'mailboxId' => $message->getMailboxId()]);
+ $mappedUids[spl_object_id($message)] = $message->getUid();
+ }
+
+ // update remote store
+ $mutatedMessages = match ($operation) {
+ 'move' => $this->protocolFactory
+ ->messageConnector($account)
+ ->moveMessages($account, $trashMailbox, $sourceMailbox, ...$messages),
+ 'delete' => $this->protocolFactory
+ ->messageConnector($account)
+ ->deleteMessages($account, $sourceMailbox, ...$messages),
+ };
+
+ // update local store
+ if ($operation === 'move') {
+ foreach ($mutatedMessages as $mutatedMessage) {
+ $this->dbMessageMapper->update($mutatedMessage);
+ }
+ }
+ if ($operation === 'delete') {
+ $mutatedUids = array_map(static fn (Message $message): int => $message->getUid(), $mutatedMessages);
+ $this->dbMessageMapper->deleteByUid($sourceMailbox, ...$mutatedUids);
}
- $this->eventDispatcher->dispatchTyped(
- new MessageDeletedEvent($account, $mailbox, $messageUid)
- );
+ // dispatch events
+ foreach ($mutatedMessages as $mutatedMessage) {
+ $this->eventDispatcher->dispatchTyped(new MessageDeletedEvent($account, $sourceMailbox, $mappedUids[spl_object_id($mutatedMessage)]));
+ }
}
/**
- * @param Account $account
- * @param string $sourceFolderId
- * @param string $destFolderId
- * @param int $messageId
- *
- * @return ?int the new UID (or null if it couldn't be determined)
* @throws ServiceException
- *
*/
- private function moveMessageOnSameAccount(Account $account,
- string $sourceFolderId,
- string $destFolderId,
- int $messageId): ?int {
- $client = $this->imapClientFactory->getClient($account);
- try {
- return $this->imapMessageMapper->move($client, $sourceFolderId, $messageId, $destFolderId);
- } finally {
- $client->logout();
+ public function flagMessages(Account $account, Mailbox $mailbox, string $flag, bool $value, Message ...$messages): void {
+ if ($messages === []) {
+ return;
}
- }
+ // update remote store
+ $mutatedMessages = $this->protocolFactory
+ ->messageConnector($account)
+ ->flagMessages($account, $mailbox, $flag, $value, ...$messages);
- #[\Override]
- public function markFolderAsRead(Account $account, Mailbox $mailbox): void {
- $client = $this->imapClientFactory->getClient($account);
- try {
- $this->imapMessageMapper->markAllRead($client, $mailbox->getName());
- } finally {
- $client->logout();
- }
- }
+ // update local store
+ $this->dbMessageMapper->updateBulk($account, true, ...$mutatedMessages);
- #[\Override]
- public function updateSubscription(Account $account, Mailbox $mailbox, bool $subscribed): Mailbox {
- /**
- * 1. Change subscription on IMAP
- */
- $client = $this->imapClientFactory->getClient($account);
- try {
- $client->subscribeMailbox($mailbox->getName(), $subscribed);
-
- /**
- * 2. Pull changes into the mailbox database cache
- */
- $this->mailboxSync->sync($account, $this->logger, true, $client);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- "Could not set subscription status for mailbox {$mailbox->getId()} on IMAP: {$e->getMessage()}",
- $e->getCode(),
- $e
- );
- } finally {
- $client->logout();
+ // dispatch events
+ foreach ($mutatedMessages as $message) {
+ $this->eventDispatcher->dispatchTyped(new MessageFlaggedEvent($account, $mailbox, $message, $flag, $value));
}
-
- /**
- * 3. Return the updated object
- */
- return $this->mailboxMapper->find($account, $mailbox->getName());
}
- #[\Override]
- public function enableMailboxBackgroundSync(Mailbox $mailbox,
- bool $syncInBackground): Mailbox {
- $mailbox->setSyncInBackground($syncInBackground);
-
- return $this->mailboxMapper->update($mailbox);
- }
-
- #[\Override]
- public function flagMessage(Account $account, string $mailbox, int $uid, string $flag, bool $value): void {
- try {
- $mb = $this->mailboxMapper->find($account, $mailbox);
- } catch (DoesNotExistException $e) {
- throw new ClientException("Mailbox $mailbox does not exist", 0, $e);
- }
-
- $client = $this->imapClientFactory->getClient($account);
- try {
- // Only send system flags to the IMAP server as other flags might not be supported
- $imapFlags = $this->filterFlags($client, $account, $flag, $mailbox);
- foreach ($imapFlags as $imapFlag) {
- if (empty($imapFlag) === true) {
- continue;
- }
- if ($value) {
- $this->imapMessageMapper->addFlag($client, $mb, [$uid], $imapFlag);
- } else {
- $this->imapMessageMapper->removeFlag($client, $mb, [$uid], $imapFlag);
- }
- }
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not set message flag on IMAP: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- } finally {
- $client->logout();
- }
-
- // Looking the message up by uid is a shortcut to avoid changing this method's
- // signature, which the JMAP PR does anyway.
- $messages = $this->dbMessageMapper->findByUids($mb, [$uid]);
- if (count($messages) < 1) {
- // The message should be in the database cache, otherwise the client wouldn't
- // know about the uid. Skip the event rather than fail the whole flag operation.
+ /**
+ * @throws ServiceException
+ */
+ public function tagMessages(Account $account, Mailbox $mailbox, Tag $tag, bool $value, Message ...$messages): void {
+ if ($messages === []) {
return;
}
+ // update remote store
+ $mutatedMessages = $this->protocolFactory
+ ->messageConnector($account)
+ ->tagMessages($account, $mailbox, $tag, $value, ...$messages);
- $message = reset($messages);
-
- $this->eventDispatcher->dispatch(
- MessageFlaggedEvent::class,
- new MessageFlaggedEvent(
- $account,
- $mb,
- $message,
- $flag,
- $value
- )
- );
+ // update local store
+ $this->dbMessageMapper->updateBulk($account, true, ...$mutatedMessages);
}
/**
- * Tag (flag) multiple messages on IMAP using a given client instance
- *
- * @param Message[] $messages
- *
- * @throws ClientException
* @throws ServiceException
*/
- public function tagMessagesWithClient(Horde_Imap_Client_Socket $client, Account $account, Mailbox $mailbox, array $messages, Tag $tag, bool $value):void {
- if ($this->isPermflagsEnabled($client, $account, $mailbox->getName()) === true) {
- $messageIds = array_map(static fn (Message $message) => $message->getUid(), $messages);
- try {
- if ($value) {
- // imap keywords and flags work the same way
- $this->imapMessageMapper->addFlag($client, $mailbox, $messageIds, $tag->getImapLabel());
- } else {
- $this->imapMessageMapper->removeFlag($client, $mailbox, $messageIds, $tag->getImapLabel());
- }
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not set message keyword on IMAP: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- }
+ public function markFolderAsRead(Account $account, Mailbox $mailbox): void {
+ // find all messages in mailbox with their remote ids
+ $messages = $this->dbMessageMapper->findByUids($mailbox, $this->dbMessageMapper->findAllUids($mailbox));
+ if ($messages === []) {
+ return;
}
+ $this->flagMessages($account, $mailbox, 'seen', true, ...$messages);
+ }
- if ($value) {
- foreach ($messages as $message) {
- $this->tagMapper->tagMessage($tag, $message->getMessageId(), $account->getUserId());
- }
- } else {
- foreach ($messages as $message) {
- $this->tagMapper->untagMessage($tag, $message->getMessageId());
- }
- }
+ public function getThread(Account $account, string $threadRootId): array {
+ return $this->dbMessageMapper->findThread($account, $threadRootId);
}
/**
- * Tag (flag) a message on IMAP
+ * Finds all messages in the thread of the given thread root id
*
- * @param Account $account
- * @param string $mailbox
- * @param Message $message
- * @param Tag $tag
- * @param boolean $value
- * @return void
- *
- * @throws ClientException
- * @throws ServiceException
- * @uses
- *
- * @link https://github.com/nextcloud/mail/issues/25
+ * @return array array of messages in the thread, keyed by remote id
+ * @throws DoesNotExistException
*/
- #[\Override]
- public function tagMessage(Account $account, string $mailbox, Message $message, Tag $tag, bool $value): void {
- try {
- $mb = $this->mailboxMapper->find($account, $mailbox);
- } catch (DoesNotExistException $e) {
- throw new ClientException("Mailbox $mailbox does not exist", 0, $e);
+ public function fetchThread(Account $account, Mailbox $mailbox, string $threadRootId): array {
+ $mailAccount = $account->getMailAccount();
+ $messageInTrash = $mailbox->getId() === $mailAccount->getTrashMailboxId();
+ $threadMessages = $this->threadMapper->findMessageUidsAndMailboxNamesByAccountAndThreadRoot(
+ $mailAccount,
+ $threadRootId,
+ $messageInTrash,
+ );
+
+ // group message uids by mailbox
+ $uids = [];
+ foreach ($threadMessages as $threadMessage) {
+ $uids[$threadMessage['mailboxName']][] = $threadMessage['messageUid'];
}
- $client = $this->imapClientFactory->getClient($account);
- try {
- $this->tagMessagesWithClient($client, $account, $mb, [$message], $tag, $value);
- } finally {
- $client->logout();
+ unset($threadMessages);
+
+ // retrieve messages from local store
+ $messages = [];
+ $mailboxes = [];
+ foreach ($uids as $mailboxName => $messageUids) {
+ $sourceMailbox = $mailboxes[$mailboxName] ??= $this->mailboxMapper->find($account, $mailboxName);
+ $sourceMessages = $this->dbMessageMapper->findByUids($sourceMailbox, $messageUids);
+ $messages = array_merge($messages, $sourceMessages);
}
+
+ return $messages;
}
/**
- * @param Account $account
- *
- * @return Quota|null
- * @see https://tools.ietf.org/html/rfc2087
+ * @throws DoesNotExistException
+ * @throws ServiceException
*/
- #[\Override]
- public function getQuota(Account $account): ?Quota {
- /**
- * Get all the quotas roots of the user's mailboxes
- */
- $client = $this->imapClientFactory->getClient($account);
- try {
- $quotas = array_map(static fn (Folder $mb) => $client->getQuotaRoot($mb->getMailbox()), $this->folderMapper->getFolders($account, $client));
- } catch (Horde_Imap_Client_Exception_NoSupportExtension $ex) {
- return null;
- } finally {
- $client->logout();
+ public function moveThread(Account $srcAccount, Mailbox $srcMailbox, Account $dstAccount, Mailbox $dstMailbox, string $threadRootId): array {
+ if ($srcAccount->getId() !== $dstAccount->getId()) {
+ throw new ServiceException('It is not possible to move across accounts yet');
}
- /**
- * Extract the 'storage' quota
- *
- * Falls back to 0/0 if this quota has no storage information
- *
- * @see https://tools.ietf.org/html/rfc2087#section-3
- */
- $storageQuotas = array_map(static fn (array $root) => $root['storage'] ?? [
- 'usage' => 0,
- 'limit' => 0,
- ], array_merge(...array_values($quotas)));
-
- if ($storageQuotas === []) {
- // Nothing left to do, and array_merge doesn't like to be called with zero arguments.
- return null;
+ $messages = $this->fetchThread($srcAccount, $srcMailbox, $threadRootId);
+ if ($messages === []) {
+ return [];
}
- /**
- * Deduplicate identical quota roots
- */
- $storage = array_merge(...array_values($storageQuotas));
-
- return new Quota(
- 1024 * (int)($storage['usage'] ?? 0),
- 1024 * (int)($storage['limit'] ?? 0)
- );
+ return $this->moveMessages($srcAccount, $dstMailbox, $srcMailbox, ...$messages);
}
- #[\Override]
- public function renameMailbox(Account $account, Mailbox $mailbox, string $name): Mailbox {
- /*
- * 1. Rename on IMAP
- */
- $client = $this->imapClientFactory->getClient($account);
- try {
- $this->folderMapper->renameFolder(
- $client,
- $mailbox->getName(),
- $name
- );
-
- /**
- * 2. Get the IMAP changes into our database cache
- */
- $this->mailboxSync->sync($account, $this->logger, true, $client);
- } finally {
- $client->logout();
+ /**
+ * @throws ClientException
+ * @throws DoesNotExistException
+ * @throws ServiceException
+ */
+ public function deleteThread(Account $account, Mailbox $mailbox, string $threadRootId): void {
+ if ($account->getMailAccount()->getTrashMailboxId() === null) {
+ throw new TrashMailboxNotSetException();
}
- /**
- * 3. Return the cached object with the new ID
- */
- try {
- return $this->mailboxMapper->find($account, $name);
- } catch (DoesNotExistException $e) {
- throw new ServiceException("The renamed mailbox $name does not exist", 0, $e);
+ $messages = $this->fetchThread($account, $mailbox, $threadRootId);
+ if ($messages === []) {
+ return;
}
+
+ $this->deleteMessages($account, $mailbox, ...$messages);
}
/**
* @param Account $account
* @param Mailbox $mailbox
+ * @param Message $message
+ * @return Attachment[]
*
+ * @throws DoesNotExistException
* @throws ServiceException
*/
- #[\Override]
- public function deleteMailbox(Account $account,
- Mailbox $mailbox): void {
- $client = $this->imapClientFactory->getClient($account);
- try {
- $this->folderMapper->delete($client, $mailbox->getName());
- } finally {
- $client->logout();
- }
- $this->mailboxMapper->delete($mailbox);
+ public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message): array {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchAttachments($account, $mailbox, $message);
}
/**
- * Clear messages in folder
- *
* @param Account $account
* @param Mailbox $mailbox
+ * @param Message $message
+ * @param string $attachmentId
+ * @return Attachment
*
* @throws DoesNotExistException
- * @throws Horde_Imap_Client_Exception
- * @throws Horde_Imap_Client_Exception_NoSupportExtension
* @throws ServiceException
*/
- #[\Override]
- public function clearMailbox(Account $account,
- Mailbox $mailbox): void {
- $client = $this->imapClientFactory->getClient($account);
- $trashMailboxId = $account->getMailAccount()->getTrashMailboxId();
- $currentMailboxId = $mailbox->getId();
- try {
- if (($currentMailboxId !== $trashMailboxId) && !is_null($trashMailboxId)) {
- $trash = $this->mailboxMapper->findById($trashMailboxId);
- $client->copy($mailbox->getName(), $trash->getName(), [
- 'move' => true
- ]);
- } else {
- $client->expunge($mailbox->getName(), [
- 'delete' => true
- ]);
- }
- $this->dbMessageMapper->deleteAll($mailbox);
- } finally {
- $client->logout();
- }
+ public function getMailAttachment(Account $account, Mailbox $mailbox, Message $message, string $attachmentId): Attachment {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->fetchAttachment($account, $mailbox, $message, $attachmentId);
}
/**
* @param Account $account
- * @param Mailbox $mailbox
- * @param Message $message
- * @return Attachment[]
+ *
+ * @return Quota|null
+ * @see https://tools.ietf.org/html/rfc2087
+ *
+ * @throws ServiceException
*/
- #[\Override]
- public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message): array {
- $client = $this->imapClientFactory->getClient($account);
- try {
- return $this->imapMessageMapper->getAttachments(
- $client,
- $mailbox->getName(),
- $message->getUid(),
- $account->getUserId(),
- );
- } finally {
- $client->logout();
- }
+ public function getQuota(Account $account): ?Quota {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->getQuota($account);
}
/**
+ * Check IMAP server for support for PERMANENTFLAGS
+ *
* @param Account $account
* @param Mailbox $mailbox
- * @param Message $message
- * @param string $attachmentId
- * @return Attachment
+ * @return boolean
*
- * @throws DoesNotExistException
- * @throws Horde_Imap_Client_Exception
- * @throws Horde_Imap_Client_Exception_NoSupportExtension
* @throws ServiceException
- * @throws Horde_Mime_Exception
*/
- #[\Override]
- public function getMailAttachment(Account $account,
- Mailbox $mailbox,
- Message $message,
- string $attachmentId): Attachment {
- $client = $this->imapClientFactory->getClient($account);
- try {
- return $this->imapMessageMapper->getAttachment(
- $client,
- $mailbox->getName(),
- $message->getUid(),
- $attachmentId,
- $account->getUserId(),
- );
- } finally {
- $client->logout();
- }
+ public function isPermflagsEnabled(Account $account, Mailbox $mailbox): bool {
+ return $this->protocolFactory
+ ->messageConnector($account)
+ ->isPermflagsEnabled($account, $mailbox);
}
/**
@@ -715,8 +517,7 @@ public function getMailAttachment(Account $account,
* @return Tag
* @throws ClientException
*/
- #[\Override]
- public function getTagByImapLabel(string $imapLabel, string $userId): Tag {
+ public function getTagByLabel(string $imapLabel, string $userId): Tag {
try {
return $this->tagMapper->getTagByImapLabel($imapLabel, $userId);
} catch (DoesNotExistException $e) {
@@ -725,61 +526,8 @@ public function getTagByImapLabel(string $imapLabel, string $userId): Tag {
}
/**
- * Filter out IMAP flags that aren't supported by the client server
- *
- * @param string $flag
- * @param string $mailbox
- * @return array
- */
- public function filterFlags(Horde_Imap_Client_Socket $client, Account $account, string $flag, string $mailbox): array {
- // check if flag is RFC defined system flag
- if (array_key_exists($flag, self::SYSTEM_FLAGS) === true) {
- return self::SYSTEM_FLAGS[$flag];
- }
- // check if server supports custom keywords / this specific keyword
- try {
- $capabilities = $client->status($mailbox, Horde_Imap_Client::STATUS_PERMFLAGS);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not get message flag options from IMAP: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- }
- // check if server returned supported flags
- if (!isset($capabilities['permflags'])) {
- return [];
- }
- // check if server supports custom flags or specific flag
- if (in_array("\*", $capabilities['permflags']) || in_array($flag, $capabilities['permflags'])) {
- return [$flag];
- }
-
- return [];
- }
-
- /**
- * Check IMAP server for support for PERMANENTFLAGS
- *
- * @param Account $account
- * @param string $mailbox
- * @return boolean
+ * @throws ClientException
*/
- #[\Override]
- public function isPermflagsEnabled(Horde_Imap_Client_Socket $client, Account $account, string $mailbox): bool {
- try {
- $capabilities = $client->status($mailbox, Horde_Imap_Client::STATUS_PERMFLAGS);
- } catch (Horde_Imap_Client_Exception $e) {
- throw new ServiceException(
- 'Could not get message flag options from IMAP: ' . $e->getMessage(),
- $e->getCode(),
- $e
- );
- }
- return (is_array($capabilities) === true && array_key_exists('permflags', $capabilities) === true && in_array("\*", $capabilities['permflags'], true) === true);
- }
-
- #[\Override]
public function createTag(string $displayName, string $color, string $userId): Tag {
try {
$imapLabel = $this->imapFlag->create($displayName);
@@ -788,7 +536,11 @@ public function createTag(string $displayName, string $color, string $userId): T
}
try {
- return $this->getTagByImapLabel($imapLabel, $userId);
+ try {
+ return $this->tagMapper->getTagByImapLabel($imapLabel, $userId);
+ } catch (DoesNotExistException $e) {
+ throw new ClientException('Unknown Tag', 0, $e);
+ }
} catch (ClientException $e) {
// it's valid that a tag does not exist.
}
@@ -803,7 +555,9 @@ public function createTag(string $displayName, string $color, string $userId): T
return $this->tagMapper->insert($tag);
}
- #[\Override]
+ /**
+ * @throws ClientException
+ */
public function updateTag(int $id, string $displayName, string $color, string $userId): Tag {
try {
$tag = $this->tagMapper->getTagForUser($id, $userId);
@@ -817,120 +571,55 @@ public function updateTag(int $id, string $displayName, string $color, string $u
return $this->tagMapper->update($tag);
}
- #[\Override]
- public function deleteTag(int $id, string $userId, array $accounts) :Tag {
+ /**
+ * @param int $id tag id
+ * @param string $userId user id of the tag owner
+ * @param Account[] $accounts accounts to remove the tag from
+ *
+ * @throws ClientException
+ * @throws ServiceException
+ */
+ public function deleteTag(int $id, string $userId, array $accounts): Tag {
try {
$tag = $this->tagMapper->getTagForUser($id, $userId);
} catch (DoesNotExistException $e) {
throw new ClientException('Tag not found', 0, $e);
}
- foreach ($accounts as $account) {
- $this->deleteTagForAccount($id, $userId, $tag, $account);
- }
- return $this->tagMapper->delete($tag);
- }
-
- #[\Override]
- public function deleteTagForAccount(int $id, string $userId, Tag $tag, Account $account) :void {
- try {
- $messageTags = $this->messageTagsMapper->getMessagesByTag($id);
- $messages = array_merge(... array_map(fn ($messageTag) => $this->getByMessageId($account, $messageTag->getImapMessageId()), array_values($messageTags)));
- } catch (DoesNotExistException $e) {
- throw new ClientException('Messages not found', 0, $e);
- }
-
- $client = $this->imapClientFactory->getClient($account);
+ // find all messages with this tag (independent of the account)
+ $messageTags = $this->messageTagsMapper->getMessagesByTag($id);
- foreach ($messageTags as $messageTag) {
- $this->messageTagsMapper->delete($messageTag);
- }
- $groupedMessages = [];
- foreach ($messages as $message) {
- $mailboxId = $message->getMailboxId();
- if (array_key_exists($mailboxId, $groupedMessages)) {
- $groupedMessages[$mailboxId][] = $message;
- } else {
- $groupedMessages[$mailboxId] = [$message];
+ foreach ($accounts as $account) {
+ try {
+ $messages = array_merge(... array_map(fn ($messageTag) => $this->getByMessageId($account, $messageTag->getImapMessageId()), array_values($messageTags)));
+ } catch (DoesNotExistException $e) {
+ throw new ClientException('Messages not found', 0, $e);
}
- }
- try {
- foreach ($groupedMessages as $mailboxId => $messages) {
- $mailbox = $this->getMailbox($userId, $mailboxId);
- $this->tagMessagesWithClient($client, $account, $mailbox, $messages, $tag, false);
+ if ($messages === []) {
+ continue;
}
- } finally {
- $client->logout();
- }
- }
-
- #[\Override]
- public function moveThread(Account $srcAccount, Mailbox $srcMailbox, Account $dstAccount, Mailbox $dstMailbox, string $threadRootId): array {
- $mailAccount = $srcAccount->getMailAccount();
- $messageInTrash = $srcMailbox->getId() === $mailAccount->getTrashMailboxId();
- $messages = $this->threadMapper->findMessageUidsAndMailboxNamesByAccountAndThreadRoot(
- $mailAccount,
- $threadRootId,
- $messageInTrash
- );
-
- $newUids = [];
- foreach ($messages as $message) {
- $this->logger->debug('move message', [
- 'messageId' => $message['messageUid'],
- 'srcMailboxId' => $srcMailbox->getId(),
- 'dstMailboxId' => $dstMailbox->getId()
- ]);
-
- $newUid = $this->moveMessage(
- $srcAccount,
- $message['mailboxName'],
- $message['messageUid'],
- $dstAccount,
- $dstMailbox->getName()
- );
- if ($newUid !== null) {
- $newUids[] = $newUid;
+ // the connector removes keywords per mailbox, so group the messages accordingly
+ $messagesByMailbox = [];
+ foreach ($messages as $message) {
+ $messagesByMailbox[$message->getMailboxId()][] = $message;
+ }
+ foreach ($messagesByMailbox as $mailboxId => $mailboxMessages) {
+ try {
+ $mailbox = $this->mailboxMapper->findById($mailboxId);
+ } catch (DoesNotExistException $e) {
+ continue;
+ }
+ $this->tagMessages($account, $mailbox, $tag, false, ...$mailboxMessages);
}
}
- return $newUids;
- }
-
- /**
- * @throws ClientException
- * @throws ServiceException
- */
- #[\Override]
- public function deleteThread(Account $account, Mailbox $mailbox, string $threadRootId): void {
- $mailAccount = $account->getMailAccount();
- $messageInTrash = $mailbox->getId() === $mailAccount->getTrashMailboxId();
-
- $messages = $this->threadMapper->findMessageUidsAndMailboxNamesByAccountAndThreadRoot(
- $mailAccount,
- $threadRootId,
- $messageInTrash
- );
- foreach ($messages as $message) {
- $this->logger->debug('deleting message', [
- 'messageId' => $message['messageUid'],
- 'mailboxId' => $mailbox->getId(),
- ]);
-
- $this->deleteMessage(
- $account,
- $message['mailboxName'],
- $message['messageUid']
- );
+ // update the local store
+ foreach ($messageTags as $messageTag) {
+ $this->messageTagsMapper->delete($messageTag);
}
- }
- /**
- * @return Message[]
- */
- #[\Override]
- public function getByMessageId(Account $account, string $messageId): array {
- return $this->dbMessageMapper->findByMessageId($account, $messageId);
+ return $this->tagMapper->delete($tag);
}
+
}
diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php
index c44dbde5da..d67155942b 100644
--- a/lib/Service/MailTransmission.php
+++ b/lib/Service/MailTransmission.php
@@ -30,7 +30,6 @@
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Mailbox;
@@ -42,9 +41,9 @@
use OCA\Mail\Events\SaveDraftEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\DataUri\DataUriParser;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCA\Mail\Support\PerformanceLogger;
@@ -61,7 +60,7 @@ class MailTransmission implements IMailTransmission {
];
public function __construct(
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
private SmtpClientFactory $smtpClientFactory,
private IEventDispatcher $eventDispatcher,
private MailboxMapper $mailboxMapper,
@@ -70,7 +69,7 @@ public function __construct(
private PerformanceLogger $performanceLogger,
private AliasesService $aliasesService,
private TransmissionService $transmissionService,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
) {
}
@@ -252,7 +251,7 @@ public function saveLocalDraft(Account $account, LocalMessage $message): void {
$perfLogger->step('build local draft message');
// Use a null transport to trigger MIME body encoding without sending
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$mail->send(new Horde_Mail_Transport_Null(), false, false);
$perfLogger->step('create IMAP draft message');
@@ -334,7 +333,7 @@ public function saveDraft(NewMessageData $message, ?Message $previousDraft = nul
$perfLogger->step('build draft message');
// Use a null transport to trigger MIME body encoding without sending
- $client = $this->imapClientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$mail->send(new Horde_Mail_Transport_Null(), false, false);
$perfLogger->step('create IMAP message');
@@ -402,7 +401,7 @@ public function sendMdn(Account $account, Mailbox $mailbox, Message $message): v
'peek' => true,
]);
- $imapClient = $this->imapClientFactory->getClient($account);
+ $imapClient = $this->protocolFactory->imapClient($account);
try {
/** @var Horde_Imap_Client_Data_Fetch[] $fetchResults */
$fetchResults = iterator_to_array($imapClient->fetch($mailbox->getName(), $query, [
diff --git a/lib/Service/OutboxService.php b/lib/Service/OutboxService.php
index f4aee70355..530f3218ab 100644
--- a/lib/Service/OutboxService.php
+++ b/lib/Service/OutboxService.php
@@ -10,14 +10,13 @@
namespace OCA\Mail\Service;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\OutboxMessageCreatedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -39,8 +38,8 @@ public function __construct(
private LocalMessageMapper $mapper,
private AttachmentService $attachmentService,
IEventDispatcher $eventDispatcher,
- private IMAPClientFactory $clientFactory,
- private IMailManager $mailManager,
+ private ProtocolFactory $protocolFactory,
+ private MailManager $mailManager,
private AccountService $accountService,
ITimeFactory $timeFactory,
private LoggerInterface $logger,
@@ -118,7 +117,7 @@ public function saveMessage(Account $account, LocalMessage $message, array $to,
return $message;
}
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
@@ -150,7 +149,7 @@ public function updateMessage(Account $account, LocalMessage $message, array $to
return $message;
}
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
diff --git a/lib/Service/Search/MailSearch.php b/lib/Service/Search/MailSearch.php
index 6212fa5214..aacb4d3046 100644
--- a/lib/Service/Search/MailSearch.php
+++ b/lib/Service/Search/MailSearch.php
@@ -20,7 +20,7 @@
use OCA\Mail\Exception\MailboxNotCachedException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\PreviewEnhancer;
-use OCA\Mail\IMAP\Search\Provider as ImapSearchProvider;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUser;
@@ -31,7 +31,7 @@ class MailSearch implements IMailSearch {
public function __construct(
private FilterStringParser $filterStringParser,
- private ImapSearchProvider $imapSearchProvider,
+ private MailManager $mailManager,
private MessageMapper $messageMapper,
private PreviewEnhancer $previewEnhancer,
ITimeFactory $timeFactory,
@@ -140,7 +140,7 @@ private function getIdsLocally(Account $account, Mailbox $mailbox, SearchQuery $
return $this->messageMapper->findIdsByQuery($mailbox, $query, $sortOrder, $limit);
}
- $fromImap = $this->imapSearchProvider->findMatches(
+ $fromImap = $this->mailManager->findMessages(
$account,
$mailbox,
$query
diff --git a/lib/Service/SetupService.php b/lib/Service/SetupService.php
index beb857e560..e7ce92aec2 100644
--- a/lib/Service/SetupService.php
+++ b/lib/Service/SetupService.php
@@ -18,7 +18,7 @@
use OCA\Mail\Db\TagMapper;
use OCA\Mail\Exception\CouldNotConnectException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCP\Security\ICrypto;
use Psr\Log\LoggerInterface;
@@ -32,7 +32,7 @@ public function __construct(
private AccountService $accountService,
ICrypto $crypto,
private SmtpClientFactory $smtpClientFactory,
- private IMAPClientFactory $imapClientFactory,
+ private ProtocolFactory $protocolFactory,
private LoggerInterface $logger,
private TagMapper $tagMapper,
) {
@@ -109,7 +109,7 @@ public function createNewAccount(string $accountName,
protected function testConnectivity(Account $account): void {
$mailAccount = $account->getMailAccount();
- $imapClient = $this->imapClientFactory->getClient($account);
+ $imapClient = $this->protocolFactory->imapClient($account);
try {
$imapClient->login();
} catch (Horde_Imap_Client_Exception $e) {
diff --git a/lib/Service/SnoozeService.php b/lib/Service/SnoozeService.php
index 56426923fa..0fb224be01 100644
--- a/lib/Service/SnoozeService.php
+++ b/lib/Service/SnoozeService.php
@@ -10,7 +10,6 @@
namespace OCA\Mail\Service;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
@@ -20,7 +19,6 @@
use OCA\Mail\Db\MessageSnoozeMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
@@ -32,12 +30,11 @@ class SnoozeService {
public function __construct(
private ITimeFactory $time,
private LoggerInterface $logger,
- private IMAPClientFactory $clientFactory,
private MessageMapper $messageMapper,
private MessageSnoozeMapper $messageSnoozeMapper,
private MailAccountMapper $accountMapper,
private MailboxMapper $mailboxMapper,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private AccountService $accountService,
) {
}
@@ -87,10 +84,10 @@ public function snoozeMessage(
): void {
$newUid = $this->mailManager->moveMessage(
$srcAccount,
- $srcMailbox->getName(),
- $message->getUid(),
+ $srcMailbox,
+ $message,
$dstAccount,
- $dstMailbox->getName()
+ $dstMailbox
);
// TODO: This is bad - we should handle this case more gracefully!
@@ -127,21 +124,24 @@ public function unSnoozeMessage(
$originalMailboxName = 'INBOX';
+ $originalMailbox = null;
if ($originalMailboxId !== null) {
try {
$originalMailbox = $this->mailboxMapper->findById($originalMailboxId);
- $originalMailboxName = $originalMailbox->getName();
} catch (DoesNotExistException $e) {
// Could not find mailbox, moving back to INBOX
}
}
+ if ($originalMailbox === null) {
+ $originalMailbox = $this->mailboxMapper->find($srcAccount, $originalMailboxName);
+ }
$this->mailManager->moveMessage(
$srcAccount,
- $snoozedMailbox->getName(),
- $message->getUid(),
+ $snoozedMailbox,
+ $message,
$srcAccount,
- $originalMailboxName
+ $originalMailbox
);
$this->messageSnoozeMapper->deleteByMailboxIdAndUid(
@@ -204,24 +204,27 @@ public function unSnoozeThread(
$originalMailboxName = 'INBOX';
+ $originalMailbox = null;
if ($originalMailboxId !== null) {
try {
$originalMailbox = $this->mailboxMapper->findById($originalMailboxId);
- $originalMailboxName = $originalMailbox->getName();
} catch (DoesNotExistException $e) {
// Could not find mailbox, moving back to INBOX
}
}
+ if ($originalMailbox === null) {
+ $originalMailbox = $this->mailboxMapper->find($srcAccount, $originalMailboxName);
+ }
$messages = $this->messageMapper->findThread($srcAccount, $selectedMessage->getThreadRootId());
foreach ($messages as $message) {
$this->mailManager->moveMessage(
$srcAccount,
- $snoozedMailbox->getName(),
- $message->getUid(),
+ $snoozedMailbox,
+ $message,
$srcAccount,
- $originalMailboxName
+ $originalMailbox
);
$this->messageSnoozeMapper->deleteByMailboxIdAndUid(
@@ -301,42 +304,40 @@ private function wakeMessagesByAccount(Account $account): void {
return;
}
- $client = $this->clientFactory->getClient($account);
- try {
- foreach ($messages as $message) {
- $srcMailboxId = $this->messageSnoozeMapper->getSrcMailboxId(
- $message->getMailboxId(),
- $message->getUid(),
- );
-
- $srcMailboxName = 'INBOX';
-
- if ($srcMailboxId !== null) {
- try {
- $srcMailbox = $this->mailboxMapper->findById($srcMailboxId);
- $srcMailboxName = $srcMailbox->getName();
- } catch (DoesNotExistException $e) {
- // Could not find mailbox, moving back to INBOX
- }
+ foreach ($messages as $message) {
+ $srcMailboxId = $this->messageSnoozeMapper->getSrcMailboxId(
+ $message->getMailboxId(),
+ $message->getUid(),
+ );
+
+ $srcMailboxName = 'INBOX';
+
+ $srcMailbox = null;
+ if ($srcMailboxId !== null) {
+ try {
+ $srcMailbox = $this->mailboxMapper->findById($srcMailboxId);
+ } catch (DoesNotExistException $e) {
+ // Could not find mailbox, moving back to INBOX
}
+ }
+ if ($srcMailbox === null) {
+ $srcMailbox = $this->mailboxMapper->find($account, $srcMailboxName);
+ }
- $this->mailManager->flagMessage($account, $snoozeMailbox->getName(), $message->getUid(), 'seen', false);
+ $this->mailManager->flagMessages($account, $snoozeMailbox, 'seen', false, $message);
- $this->mailManager->moveMessage(
- $account,
- $snoozeMailbox->getName(),
- $message->getUid(),
- $account,
- $srcMailboxName
- );
+ $this->mailManager->moveMessage(
+ $account,
+ $snoozeMailbox,
+ $message,
+ $account,
+ $srcMailbox
+ );
- $this->messageSnoozeMapper->deleteByMailboxIdAndUid(
- $message->getMailboxId(),
- $message->getUid(),
- );
- }
- } finally {
- $client->logout();
+ $this->messageSnoozeMapper->deleteByMailboxIdAndUid(
+ $message->getMailboxId(),
+ $message->getUid(),
+ );
}
}
diff --git a/lib/Service/Sync/ImapToDbSynchronizer.php b/lib/Service/Sync/ImapToDbSynchronizer.php
index 31d679696a..3efa591552 100644
--- a/lib/Service/Sync/ImapToDbSynchronizer.php
+++ b/lib/Service/Sync/ImapToDbSynchronizer.php
@@ -14,7 +14,6 @@
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Ids;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper as DatabaseMessageMapper;
@@ -29,12 +28,13 @@
use OCA\Mail\Exception\MailboxNotCachedException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\UidValidityChangedException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\IMAP\Sync\Request;
use OCA\Mail\IMAP\Sync\Synchronizer;
use OCA\Mail\Model\IMAPMessage;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Classification\NewMessagesClassifier;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Support\PerformanceLogger;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
@@ -55,7 +55,7 @@ class ImapToDbSynchronizer {
public function __construct(
private DatabaseMessageMapper $dbMapper,
- private IMAPClientFactory $clientFactory,
+ private ProtocolFactory $protocolFactory,
private ImapMessageMapper $imapMapper,
private MailboxMapper $mailboxMapper,
DatabaseMessageMapper $messageMapper,
@@ -63,7 +63,7 @@ public function __construct(
IEventDispatcher $dispatcher,
private PerformanceLogger $performanceLogger,
private LoggerInterface $logger,
- private IMailManager $mailManager,
+ private MailManager $mailManager,
private TagMapper $tagMapper,
private NewMessagesClassifier $newMessagesClassifier,
) {
@@ -84,7 +84,7 @@ public function syncAccount(Account $account,
$sentMailboxId = $account->getMailAccount()->getSentMailboxId();
$trashRetentionDays = $account->getMailAccount()->getTrashRetentionDays();
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
foreach ($this->mailboxMapper->findAll($account) as $mailbox) {
$syncTrash = $trashMailboxId === $mailbox->getId() && $trashRetentionDays !== null;
@@ -304,7 +304,7 @@ private function runInitialSync(
// every iteration of the findAll loop, causing a memory leak on large
// mailboxes (commit e50c214ff). Do NOT logout $client — the caller owns
// it and we need it below for getSyncToken.
- $noCacheClient = $this->clientFactory->getClient($account, false);
+ $noCacheClient = $this->protocolFactory->imapClient($account, false);
try {
$highestKnownUid = $this->dbMapper->findHighestUid($mailbox);
try {
@@ -466,7 +466,7 @@ private function runPartialSync(
);
$perf->step('get changed messages via Horde');
- $permflagsEnabled = $this->mailManager->isPermflagsEnabled($client, $account, $mailbox->getName());
+ $permflagsEnabled = $this->mailManager->isPermflagsEnabled($account, $mailbox);
foreach (array_chunk($response->getChangedMessages(), 500) as $chunk) {
$this->dbMapper->updateBulk($account, $permflagsEnabled, ...array_map(static fn (IMAPMessage $imapMessage) => $imapMessage->toDbMessage($mailbox->getId(), $account->getMailAccount()), $chunk));
@@ -536,7 +536,7 @@ public function repairSync(
);
// Need to use a client without a cache here (to disable QRESYNC entirely)
- $client = $this->clientFactory->getClient($account, false);
+ $client = $this->protocolFactory->imapClient($account, false);
try {
$knownUids = $this->dbMapper->findAllUids($mailbox);
$hordeMailbox = new \Horde_Imap_Client_Mailbox($mailbox->getName());
diff --git a/lib/Service/Sync/SyncService.php b/lib/Service/Sync/SyncService.php
index 2dd31103f3..03c834d443 100644
--- a/lib/Service/Sync/SyncService.php
+++ b/lib/Service/Sync/SyncService.php
@@ -18,10 +18,10 @@
use OCA\Mail\Exception\MailboxLockedException;
use OCA\Mail\Exception\MailboxNotCachedException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\IMAP\PreviewEnhancer;
use OCA\Mail\IMAP\Sync\Response;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Search\FilterStringParser;
use OCA\Mail\Service\Search\SearchQuery;
use Psr\Log\LoggerInterface;
@@ -31,7 +31,7 @@
class SyncService {
public function __construct(
- private IMAPClientFactory $clientFactory,
+ private ProtocolFactory $protocolFactory,
private ImapToDbSynchronizer $synchronizer,
private FilterStringParser $filterStringParser,
private MessageMapper $messageMapper,
@@ -48,9 +48,8 @@ public function __construct(
* @throws MailboxLockedException
* @throws ServiceException
*/
- public function clearCache(Account $account,
- Mailbox $mailbox): void {
- $this->synchronizer->clearCache($account, $mailbox);
+ public function clearCache(Account $account, Mailbox $mailbox): void {
+ $this->protocolFactory->messageConnector($account)->clearCache($account, $mailbox);
}
/**
@@ -60,7 +59,7 @@ public function clearCache(Account $account,
* @throws ServiceException
*/
public function repairSync(Account $account, Mailbox $mailbox): void {
- $this->synchronizer->repairSync($account, $mailbox, $this->logger);
+ $this->protocolFactory->messageConnector($account)->repairSync($account, $mailbox);
}
/**
@@ -84,26 +83,26 @@ public function syncMailbox(Account $account,
?int $lastMessageTimestamp,
?array $knownIds = null,
string $sortOrder = IMailSearch::ORDER_NEWEST_FIRST,
- ?string $filter = null): Response {
+ ?string $filter = null,
+ ): Response {
if ($partialOnly && !$mailbox->isCached()) {
throw MailboxNotCachedException::from($mailbox);
}
- $client = $this->clientFactory->getClient($account);
-
- $this->synchronizer->sync(
- $account,
- $client,
- $mailbox,
- $this->logger,
- $criteria,
- $knownIds === null ? null : $this->messageMapper->findUidsForIds($mailbox, $knownIds),
- !$partialOnly
- );
-
- $this->mailboxSync->syncStats($client, $mailbox);
-
- $client->logout();
+ $this->protocolFactory
+ ->mailboxConnector($account)
+ ->syncOne($account, $mailbox);
+
+ $this->protocolFactory
+ ->messageConnector($account)
+ ->syncMailbox(
+ $account,
+ $mailbox,
+ $this->logger,
+ $criteria,
+ $knownIds === null ? null : $this->messageMapper->findUidsForIds($mailbox, $knownIds),
+ !$partialOnly,
+ );
$query = $filter === null ? null : $this->filterStringParser->parse($filter);
return $this->getDatabaseSyncChanges(
diff --git a/lib/SetupChecks/MailConnectionPerformance.php b/lib/SetupChecks/MailConnectionPerformance.php
index ec764e1216..aff68314a9 100644
--- a/lib/SetupChecks/MailConnectionPerformance.php
+++ b/lib/SetupChecks/MailConnectionPerformance.php
@@ -13,7 +13,7 @@
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\ProvisioningMapper;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
@@ -26,7 +26,7 @@ public function __construct(
private LoggerInterface $logger,
private ProvisioningMapper $provisioningMapper,
private MailAccountMapper $accountMapper,
- private IMAPClientFactory $clientFactory,
+ private ProtocolFactory $protocolFactory,
private MicroTime $microtime,
) {
}
@@ -59,7 +59,7 @@ public function run(): SetupResult {
foreach ($collection as $accountId) {
$account = new Account($this->accountMapper->findById((int)$accountId));
try {
- $client = $this->clientFactory->getClient($account);
+ $client = $this->protocolFactory->imapClient($account);
} catch (ServiceException $e) {
$this->logger->warning('Error occurred while getting IMAP client for setup check: ' . $e->getMessage(), [
'exception' => $e,
diff --git a/tests/Integration/Connector/AbstractMailboxConnectorTest.php b/tests/Integration/Connector/AbstractMailboxConnectorTest.php
new file mode 100644
index 0000000000..0311668b1f
--- /dev/null
+++ b/tests/Integration/Connector/AbstractMailboxConnectorTest.php
@@ -0,0 +1,117 @@
+mailboxMapper = Server::get(MailboxMapper::class);
+ $this->connector = $this->createConnector();
+ $this->account = $this->createAccount();
+ $this->connector->syncAll($this->account, true);
+ }
+
+ /** Unique per test run so leftover server state never collides. */
+ private function uniqueName(string $prefix): string {
+ return $prefix . '-' . uniqid();
+ }
+
+ /** @return string[] */
+ private function localMailboxNames(): array {
+ return array_map(
+ static fn (Mailbox $mailbox): string => $mailbox->getName(),
+ $this->mailboxMapper->findAll($this->account),
+ );
+ }
+
+ public function testSyncListsTheInbox(): void {
+ $inboxes = array_filter(
+ $this->mailboxMapper->findAll($this->account),
+ static fn (Mailbox $mailbox): bool => $mailbox->isInbox(),
+ );
+
+ self::assertNotEmpty($inboxes, 'Synced mailboxes should include the inbox');
+ }
+
+ public function testCreateMailboxAppearsLocally(): void {
+ $name = $this->uniqueName('Created');
+
+ $created = $this->connector->create($this->account, $name);
+
+ self::assertSame($name, $created->getName());
+ self::assertContains($name, $this->localMailboxNames());
+
+ $this->connector->delete($this->account, $created);
+ }
+
+ public function testDeleteMailboxRemovesItLocally(): void {
+ $name = $this->uniqueName('Doomed');
+ $created = $this->connector->create($this->account, $name);
+
+ $this->connector->delete($this->account, $created);
+
+ self::assertNotContains($name, $this->localMailboxNames());
+ $this->expectException(DoesNotExistException::class);
+ $this->mailboxMapper->find($this->account, $name);
+ }
+
+ public function testRenameMailbox(): void {
+ $name = $this->uniqueName('Before');
+ $newName = $this->uniqueName('After');
+ $created = $this->connector->create($this->account, $name);
+
+ $renamed = $this->connector->rename($this->account, $created, $newName);
+
+ self::assertSame($newName, $renamed->getName());
+ $names = $this->localMailboxNames();
+ self::assertContains($newName, $names);
+ self::assertNotContains($name, $names);
+
+ $this->connector->delete($this->account, $renamed);
+ }
+
+ public function testSubscribeAndUnsubscribe(): void {
+ $name = $this->uniqueName('Sub');
+ $created = $this->connector->create($this->account, $name);
+
+ $unsubscribed = $this->connector->subscribe($this->account, $created, false);
+ self::assertStringNotContainsString('\\subscribed', $unsubscribed->getAttributes() ?? '');
+
+ $subscribed = $this->connector->subscribe($this->account, $unsubscribed, true);
+ self::assertStringContainsString('\\subscribed', $subscribed->getAttributes() ?? '');
+
+ $this->connector->delete($this->account, $subscribed);
+ }
+}
diff --git a/tests/Integration/Connector/ImapMailboxConnectorTest.php b/tests/Integration/Connector/ImapMailboxConnectorTest.php
new file mode 100644
index 0000000000..835eb27add
--- /dev/null
+++ b/tests/Integration/Connector/ImapMailboxConnectorTest.php
@@ -0,0 +1,30 @@
+createTestAccount());
+ }
+}
diff --git a/tests/Integration/Connector/JmapMailboxConnectorTest.php b/tests/Integration/Connector/JmapMailboxConnectorTest.php
new file mode 100644
index 0000000000..165270541c
--- /dev/null
+++ b/tests/Integration/Connector/JmapMailboxConnectorTest.php
@@ -0,0 +1,28 @@
+createTestAccount());
+ }
+}
diff --git a/tests/Integration/Db/TagMapperTest.php b/tests/Integration/Db/TagMapperTest.php
new file mode 100644
index 0000000000..88d9b14c9d
--- /dev/null
+++ b/tests/Integration/Db/TagMapperTest.php
@@ -0,0 +1,54 @@
+db = \OCP\Server::get(IDBConnection::class);
+ $this->mapper = new TagMapper(
+ $this->db,
+ $this->createMock(IL10N::class),
+ );
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('mail_message_tags')->executeStatement();
+ $qb->delete($this->mapper->getTableName())->executeStatement();
+ }
+
+ public function testTagMessageSetsUserIdWhenInsertingNewTag(): void {
+ $tag = new Tag();
+ $tag->setImapLabel('project-x');
+ $tag->setDisplayName('project-x');
+ $tag->setColor('');
+ $tag->setIsDefaultTag(false);
+
+ $this->mapper->tagMessage($tag, '', 'sync-user');
+
+ $storedTag = $this->mapper->getTagByImapLabel('project-x', 'sync-user');
+
+ self::assertSame('sync-user', $storedTag->getUserId());
+ self::assertSame('project-x', $storedTag->getImapLabel());
+ }
+}
diff --git a/tests/Integration/Framework/JmapTestAccount.php b/tests/Integration/Framework/JmapTestAccount.php
index 6de1eaecba..9f35d7481d 100644
--- a/tests/Integration/Framework/JmapTestAccount.php
+++ b/tests/Integration/Framework/JmapTestAccount.php
@@ -31,8 +31,8 @@ public function createTestAccount(?string $userId = null): MailAccount {
$mailAccount->setName('Tester');
$mailAccount->setEmail('user@example.com');
$mailAccount->setProtocol(MailAccount::PROTOCOL_JMAP);
- $mailAccount->setInboundHost('127.0.0.1');
- $mailAccount->setInboundPort(10080);
+ $mailAccount->setInboundHost('stalwart');
+ $mailAccount->setInboundPort(8080);
$mailAccount->setInboundSslMode('none');
$mailAccount->setInboundUser('user@example.com');
$mailAccount->setInboundPassword(Server::get(ICrypto::class)->encrypt('mypassword'));
diff --git a/tests/Integration/IMAP/MessageMapperTest.php b/tests/Integration/IMAP/MessageMapperTest.php
index d8e2fbd7e4..6bde410533 100644
--- a/tests/Integration/IMAP/MessageMapperTest.php
+++ b/tests/Integration/IMAP/MessageMapperTest.php
@@ -13,9 +13,9 @@
use Horde_Imap_Client;
use Horde_Imap_Client_Exception;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
use OCA\Mail\Tests\Integration\Framework\ImapTestAccount;
@@ -44,8 +44,8 @@ public function testTagging(): void {
$imapMessageMapper = Server::get(ImapMessageMapper::class);
/** @var MessageMapper $messageMapper */
$messageMapper = Server::get(MessageMapper::class);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -126,8 +126,8 @@ public function testGetFlagged(): void {
$account = $this->createTestAccount();
/** @var ImapMessageMapper $messageMapper */
$imapMessageMapper = Server::get(ImapMessageMapper::class);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
diff --git a/tests/Integration/MailboxSynchronizationTest.php b/tests/Integration/MailboxSynchronizationTest.php
index ba7c5b9a24..59b676b9fb 100644
--- a/tests/Integration/MailboxSynchronizationTest.php
+++ b/tests/Integration/MailboxSynchronizationTest.php
@@ -12,11 +12,12 @@
use Horde_Imap_Client;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Controller\MailboxesController;
+use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCA\Mail\Service\Sync\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
@@ -48,11 +49,12 @@ protected function setUp(): void {
Server::get(IRequest::class),
Server::get(AccountService::class),
$this->getTestAccountUserId(),
- Server::get(IMailManager::class),
+ Server::get(MailManager::class),
Server::get(SyncService::class),
Server::get(IConfig::class),
Server::get(ITimeFactory::class),
Server::get(DelegationService::class),
+ Server::get(MailboxMapper::class),
);
$this->account = $this->createTestAccount('user12345');
@@ -65,8 +67,8 @@ public function tearDown(): void {
}
public function testSyncEmptyMailbox() {
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -105,8 +107,8 @@ public function testSyncEmptyMailbox() {
public function testSyncNewMessage() {
/** @var SyncService $syncService */
$syncService = Server::get(SyncService::class);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -153,8 +155,8 @@ public function testSyncChangedMessage() {
->to('user@domain.tld')
->finish();
$uid = $this->saveMessage($mailbox, $message, $this->account);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -194,8 +196,8 @@ public function testSyncVanishedMessage() {
->to('user@domain.tld')
->finish();
$uid = $this->saveMessage($mailbox, $message, $this->account);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -244,8 +246,8 @@ public function testUnsolicitedVanishedMessage() {
->subject('Msg 2')
->finish();
$uid2 = $this->saveMessage($mailbox, $message, $this->account);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -279,14 +281,12 @@ public function testUnsolicitedVanishedMessage() {
self::assertCount(2, $dbMessageMapper->findAllUids($inbox));
// Receive unsolicited vanished uid
- $client = $this->getClient($this->account);
+ $message = $dbMessageMapper->findByUids($inbox, [$uid2])[0];
$mailManager->getSource(
- $client,
new Account($this->account),
- $mailbox,
- $uid2,
+ $inbox,
+ $message,
);
- $client->logout();
// Assert that the unsolicited change was synced to the db
self::assertCount(1, $dbMessageMapper->findAllUids($inbox));
diff --git a/tests/Integration/Service/AntiSpamServiceIntegrationTest.php b/tests/Integration/Service/AntiSpamServiceIntegrationTest.php
index b17ea36126..0a49f001ce 100644
--- a/tests/Integration/Service/AntiSpamServiceIntegrationTest.php
+++ b/tests/Integration/Service/AntiSpamServiceIntegrationTest.php
@@ -10,10 +10,10 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Imap_Client;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Service\AntiSpamService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
use OCA\Mail\Tests\Integration\Framework\ImapTestAccount;
@@ -51,8 +51,8 @@ public function testFlagJunkWithSpamReportActive(): void {
$imapMessageMapper = Server::get(ImapMessageMapper::class);
/** @var MessageMapper $messageMapper */
$messageMapper = Server::get(MessageMapper::class);
- /** @var IMailManager $mailManager */
- $mailManager = Server::get(IMailManager::class);
+ /** @var MailManager $mailManager */
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -79,15 +79,17 @@ public function testFlagJunkWithSpamReportActive(): void {
null
);
+ $message = $messageMapper->findByUids($inbox, [$newUid])[0];
+
// now we flag this message as junk
- $mailManager->flagMessage(new Account($account), $inbox->getName(), $newUid, 'junk', true);
+ $mailManager->flagMessages(new Account($account), $inbox, 'junk', true, $message);
// if everything runs through, we can assert the run has been fine,
// but we can't really test if Listener and Transmission have actually sent the message
$this->addToAssertionCount(1);
// now we flag this message as not junk
- $mailManager->flagMessage(new Account($account), $inbox->getName(), $newUid, 'notjunk', true);
+ $mailManager->flagMessages(new Account($account), $inbox, 'notjunk', true, $message);
// same as before
$this->addToAssertionCount(1);
diff --git a/tests/Integration/Service/DraftServiceIntegrationTest.php b/tests/Integration/Service/DraftServiceIntegrationTest.php
index 828fa89287..5f9e9d564e 100644
--- a/tests/Integration/Service/DraftServiceIntegrationTest.php
+++ b/tests/Integration/Service/DraftServiceIntegrationTest.php
@@ -12,18 +12,18 @@
use ChristophWurst\Nextcloud\Testing\TestUser;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalAttachmentMapper;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\MailAccount;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Attachment\AttachmentStorage;
use OCA\Mail\Service\DraftsService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\OutboxService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
use OCA\Mail\Tests\Integration\Framework\ImapTestAccount;
@@ -66,7 +66,7 @@ class DraftServiceIntegrationTest extends TestCase {
/** @var IEventDispatcher */
private $eventDispatcher;
- /** @var IMAPClientFactory */
+ /** @var ProtocolFactory */
private $clientFactory;
/** @var LocalMessageMapper */
@@ -92,7 +92,7 @@ protected function setUp(): void {
$c = Server::get(ContainerInterface::class);
$userContainer = $c->get(IServerContainer::class);
$this->userFolder = $userContainer->getUserFolder($this->account->getUserId());
- $mailManager = Server::get(IMailManager::class);
+ $mailManager = Server::get(MailManager::class);
$this->attachmentService = new AttachmentService(
$this->userFolder,
Server::get(LocalAttachmentMapper::class),
@@ -109,7 +109,7 @@ protected function setUp(): void {
$this->mapper = Server::get(LocalMessageMapper::class);
$this->transmission = Server::get(IMailTransmission::class);
$this->eventDispatcher = Server::get(IEventDispatcher::class);
- $this->clientFactory = Server::get(IMAPClientFactory::class);
+ $this->clientFactory = Server::get(ProtocolFactory::class);
$this->accountService = $this->createMock(AccountService::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
diff --git a/tests/Integration/Service/MailTransmissionIntegrationTest.php b/tests/Integration/Service/MailTransmissionIntegrationTest.php
index ca124f63d4..70013d9cac 100644
--- a/tests/Integration/Service/MailTransmissionIntegrationTest.php
+++ b/tests/Integration/Service/MailTransmissionIntegrationTest.php
@@ -13,7 +13,6 @@
use OC;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
@@ -23,10 +22,10 @@
use OCA\Mail\Db\Message;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Db\RecipientMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\Chain;
use OCA\Mail\Send\CopySentMessageHandler;
@@ -35,6 +34,7 @@
use OCA\Mail\Send\SentMailboxHandler;
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\Attachment\UploadedFile;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\TransmissionService;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -129,10 +129,10 @@ protected function setUp(): void {
Server::get(FlagRepliedMessageHandler::class),
$this->attachmentService,
$this->localMessageMapper,
- Server::get(IMAPClientFactory::class),
+ Server::get(ProtocolFactory::class),
);
- $this->transmission = new MailTransmission(Server::get(IMAPClientFactory::class),
+ $this->transmission = new MailTransmission(Server::get(ProtocolFactory::class),
Server::get(SmtpClientFactory::class),
Server::get(IEventDispatcher::class),
Server::get(MailboxMapper::class),
@@ -141,7 +141,7 @@ protected function setUp(): void {
Server::get(PerformanceLogger::class),
Server::get(AliasesService::class),
Server::get(TransmissionService::class),
- Server::get(IMailManager::class)
+ Server::get(MailManager::class)
);
}
diff --git a/tests/Integration/Service/OutboxServiceIntegrationTest.php b/tests/Integration/Service/OutboxServiceIntegrationTest.php
index c392a656d6..0bef06dc14 100644
--- a/tests/Integration/Service/OutboxServiceIntegrationTest.php
+++ b/tests/Integration/Service/OutboxServiceIntegrationTest.php
@@ -13,18 +13,18 @@
use Horde_Imap_Client;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachmentMapper;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Attachment\AttachmentStorage;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\OutboxService;
use OCA\Mail\Service\Sync\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
@@ -65,7 +65,7 @@ class OutboxServiceIntegrationTest extends TestCase {
/** @var IEventDispatcher */
private $eventDispatcher;
- /** @var IMAPClientFactory */
+ /** @var ProtocolFactory */
private $clientFactory;
/** @var LocalMessageMapper */
@@ -93,7 +93,7 @@ protected function setUp(): void {
$c = Server::get(ContainerInterface::class);
$userContainer = $c->get(IServerContainer::class);
$this->userFolder = $userContainer->getUserFolder($this->account->getUserId());
- $mailManager = Server::get(IMailManager::class);
+ $mailManager = Server::get(MailManager::class);
$this->attachmentService = new AttachmentService(
$this->userFolder,
Server::get(LocalAttachmentMapper::class),
@@ -109,7 +109,7 @@ protected function setUp(): void {
$this->client = $this->getClient($this->account);
$this->mapper = Server::get(LocalMessageMapper::class);
$this->eventDispatcher = Server::get(IEventDispatcher::class);
- $this->clientFactory = Server::get(IMAPClientFactory::class);
+ $this->clientFactory = Server::get(ProtocolFactory::class);
$this->accountService = Server::get(AccountService::class);
$this->timeFactory = Server::get(ITimeFactory::class);
$this->chain = Server::get(Chain::class);
diff --git a/tests/Integration/Sync/ImapToDbSynchronizerTest.php b/tests/Integration/Sync/ImapToDbSynchronizerTest.php
index 7ac1c7aa3e..7afc93a57c 100644
--- a/tests/Integration/Sync/ImapToDbSynchronizerTest.php
+++ b/tests/Integration/Sync/ImapToDbSynchronizerTest.php
@@ -11,9 +11,9 @@
use Horde_Imap_Client;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCA\Mail\Service\Sync\SyncService;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
@@ -59,7 +59,7 @@ public function testRepairSync(): void {
$uid3 = $this->saveMessage($mailbox, $message, $this->account);
// Retrieve mailbox object
- $mailManager = Server::get(IMailManager::class);
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
@@ -120,7 +120,7 @@ public function testRepairSyncNoopIfNoneVanished(): void {
$uid3 = $this->saveMessage($mailbox, $message, $this->account);
// Retrieve mailbox object
- $mailManager = Server::get(IMailManager::class);
+ $mailManager = Server::get(MailManager::class);
$mailBoxes = $mailManager->getMailboxes(new Account($this->account));
$inbox = null;
foreach ($mailBoxes as $mailBox) {
diff --git a/tests/Unit/BackgroundJob/ContextChat/SubmitContentJobTest.php b/tests/Unit/BackgroundJob/ContextChat/SubmitContentJobTest.php
index 1581c1aeb2..375895c62a 100644
--- a/tests/Unit/BackgroundJob/ContextChat/SubmitContentJobTest.php
+++ b/tests/Unit/BackgroundJob/ContextChat/SubmitContentJobTest.php
@@ -21,7 +21,7 @@
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Events\MessageDeletedEvent;
use OCA\Mail\Events\NewMessagesSynchronized;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Exception\ClientException;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\ContextChat\TaskService;
@@ -63,9 +63,6 @@ class SubmitContentJobTest extends TestCase {
/** @var ITimeFactory|MockObject */
private $time;
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
-
/** @var LoggerInterface|MockObject */
private $logger;
@@ -87,7 +84,6 @@ protected function setUp(): void {
$this->accountService = $this->createMock(AccountService::class);
$this->mailManager = $this->createMock(MailManager::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->contextChatProvider = $this->createMock(ContextChatProvider::class);
$this->contentManager = $this->createMock(IContentManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -99,7 +95,6 @@ protected function setUp(): void {
$this->accountService,
$this->mailManager,
$this->messageMapper,
- $this->imapClientFactory,
$this->contextChatProvider,
$this->contentManager,
$this->logger,
@@ -174,8 +169,6 @@ public function testRunWithContextChat(): void {
$message->setId(2);
$message->setUid(2);
$this->messageMapper->expects($this->once())->method('findByIds')->willReturn([$message]);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())->method('getClient')->willReturn($client);
$imapMessage = $this->createMock(IMAPMessage::class);
$this->mailManager->expects($this->once())->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->expects($this->once())->method('isEncrypted')->willReturn(false);
@@ -184,7 +177,6 @@ public function testRunWithContextChat(): void {
$imapMessage->expects($this->once())->method('getSubject')->willReturn('subject');
$sent = new \Horde_Imap_Client_DateTime('2025-01-01 00:00:00');
$imapMessage->expects($this->once())->method('getSentDate')->willReturn($sent);
- $client->expects($this->once())->method('close');
$this->contextChatProvider->expects($this->once())->method('getAppId')->willReturn('mail');
$this->contextChatProvider->expects($this->once())->method('getId')->willReturn('mail');
$this->contentManager->expects($this->once())->method('submitContent');
@@ -264,10 +256,7 @@ public function testRunWithContextChatWithTimeout(): void {
$this->accountService->expects($this->once())->method('findById')->with()->willReturn($account);
$message = new Message();
$this->messageMapper->expects($this->once())->method('findByIds')->willReturn([$message]);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())->method('getClient')->willReturn($client);
$this->mailManager->expects($this->never())->method('getImapMessage'); // will not get called because the job takes too long already
- $client->expects($this->once())->method('close');
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
@@ -310,13 +299,56 @@ public function testRunWithContextChatWithEncryptedMessage(): void {
$message->setId(1);
$message->setUid(1);
$this->messageMapper->expects($this->once())->method('findByIds')->willReturn([$message]);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())->method('getClient')->willReturn($client);
$imapMessage = $this->createMock(IMAPMessage::class);
$this->mailManager->expects($this->once())->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->expects($this->once())->method('isEncrypted')->willReturn(true);
$imapMessage->expects($this->never())->method('getFullMessage');
- $client->expects($this->once())->method('close');
+
+ $this->submitContentJob->setLastRun(0);
+ $this->submitContentJob->start($this->createMock(IJobList::class));
+ }
+
+ public function testRunWithContextChatSkipsMessageVanishedFromRemote(): void {
+ $this->contentManager->expects($this->once())
+ ->method('isContextChatAvailable')
+ ->willReturn(true);
+ $task = new Task();
+ $task->setLastMessageId(0);
+ $task->setMailboxId(1);
+ $task->setId(1);
+ $this->taskService->expects($this->once())->method('findNext')->willReturn($task);
+ $mailbox = new Mailbox();
+ $mailbox->setId(1);
+ $mailbox->setAccountId(5);
+ $this->mailboxMapper->expects($this->once())->method('findById')->willReturn($mailbox);
+ $this->time->expects($this->any())->method('getTime')
+ ->willReturn(
+ // returned when Job#start asks
+ 12 * 60 * 60,
+ 12 * 60 * 60,
+ // returned when filtering messages
+ ContextChatProvider::CONTEXT_CHAT_MESSAGE_MAX_AGE,
+ // returned before processing messages
+ 0,
+ // returned on first message
+ 0,
+ 0,
+ 0,
+ 0,
+ );
+ $this->messageMapper->expects($this->once())->method('findIdsAfter')
+ ->with($mailbox, 0, 0, ContextChatProvider::CONTEXT_CHAT_IMPORT_MAX_ITEMS)->willReturn([1]);
+ $account = $this->createMock(Account::class);
+ $account->expects($this->any())->method('getUserId')->willReturn('user123');
+ $this->accountService->expects($this->once())->method('findById')->with()->willReturn($account);
+ $message = new Message();
+ $message->setId(1);
+ $message->setUid(1);
+ $this->messageMapper->expects($this->once())->method('findByIds')->willReturn([$message]);
+ $this->mailManager->expects($this->once())->method('getImapMessage')
+ ->willThrowException(new ClientException('Message not found on remote server'));
+ $this->contentManager->expects($this->never())->method('submitContent');
+ $this->taskService->expects($this->once())->method('setLastMessage')->with($task->getMailboxId(), 1);
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
@@ -331,7 +363,6 @@ public function testRunWithContextChatWithFindNextTaskException(): void {
$this->taskService->expects($this->once())->method('findNext')->willThrowException(new \OCP\DB\Exception('An error'));
$this->contentManager->expects($this->never())->method('submitContent');
- $this->imapClientFactory->expects($this->never())->method('getClient');
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
@@ -346,7 +377,6 @@ public function testRunWithContextChatWithFindNextTaskException2(): void {
$this->taskService->expects($this->once())->method('findNext')->willThrowException(new DoesNotExistException('ERROR'));
$this->contentManager->expects($this->never())->method('submitContent');
- $this->imapClientFactory->expects($this->never())->method('getClient');
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
@@ -369,7 +399,6 @@ public function testRunWithContextChatWithFindByIdException1(): void {
$mailbox->setAccountId(5);
$this->mailboxMapper->expects($this->once())->method('findById')->willThrowException(new \OCA\Mail\Exception\ServiceException());
$this->contentManager->expects($this->never())->method('submitContent');
- $this->imapClientFactory->expects($this->never())->method('getClient');
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
@@ -392,7 +421,6 @@ public function testRunWithContextChatWithFindByIdException2(): void {
$mailbox->setAccountId(5);
$this->mailboxMapper->expects($this->once())->method('findById')->willThrowException(new DoesNotExistException('ERROR'));
$this->contentManager->expects($this->never())->method('submitContent');
- $this->imapClientFactory->expects($this->never())->method('getClient');
$this->submitContentJob->setLastRun(0);
$this->submitContentJob->start($this->createMock(IJobList::class));
diff --git a/tests/Unit/BackgroundJob/SyncJobTest.php b/tests/Unit/BackgroundJob/SyncJobTest.php
index 5977d3f4a8..ec7b49aedb 100644
--- a/tests/Unit/BackgroundJob/SyncJobTest.php
+++ b/tests/Unit/BackgroundJob/SyncJobTest.php
@@ -56,12 +56,9 @@ public function testAccountDoesntExist(): void {
->expects(self::once())
->method('remove')
->with(SyncJob::class, ['accountId' => 123]);
- $this->serviceMock->getParameter('mailboxSync')
+ $this->serviceMock->getParameter('protocolFactory')
->expects(self::never())
- ->method('sync');
- $this->serviceMock->getParameter('syncService')
- ->expects(self::never())
- ->method('syncAccount');
+ ->method('mailboxConnector');
$this->job->setArgument([
'accountId' => 123,
@@ -71,9 +68,8 @@ public function testAccountDoesntExist(): void {
}
public function testNoAuthentication(): void {
- $mailAccount = $this->createConfiguredMock(MailAccount::class, [
- 'canAuthenticateImap' => false,
- ]);
+ $mailAccount = new MailAccount();
+ $mailAccount->setProtocol(MailAccount::PROTOCOL_IMAP);
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(123);
$account->method('getUserId')->willReturn('user123');
@@ -91,12 +87,9 @@ public function testNoAuthentication(): void {
$this->serviceMock->getParameter('userManager')
->expects(self::never())
->method('get');
- $this->serviceMock->getParameter('mailboxSync')
+ $this->serviceMock->getParameter('protocolFactory')
->expects(self::never())
- ->method('sync');
- $this->serviceMock->getParameter('syncService')
- ->expects(self::never())
- ->method('syncAccount');
+ ->method('mailboxConnector');
$this->job->setArgument([
'accountId' => 123,
@@ -127,12 +120,9 @@ public function testUserDoesntExist(): void {
->expects(self::once())
->method('debug')
->with('Account 123 of user user123 could not be found or was disabled, skipping background sync');
- $this->serviceMock->getParameter('mailboxSync')
- ->expects(self::never())
- ->method('sync');
- $this->serviceMock->getParameter('syncService')
+ $this->serviceMock->getParameter('protocolFactory')
->expects(self::never())
- ->method('syncAccount');
+ ->method('mailboxConnector');
$this->job->setArgument([
'accountId' => 123,
diff --git a/tests/Unit/Controller/AccountsControllerTest.php b/tests/Unit/Controller/AccountsControllerTest.php
index f44fb623e1..873a4777eb 100644
--- a/tests/Unit/Controller/AccountsControllerTest.php
+++ b/tests/Unit/Controller/AccountsControllerTest.php
@@ -12,7 +12,6 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Controller\AccountsController;
use OCA\Mail\Db\MailAccount;
@@ -24,6 +23,7 @@
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\SetupService;
use OCA\Mail\Service\Sync\SyncService;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -75,7 +75,7 @@ class AccountsControllerTest extends TestCase {
/** @var SetupService|MockObject */
private $setupService;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var SyncService|MockObject */
@@ -110,7 +110,7 @@ protected function setUp(): void {
$this->aliasesService = $this->createMock(AliasesService::class);
$this->transmission = $this->createMock(IMailTransmission::class);
$this->setupService = $this->createMock(SetupService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->syncService = $this->createMock(SyncService::class);
$this->mailboxSync = $this->createMock(mailboxSync::class);
$this->config = $this->createMock(IConfig::class);
diff --git a/tests/Unit/Controller/MailboxesApiControllerTest.php b/tests/Unit/Controller/MailboxesApiControllerTest.php
index 052d40c0ff..53e64c0473 100644
--- a/tests/Unit/Controller/MailboxesApiControllerTest.php
+++ b/tests/Unit/Controller/MailboxesApiControllerTest.php
@@ -11,7 +11,6 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Controller\MailboxesApiController;
use OCA\Mail\Db\MailAccount;
@@ -20,6 +19,7 @@
use OCA\Mail\Folder;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\IRequest;
@@ -31,7 +31,7 @@ class MailboxesApiControllerTest extends TestCase {
private MailboxesApiController $controller;
private IRequest&MockObject $request;
- private IMailManager|MockObject $mailManager;
+ private MailManager|MockObject $mailManager;
private AccountService&MockObject $accountService;
private MockObject|IMailSearch $mailSearch;
private DelegationService&MockObject $delegationService;
@@ -41,7 +41,7 @@ protected function setUp(): void {
$this->request = $this->createMock(IRequest::class);
$this->accountService = $this->createMock(AccountService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->mailSearch = $this->createMock(IMailSearch::class);
$this->delegationService = $this->createMock(DelegationService::class);
$this->delegationService->method('resolveAccountUserId')
diff --git a/tests/Unit/Controller/MailboxesControllerTest.php b/tests/Unit/Controller/MailboxesControllerTest.php
index 8531b024af..b38bc7ad97 100644
--- a/tests/Unit/Controller/MailboxesControllerTest.php
+++ b/tests/Unit/Controller/MailboxesControllerTest.php
@@ -11,14 +11,15 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Controller\MailboxesController;
use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Exception\NotImplemented;
use OCA\Mail\Folder;
use OCA\Mail\IMAP\MailboxStats;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\SyncService;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -39,7 +40,7 @@ class MailboxesControllerTest extends TestCase {
/** @var string */
private $userId = 'john';
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var MailboxesController */
@@ -51,19 +52,21 @@ class MailboxesControllerTest extends TestCase {
private IConfig|MockObject $config;
private ITimeFactory|MockObject $timeFactory;
private DelegationService|MockObject $delegationService;
+ private MailboxMapper|MockObject $mailboxMapper;
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->mailManager = $this->createMock(MailManager::class);
$this->syncService = $this->createMock(SyncService::class);
$this->config = $this->createMock(IConfig::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->delegationService = $this->createMock(DelegationService::class);
$this->delegationService->method('resolveAccountUserId')->willReturn($this->userId);
$this->delegationService->method('resolveMailboxUserId')->willReturn($this->userId);
+ $this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->controller = new MailboxesController(
$this->appName,
@@ -75,6 +78,7 @@ public function setUp(): void {
$this->config,
$this->timeFactory,
$this->delegationService,
+ $this->mailboxMapper,
);
}
@@ -226,9 +230,9 @@ public function testPatchEnableSyncLogsDelegatedAction(): void {
$this->accountService->expects($this->once())
->method('find')
->willReturn($account);
- $this->mailManager->expects($this->once())
- ->method('enableMailboxBackgroundSync')
- ->with($mailbox, true)
+ $this->mailboxMapper->expects($this->once())
+ ->method('update')
+ ->with($mailbox)
->willReturn($mailbox);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
diff --git a/tests/Unit/Controller/MessageApiControllerTest.php b/tests/Unit/Controller/MessageApiControllerTest.php
index 09fa39e1ca..0c76a7363b 100644
--- a/tests/Unit/Controller/MessageApiControllerTest.php
+++ b/tests/Unit/Controller/MessageApiControllerTest.php
@@ -21,7 +21,6 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\SmimeDecryptException;
use OCA\Mail\Exception\UploadException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Model\SmimeData;
use OCA\Mail\Service\AccountService;
@@ -52,7 +51,6 @@ class MessageApiControllerTest extends TestCase {
private AttachmentService|MockObject $attachmentService;
private OutboxService|MockObject $outboxService;
private MailManager|MockObject $mailManager;
- private IMAPClientFactory|MockObject $imapClientFactory;
private LoggerInterface|MockObject $logger;
private MockObject|ITimeFactory $time;
private MockObject|IURLGenerator $urlGenerator;
@@ -77,7 +75,6 @@ protected function setUp(): void {
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->outboxService = $this->createMock(OutboxService::class);
$this->mailManager = $this->createMock(MailManager::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->time = $this->createMock(ITimeFactory::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
@@ -96,7 +93,6 @@ protected function setUp(): void {
$this->attachmentService,
$this->outboxService,
$this->mailManager,
- $this->imapClientFactory,
$this->logger,
$this->time,
$this->urlGenerator,
@@ -128,7 +124,6 @@ public function testGet(bool $encrypted, bool $signed, array $json): void {
$message->setUid(1);
$mailbox = new Mailbox();
$mailbox->setAccountId($this->accountId);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
$imapMessage = $this->createMock(IMAPMessage::class);
$this->logger->expects(self::never())
@@ -147,14 +142,9 @@ public function testGet(bool $encrypted, bool $signed, array $json): void {
->method('find')
->with($this->userId, $this->accountId)
->willReturn($this->account);
- $this->imapClientFactory->expects(self::once())
- ->method('getClient')
- ->willReturn($client);
$this->mailManager->expects(self::once())
->method('getImapMessage')
->willReturn($imapMessage);
- $client->expects(self::once())
- ->method('logout');
$imapMessage->expects(self::once())
->method('getFullMessage')
->with($this->messageId, true)
@@ -242,7 +232,6 @@ public function testGetWithSmimeEncryptionFailed(): void {
$message->setUid(1);
$mailbox = new Mailbox();
$mailbox->setAccountId($this->accountId);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
$imapMessage = $this->createMock(IMAPMessage::class);
$smime = new SmimeData();
$smime->setIsEncrypted(true);
@@ -270,19 +259,14 @@ public function testGetWithSmimeEncryptionFailed(): void {
->method('find')
->with($this->userId, $this->accountId)
->willReturn($this->account);
- $this->imapClientFactory->expects(self::once())
- ->method('getClient')
- ->willReturn($client);
$this->mailManager->expects(self::exactly(2))
->method('getImapMessage')
- ->willReturnCallback(function ($client, $account, $mailbox, $uid, $loadBody) use ($imapMessage) {
+ ->willReturnCallback(function ($account, $mailbox, $message, $loadBody) use ($imapMessage) {
if ($loadBody) {
throw new SmimeDecryptException();
}
return $imapMessage;
});
- $client->expects(self::once())
- ->method('logout');
$imapMessage->expects(self::once())
->method('getFullMessage')
->with($this->messageId, false)
@@ -319,7 +303,6 @@ public function testGetWithSmimeException(): void {
$message->setUid(1);
$mailbox = new Mailbox();
$mailbox->setAccountId($this->accountId);
- $client = $this->createMock(\Horde_Imap_Client_Socket::class);
$this->logger->expects(self::never())
->method('warning');
@@ -337,14 +320,9 @@ public function testGetWithSmimeException(): void {
->method('find')
->with($this->userId, $this->accountId)
->willReturn($this->account);
- $this->imapClientFactory->expects(self::once())
- ->method('getClient')
- ->willReturn($client);
$this->mailManager->expects(self::once())
->method('getImapMessage')
->willThrowException(new ServiceException());
- $client->expects(self::once())
- ->method('logout');
$this->itineraryService->expects(self::never())
->method('getCached');
$this->trustedSenderService->expects(self::never())
@@ -381,8 +359,6 @@ public function testMailboxNotFound(): void {
->willThrowException(new ClientException(''));
$this->accountService->expects(self::never())
->method('find');
- $this->imapClientFactory->expects(self::never())
- ->method('getClient');
$this->mailManager->expects(self::never())
->method('getImapMessage');
$this->itineraryService->expects(self::never())
diff --git a/tests/Unit/Controller/MessagesControllerTest.php b/tests/Unit/Controller/MessagesControllerTest.php
index 1cf19b2221..8a8a4e63b9 100644
--- a/tests/Unit/Controller/MessagesControllerTest.php
+++ b/tests/Unit/Controller/MessagesControllerTest.php
@@ -18,7 +18,6 @@
use OCA\Mail\Account;
use OCA\Mail\Attachment;
use OCA\Mail\Contracts\IDkimService;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Contracts\ITrustedSenderService;
@@ -33,7 +32,6 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\AttachmentDownloadResponse;
use OCA\Mail\Http\HtmlResponse;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Model\Message;
use OCA\Mail\Service\AccountService;
@@ -124,8 +122,6 @@ class MessagesControllerTest extends TestCase {
/** @var MockObject|SmimeService */
private $smimeService;
- /** @var MockObject|IMAPClientFactory */
- private $clientFactory;
private IDkimService $dkimService;
/** @var MockObject|IUserPreferences */
@@ -145,7 +141,7 @@ protected function setUp(): void {
$this->appName = 'mail';
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->accountService = $this->createMock(AccountService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->mailSearch = $this->createMock(IMailSearch::class);
$this->itineraryService = $this->createMock(ItineraryService::class);
$this->userId = 'john';
@@ -159,7 +155,6 @@ protected function setUp(): void {
$this->trustedSenderService = $this->createMock(ITrustedSenderService::class);
$this->mailTransmission = $this->createMock(IMailTransmission::class);
$this->smimeService = $this->createMock(SmimeService::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
$this->dkimService = $this->createMock(IDkimService::class);
$this->userPreferences = $this->createMock(IUserPreferences::class);
$this->snoozeService = $this->createMock(SnoozeService::class);
@@ -202,7 +197,6 @@ protected function setUp(): void {
$this->trustedSenderService,
$this->mailTransmission,
$this->smimeService,
- $this->clientFactory,
$this->dkimService,
$this->userPreferences,
$this->snoozeService,
@@ -249,16 +243,11 @@ public function testGetHtmlBody(): void {
->method('find')
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$imapMessage = $this->createStub(IMAPMessage::class);
$this->mailManager->expects($this->exactly(2))
->method('getImapMessage')
- ->with($client, $this->account, $mailbox, 123, true)
+ ->with($this->account, $mailbox, $message, true)
->willReturn($imapMessage);
- $this->clientFactory->expects($this->exactly(2))
- ->method('getClient')
- ->with($this->account)
- ->willReturn($client);
$expectedPlainResponse = HtmlResponse::plain('');
$expectedPlainResponse->cacheFor(3600);
@@ -700,8 +689,8 @@ public function testSetFlagsUnseen() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('flagMessage')
- ->with($this->account, 'INBOX', 444, 'seen', false);
+ ->method('flagMessages')
+ ->with($this->account, $mailbox, 'seen', false, $message);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
->with($this->userId, $this->userId, "$this->userId updated flags on message <$id> with [seen=false] on behalf of $this->userId");
@@ -739,9 +728,9 @@ public function testSetTagFailing() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->willThrowException(new DoesNotExistException(''));
$this->mailManager->expects($this->never())
- ->method('getTagByImapLabel');
+ ->method('getTagByLabel');
$this->mailManager->expects($this->never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->controller->setTag($id, Tag::LABEL_IMPORTANT);
}
@@ -771,11 +760,11 @@ public function testSetTagNotFound() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('getTagByImapLabel')
+ ->method('getTagByLabel')
->with($imapLabel, $this->userId)
->willThrowException(new ClientException('Computer says no'));
$this->mailManager->expects($this->never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->controller->setTag($id, $imapLabel);
}
@@ -806,12 +795,12 @@ public function testSetTag() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('getTagByImapLabel')
+ ->method('getTagByLabel')
->with($tag->getImapLabel(), $this->userId)
->willReturn($tag);
$this->mailManager->expects($this->once())
- ->method('tagMessage')
- ->with($this->account, $mailbox->getName(), $message, $tag, true);
+ ->method('tagMessages')
+ ->with($this->account, $mailbox, $tag, true, $message);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
->with($this->userId, $this->userId, "$this->userId added tag <{$tag->getImapLabel()}> on message <$id> on behalf of $this->userId");
@@ -843,9 +832,9 @@ public function testRemoveTagFailing() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->willThrowException(new DoesNotExistException(''));
$this->mailManager->expects($this->never())
- ->method('getTagByImapLabel');
+ ->method('getTagByLabel');
$this->mailManager->expects($this->never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->controller->removeTag($id, Tag::LABEL_IMPORTANT);
}
@@ -875,11 +864,11 @@ public function testRemoveTagNotFound() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('getTagByImapLabel')
+ ->method('getTagByLabel')
->with($imapLabel, $this->userId)
->willThrowException(new ClientException('Computer says no'));
$this->mailManager->expects($this->never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->controller->removeTag($id, $imapLabel);
}
@@ -910,12 +899,12 @@ public function testRemoveTag() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('getTagByImapLabel')
+ ->method('getTagByLabel')
->with($tag->getImapLabel(), $this->userId)
->willReturn($tag);
$this->mailManager->expects($this->once())
- ->method('tagMessage')
- ->with($this->account, $mailbox->getName(), $message, $tag, false);
+ ->method('tagMessages')
+ ->with($this->account, $mailbox, $tag, false, $message);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
->with($this->userId, $this->userId, "$this->userId removed tag <{$tag->getImapLabel()}> on message <$id> on behalf of $this->userId");
@@ -949,8 +938,8 @@ public function testSetFlagsFlagged() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
- ->method('flagMessage')
- ->with($this->account, 'INBOX', 444, 'flagged', true);
+ ->method('flagMessages')
+ ->with($this->account, $mailbox, 'flagged', true, $message);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
->with($this->userId, $this->userId, "$this->userId updated flags on message <$id> with [flagged=true] on behalf of $this->userId");
@@ -988,7 +977,7 @@ public function testDestroy() {
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
->method('deleteMessage')
- ->with($this->account, 'INBOX', 444);
+ ->with($this->account, $mailbox, $message);
$this->delegationService->expects($this->once())
->method('logDelegatedAction')
->with($this->userId, $this->userId, "$this->userId deleted message <$id> on behalf of $this->userId");
@@ -1051,7 +1040,7 @@ public function testDestroyWithFolderOrMessageNotFound() {
->will($this->returnValue($this->account));
$this->mailManager->expects($this->once())
->method('deleteMessage')
- ->with($this->account, 'INBOX', 444)
+ ->with($this->account, $mailbox, $message)
->willThrowException(new ServiceException());
$this->expectException(ServiceException::class);
@@ -1177,14 +1166,10 @@ public function testExport() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$source = file_get_contents(__DIR__ . '/../../data/mail-message-123.txt');
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->mailManager->expects($this->exactly(1))
->method('getSource')
- ->with($client, $this->account, $folderId, 123)
+ ->with($this->account, $mailbox, $message)
->willReturn($source);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
$expectedResponse = new AttachmentDownloadResponse(
$source,
@@ -1225,10 +1210,9 @@ public function testSaveFile() {
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$source = file_get_contents(__DIR__ . '/../../data/mail-message-123.txt');
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->mailManager->expects($this->exactly(1))
->method('getSource')
- ->with($client, $this->account, $folderId, 123)
+ ->with($this->account, $mailbox, $message)
->willReturn($source);
$folderNode = $this->createStub(Folder::class);
$this->userFolder->expects($this->once())
@@ -1246,9 +1230,6 @@ public function testSaveFile() {
->method('newFile')
->with('Downloads/core_master has new results.eml')
->will($this->returnValue($file));
- $this->clientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
$expectedResponse = new JSONResponse();
$actualResponse = $this->controller->saveFile($messageId, $targetPath);
@@ -1287,7 +1268,7 @@ public function testGetDkim() {
->will($this->returnValue($account));
$this->dkimService->expects($this->exactly(1))
->method('validate')
- ->with($account, $mailbox, $message->getUid())
+ ->with($account, $mailbox, $message)
->willReturn(true);
$actualResponse = $this->controller->getDkim($message->getId());
@@ -1413,7 +1394,6 @@ public function testSmartReplyNoUser(): void {
$this->trustedSenderService,
$this->mailTransmission,
$this->smimeService,
- $this->clientFactory,
$this->dkimService,
$this->userPreferences,
$this->snoozeService,
diff --git a/tests/Unit/Controller/ThreadControllerTest.php b/tests/Unit/Controller/ThreadControllerTest.php
index fcbb1eb904..1dbb3dd669 100644
--- a/tests/Unit/Controller/ThreadControllerTest.php
+++ b/tests/Unit/Controller/ThreadControllerTest.php
@@ -11,7 +11,6 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Controller\ThreadController;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
@@ -22,6 +21,7 @@
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
use OCA\Mail\Service\DelegationService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\SnoozeService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
@@ -42,7 +42,7 @@ class ThreadControllerTest extends TestCase {
/** @var AccountService|MockObject */
private $accountService;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var SnoozeService|MockObject */
@@ -67,7 +67,7 @@ protected function setUp(): void {
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->userId = 'john';
$this->accountService = $this->createMock(AccountService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->snoozeService = $this->createMock(SnoozeService::class);
$this->aiIntergrationsService = $this->createMock(AiIntegrationsService::class);
$this->logger = $this->createMock(LoggerInterface::class);
diff --git a/tests/Unit/Db/MailboxTest.php b/tests/Unit/Db/MailboxTest.php
index 5d7aa17cbd..f1c4c76cea 100644
--- a/tests/Unit/Db/MailboxTest.php
+++ b/tests/Unit/Db/MailboxTest.php
@@ -23,8 +23,9 @@ protected function setUp(): void {
public static function provideCacheBusterData(): array {
return [
- ['new', 'changed', 'vanished', 'bbddae86e09069fc10c9f2ac401363b4'],
- [null, null, null, 'dca1f7641c34734a8cd1c7b1c45abf73'],
+ ['new', 'changed', 'vanished', null, 'bbddae86e09069fc10c9f2ac401363b4'],
+ [null, null, null, null, 'dca1f7641c34734a8cd1c7b1c45abf73'],
+ [null, null, null, 'state', '3cc6800869b5dcd6495d309a4bd16273'],
];
}
@@ -33,12 +34,14 @@ public function testGetCacheBuster(
?string $syncNewToken,
?string $syncChangedToken,
?string $syncVanishedToken,
+ ?string $state,
string $expectedCacheBuster,
): void {
$this->mailbox->setId(100);
$this->mailbox->setSyncNewToken($syncNewToken);
$this->mailbox->setSyncChangedToken($syncChangedToken);
$this->mailbox->setSyncVanishedToken($syncVanishedToken);
+ $this->mailbox->setState($state);
$this->assertEquals($expectedCacheBuster, $this->mailbox->getCacheBuster());
}
@@ -48,12 +51,14 @@ public function testJsonSerializeCacheBuster(
?string $syncNewToken,
?string $syncChangedToken,
?string $syncVanishedToken,
+ ?string $state,
string $expectedCacheBuster,
): void {
$this->mailbox->setId(100);
$this->mailbox->setSyncNewToken($syncNewToken);
$this->mailbox->setSyncChangedToken($syncChangedToken);
$this->mailbox->setSyncVanishedToken($syncVanishedToken);
+ $this->mailbox->setState($state);
$this->mailbox->setName('INBOX');
$json = $this->mailbox->jsonSerialize();
@@ -61,6 +66,31 @@ public function testJsonSerializeCacheBuster(
$this->assertEquals($expectedCacheBuster, $json['cacheBuster']);
}
+ public static function provideCachedData(): array {
+ return [
+ ['new', 'changed', 'vanished', null, true],
+ [null, null, null, 'state', true],
+ ['new', null, 'vanished', null, false],
+ [null, null, null, null, false],
+ ];
+ }
+
+ /** @dataProvider provideCachedData */
+ public function testIsCached(
+ ?string $syncNewToken,
+ ?string $syncChangedToken,
+ ?string $syncVanishedToken,
+ ?string $state,
+ bool $expected,
+ ): void {
+ $this->mailbox->setSyncNewToken($syncNewToken);
+ $this->mailbox->setSyncChangedToken($syncChangedToken);
+ $this->mailbox->setSyncVanishedToken($syncVanishedToken);
+ $this->mailbox->setState($state);
+
+ $this->assertSame($expected, $this->mailbox->isCached());
+ }
+
public function testHasLocksIgnoresExpiredLock(): void {
$this->mailbox->setSyncNewLock(1000);
diff --git a/tests/Unit/IMAP/ImapMessageConnectorTest.php b/tests/Unit/IMAP/ImapMessageConnectorTest.php
new file mode 100644
index 0000000000..224f098659
--- /dev/null
+++ b/tests/Unit/IMAP/ImapMessageConnectorTest.php
@@ -0,0 +1,183 @@
+protocolFactory = $this->createMock(ProtocolFactory::class);
+ $this->synchronizer = $this->createMock(ImapToDbSynchronizer::class);
+ $this->imapMailboxMapper = $this->createMock(FolderMapper::class);
+ $this->imapMessageMapper = $this->createMock(MessageMapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->connector = new ImapMessageConnector(
+ $this->protocolFactory,
+ $this->synchronizer,
+ $this->imapMailboxMapper,
+ $this->imapMessageMapper,
+ $this->logger,
+ );
+
+ $this->account = $this->createMock(Account::class);
+ $this->client = $this->createMock(Horde_Imap_Client_Socket::class);
+ $this->protocolFactory->method('imapClient')
+ ->with($this->account)
+ ->willReturn($this->client);
+ }
+
+ public function testMoveMessagesLogsOutClientWhenMapperThrows(): void {
+ $sourceMailbox = new Mailbox();
+ $sourceMailbox->setName('INBOX');
+ $targetMailbox = new Mailbox();
+ $targetMailbox->setName('Archive');
+ $message = new Message();
+ $message->setUid(1);
+
+ $this->imapMessageMapper->method('move')
+ ->willThrowException(new ServiceException('could not move'));
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $this->expectException(ServiceException::class);
+
+ $this->connector->moveMessages($this->account, $targetMailbox, $sourceMailbox, $message);
+ }
+
+ public function testDeleteMessagesLogsOutClientWhenMapperThrows(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $message = new Message();
+ $message->setUid(1);
+
+ $this->imapMessageMapper->method('expunge')
+ ->willThrowException(new ServiceException('could not expunge'));
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $this->expectException(ServiceException::class);
+
+ $this->connector->deleteMessages($this->account, $mailbox, $message);
+ }
+
+ public function testFlagMessagesLogsOutClientWhenMapperThrows(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $message = new Message();
+ $message->setUid(1);
+
+ $this->imapMessageMapper->method('addFlag')
+ ->willThrowException(new Horde_Imap_Client_Exception('store failed'));
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $this->expectException(ServiceException::class);
+
+ $this->connector->flagMessages($this->account, $mailbox, 'seen', true, $message);
+ }
+
+ public function testTagMessagesLogsOutClientWhenPermflagsCheckThrows(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $tag = new Tag();
+ $tag->setDisplayName('Important');
+ $tag->setImapLabel('$important');
+ $message = new Message();
+ $message->setUid(1);
+
+ $this->client->method('status')
+ ->willThrowException(new Horde_Imap_Client_Exception('status failed'));
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $this->expectException(ServiceException::class);
+
+ $this->connector->tagMessages($this->account, $mailbox, $tag, true, $message);
+ }
+
+ public function testTagMessagesLogsOutClientWhenPermflagsNotSupported(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $tag = new Tag();
+ $tag->setDisplayName('Important');
+ $tag->setImapLabel('$important');
+ $message = new Message();
+ $message->setUid(1);
+
+ $this->client->method('status')
+ ->willReturn(['permflags' => []]);
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $result = $this->connector->tagMessages($this->account, $mailbox, $tag, true, $message);
+
+ self::assertSame([], $result);
+ }
+
+ public function testIsPermflagsEnabledLogsOutClientWhenStatusThrows(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+
+ $this->client->method('status')
+ ->willThrowException(new Horde_Imap_Client_Exception('status failed'));
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $this->expectException(ServiceException::class);
+
+ $this->connector->isPermflagsEnabled($this->account, $mailbox);
+ }
+
+ public function testIsPermflagsEnabledLogsOutClientOnSuccess(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+
+ $this->client->method('status')
+ ->willReturn(['permflags' => ['\\*']]);
+ $this->client->expects(self::once())
+ ->method('logout');
+
+ $result = $this->connector->isPermflagsEnabled($this->account, $mailbox);
+
+ self::assertTrue($result);
+ }
+}
diff --git a/tests/Unit/IMAP/MailboxSyncTest.php b/tests/Unit/IMAP/MailboxSyncTest.php
index 0eb77709df..a09156f8f3 100644
--- a/tests/Unit/IMAP/MailboxSyncTest.php
+++ b/tests/Unit/IMAP/MailboxSyncTest.php
@@ -23,9 +23,9 @@
use OCA\Mail\Events\MailboxesSynchronizedEvent;
use OCA\Mail\Folder;
use OCA\Mail\IMAP\FolderMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxStats;
use OCA\Mail\IMAP\MailboxSync;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IDBConnection;
@@ -42,8 +42,8 @@ class MailboxSyncTest extends TestCase {
/** @var MailAccountMapper|MockObject */
private $mailAccountMapper;
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
/** @var TimeFactory|MockObject */
private $timeFactory;
@@ -62,7 +62,7 @@ protected function setUp(): void {
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->folderMapper = $this->createMock(FolderMapper::class);
$this->mailAccountMapper = $this->createMock(MailAccountMapper::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->dbConnection = $this->createMock(IDBConnection::class);
@@ -71,7 +71,7 @@ protected function setUp(): void {
$this->mailboxMapper,
$this->folderMapper,
$this->mailAccountMapper,
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->timeFactory,
$this->dispatcher,
$this->dbConnection,
@@ -81,11 +81,11 @@ protected function setUp(): void {
public function testSyncSkipped() {
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
- $mailAccount->setLastMailboxSync(100000 - 2000);
+ $mailAccount->setLastMailboxSync(100000 - 100);
$account->method('getMailAccount')->willReturn($mailAccount);
$this->timeFactory->method('getTime')->willReturn(100000);
- $this->imapClientFactory->expects($this->never())
- ->method('getClient');
+ $this->protocolFactory->expects($this->never())
+ ->method('imapClient');
$this->dispatcher->expects($this->never())->method('dispatchTyped');
$this->sync->sync($account, new NullLogger());
@@ -98,8 +98,8 @@ public function testSync(): void {
$account->method('getMailAccount')->willReturn($mailAccount);
$this->timeFactory->method('getTime')->willReturn(100000);
$client = $this->createMock(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$client->expects($this->once())
@@ -156,8 +156,8 @@ public function testSyncShared(): void {
$account->method('getMailAccount')->willReturn($mailAccount);
$this->timeFactory->method('getTime')->willReturn(100000);
$client = $this->createMock(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$personal = new Horde_Imap_Client_Data_Namespace();
@@ -218,7 +218,7 @@ public function testSyncSharedNamespaceWithoutPrefix(): void {
$account->method('getMailAccount')->willReturn($mailAccount);
$this->timeFactory->method('getTime')->willReturn(100000);
$client = $this->createMock(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->method('getClient')
+ $this->protocolFactory->method('imapClient')
->with($account)
->willReturn($client);
$personal = new Horde_Imap_Client_Data_Namespace();
@@ -273,8 +273,8 @@ public function testSyncWithClient(): void {
->with($account)
->willReturn([]);
- $this->imapClientFactory->expects($this->never())
- ->method('getClient');
+ $this->protocolFactory->expects($this->never())
+ ->method('imapClient');
$client->expects($this->never())
->method('logout');
@@ -300,8 +300,8 @@ public function testSyncWithoutClient(): void {
->with($account)
->willReturn([]);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$client->expects($this->once())
diff --git a/tests/Unit/IMAP/PreviewEnhancerTest.php b/tests/Unit/IMAP/PreviewEnhancerTest.php
index 6a08b3deb7..02591ceaea 100644
--- a/tests/Unit/IMAP/PreviewEnhancerTest.php
+++ b/tests/Unit/IMAP/PreviewEnhancerTest.php
@@ -10,14 +10,13 @@
namespace Unit\IMAP;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Socket;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMapper;
use OCA\Mail\IMAP\PreviewEnhancer;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Avatar\Avatar;
use OCA\Mail\Service\AvatarService;
@@ -26,8 +25,8 @@
class PreviewEnhancerTest extends TestCase {
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
/** @var ImapMapper|MockObject */
private $imapMapper;
/** @var DbMapper|MockObject */
@@ -44,7 +43,7 @@ class PreviewEnhancerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->imapMapper = $this->createMock(ImapMapper::class);
$this->dbMapper = $this->createMock(DbMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -52,7 +51,7 @@ protected function setUp(): void {
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->previewEnhancer = new PreviewEnhancer(
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->imapMapper,
$this->dbMapper,
$this->logger,
@@ -74,16 +73,11 @@ public function testAvatars(): void {
$message2->setFrom(new AddressList([Address::fromRaw('Bob', 'bob@example.com')]));
$messages = [$message1, $message2];
$message2Avatar = new Avatar('example.com', 'image/png', true);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->with($account)
- ->willReturn($client);
$this->attachmentService->expects($this->exactly(2))
->method('getAttachmentNames')
->withConsecutive(
- [$account, $mailbox, $message1, $client],
- [$account, $mailbox, $message2, $client],
+ [$account, $mailbox, $message1],
+ [$account, $mailbox, $message2],
)
->willReturnOnConsecutiveCalls(
[], []
diff --git a/tests/Unit/JMAP/JmapMailboxAdapterTest.php b/tests/Unit/JMAP/JmapMailboxAdapterTest.php
new file mode 100644
index 0000000000..1e3b4ecb39
--- /dev/null
+++ b/tests/Unit/JMAP/JmapMailboxAdapterTest.php
@@ -0,0 +1,158 @@
+adapter = new JmapMailboxAdapter();
+ }
+
+ /**
+ * @param array $rights permission => granted; null means "no rights object"
+ * @param array $values
+ */
+ private function source(array $values = [], ?array $rights = null): MailboxParametersResponse&MockObject {
+ $source = $this->createMock(MailboxParametersResponse::class);
+
+ $defaults = [
+ 'id' => 'remote-1',
+ 'label' => 'Folder',
+ 'in' => null,
+ 'role' => null,
+ 'subscribed' => null,
+ 'objectsTotal' => null,
+ 'objectsUnseen' => null,
+ ];
+ foreach (array_merge($defaults, $values) as $method => $value) {
+ $source->method($method)->willReturn($value);
+ }
+
+ $source->method('rights')->willReturn($rights === null ? null : $this->rights($rights));
+
+ return $source;
+ }
+
+ /**
+ * @param array $granted
+ */
+ private function rights(array $granted): MailboxPermissions&MockObject {
+ $permissions = $this->createMock(MailboxPermissions::class);
+ foreach (['readItems', 'addItems', 'removeItems', 'setSeen', 'setKeywords', 'createChild', 'rename', 'delete', 'submit'] as $right) {
+ $permissions->method($right)->willReturn($granted[$right] ?? false);
+ }
+ return $permissions;
+ }
+
+ public function testMapsBasicMetadata(): void {
+ $source = $this->source([
+ 'id' => 'mbx-7',
+ 'label' => 'Receipts',
+ 'in' => 'parent-1',
+ 'objectsTotal' => 12,
+ 'objectsUnseen' => 3,
+ ]);
+
+ $mailbox = $this->adapter->convertToMailbox($source);
+
+ self::assertSame('Receipts', $mailbox->getName());
+ self::assertSame('mbx-7', $mailbox->getRemoteId());
+ self::assertSame('parent-1', $mailbox->getRemoteParentId());
+ self::assertSame(12, $mailbox->getMessages());
+ self::assertSame(3, $mailbox->getUnseen());
+ self::assertSame(md5('mbx-7'), $mailbox->getNameHash());
+ }
+
+ public function testNullIdDoesNotBreakNameHash(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source(['id' => null, 'label' => 'X']));
+
+ self::assertSame(md5(''), $mailbox->getNameHash());
+ }
+
+ public function testRoleIsMappedToSpecialUse(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source(['role' => 'sent'], ['readItems' => true]));
+
+ self::assertSame(['sent'], json_decode($mailbox->getSpecialUse(), true));
+ }
+
+ public function testFlaggedRoleMapsToImportantSpecialUse(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source(['role' => 'flagged'], ['readItems' => true]));
+
+ self::assertSame(['flagged'], json_decode($mailbox->getSpecialUse(), true));
+ }
+
+ public function testReadableMailboxIsSelectable(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], ['readItems' => true]));
+
+ self::assertTrue($mailbox->getSelectable());
+ self::assertStringNotContainsString('\\noselect', $mailbox->getAttributes());
+ }
+
+ public function testUnreadableMailboxIsNotSelectableAndNoselect(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], ['readItems' => false]));
+
+ self::assertFalse($mailbox->getSelectable());
+ self::assertStringContainsString('\\noselect', $mailbox->getAttributes());
+ }
+
+ public function testAclStringIsBuiltFromRights(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], [
+ 'readItems' => true,
+ 'addItems' => true,
+ 'setSeen' => true,
+ ]));
+
+ $acls = $mailbox->getMyAcls();
+ self::assertStringContainsString('l', $acls);
+ self::assertStringContainsString('r', $acls);
+ self::assertStringContainsString('i', $acls);
+ self::assertStringContainsString('s', $acls);
+ }
+
+ public function testFullRightsGrantAdministerFlag(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], [
+ 'readItems' => true,
+ 'createChild' => true,
+ 'rename' => true,
+ 'delete' => true,
+ ]));
+
+ self::assertStringContainsString('a', $mailbox->getMyAcls());
+ }
+
+ public function testNoRightsObjectYieldsNullAcl(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], null));
+
+ self::assertNull($mailbox->getMyAcls());
+ }
+
+ public function testNoGrantedRightsYieldsNullAcl(): void {
+ $mailbox = $this->adapter->convertToMailbox($this->source([], []));
+
+ self::assertNull($mailbox->getMyAcls());
+ }
+
+ public function testSubscribedAttributeReflectsResponse(): void {
+ $subscribed = $this->adapter->convertToMailbox($this->source(['subscribed' => true], ['readItems' => true]));
+ $unsubscribed = $this->adapter->convertToMailbox($this->source(['subscribed' => false], ['readItems' => true]));
+
+ self::assertStringContainsString('\\subscribed', $subscribed->getAttributes());
+ self::assertStringNotContainsString('\\subscribed', $unsubscribed->getAttributes());
+ }
+}
diff --git a/tests/Unit/JMAP/JmapMailboxConnectorTest.php b/tests/Unit/JMAP/JmapMailboxConnectorTest.php
new file mode 100644
index 0000000000..636c06aa02
--- /dev/null
+++ b/tests/Unit/JMAP/JmapMailboxConnectorTest.php
@@ -0,0 +1,112 @@
+jmapOperationsService = $this->createMock(JmapOperationsService::class);
+ $this->mailboxMapper = $this->createMock(MailboxMapper::class);
+ $this->account = $this->createMock(Account::class);
+ $this->account->method('getId')->willReturn(42);
+
+ $this->connector = new JmapMailboxConnector(
+ $this->jmapOperationsService,
+ $this->mailboxMapper,
+ $this->createMock(ITimeFactory::class),
+ $this->createMock(IEventDispatcher::class),
+ $this->createMock(LoggerInterface::class),
+ );
+ }
+
+ public function testCreateFindsParentByNameHashAndHashesFullPath(): void {
+ $parent = new Mailbox();
+ $parent->setRemoteId('parent-id');
+
+ $this->jmapOperationsService->expects(self::once())
+ ->method('connect')
+ ->with($this->account)
+ ->willReturn(true);
+ $this->mailboxMapper->expects(self::once())
+ ->method('find')
+ ->with($this->account, 'Parent')
+ ->willReturn($parent);
+ $this->jmapOperationsService->expects(self::once())
+ ->method('collectionCreate')
+ ->with($parent, self::callback(static function (Mailbox $mailbox): bool {
+ return $mailbox->getName() === 'Child';
+ }))
+ ->willReturnCallback(static function (?Mailbox $location, Mailbox $mailbox): Mailbox {
+ self::assertSame('parent-id', $location?->getRemoteId());
+ $mailbox->setRemoteId('child-id');
+ $mailbox->setNameHash(md5('child-id'));
+ return $mailbox;
+ });
+ $this->mailboxMapper->expects(self::once())
+ ->method('insert')
+ ->with(self::callback(static function (Mailbox $mailbox): bool {
+ return $mailbox->getName() === 'Parent/Child'
+ && $mailbox->getNameHash() === md5('Parent/Child');
+ }))
+ ->willReturnArgument(0);
+
+ $mailbox = $this->connector->create($this->account, 'Parent/Child');
+
+ self::assertSame('Parent/Child', $mailbox->getName());
+ self::assertSame(md5('Parent/Child'), $mailbox->getNameHash());
+ }
+
+ public function testRenameHashesFullPath(): void {
+ $mailbox = new Mailbox();
+ $mailbox->setRemoteId('mailbox-id');
+ $mailbox->setName('Parent/Before');
+ $mailbox->setNameHash(md5('Parent/Before'));
+
+ $this->jmapOperationsService->expects(self::once())
+ ->method('connect')
+ ->with($this->account)
+ ->willReturn(true);
+ $this->jmapOperationsService->expects(self::once())
+ ->method('collectionModify')
+ ->with('mailbox-id', self::callback(static function (Mailbox $mailbox): bool {
+ return $mailbox->getName() === 'After';
+ }), ['name'])
+ ->willReturnArgument(1);
+ $this->mailboxMapper->expects(self::once())
+ ->method('update')
+ ->with(self::callback(static function (Mailbox $mailbox): bool {
+ return $mailbox->getName() === 'Parent/After'
+ && $mailbox->getNameHash() === md5('Parent/After');
+ }))
+ ->willReturnArgument(0);
+
+ $renamed = $this->connector->rename($this->account, $mailbox, 'Parent/After');
+
+ self::assertSame('Parent/After', $renamed->getName());
+ self::assertSame(md5('Parent/After'), $renamed->getNameHash());
+ }
+}
diff --git a/tests/Unit/JMAP/JmapMessageAdapterTest.php b/tests/Unit/JMAP/JmapMessageAdapterTest.php
new file mode 100644
index 0000000000..5e1c991342
--- /dev/null
+++ b/tests/Unit/JMAP/JmapMessageAdapterTest.php
@@ -0,0 +1,183 @@
+adapter = new JmapMessageAdapter($this->createMock(Html::class));
+ }
+
+ /**
+ * Build a fully stubbed JMAP message response. Every accessor returns a
+ * harmless default so a test only has to override what it cares about.
+ *
+ * @param array $values
+ * @param array $keywords keyword => present
+ */
+ private function source(array $values = [], array $keywords = []): MailParametersResponse&MockObject {
+ $source = $this->createMock(MailParametersResponse::class);
+
+ $defaults = [
+ 'id' => 'remote-1',
+ 'messageId' => null,
+ 'inReplyTo' => null,
+ 'references' => null,
+ 'thread' => null,
+ 'subject' => null,
+ 'sent' => null,
+ 'received' => null,
+ 'answered' => null,
+ 'draft' => null,
+ 'flagged' => null,
+ 'seen' => null,
+ 'forwarded' => null,
+ 'junk' => null,
+ 'notjunk' => null,
+ 'bodyTextPreview' => null,
+ 'hasAttachment' => null,
+ 'from' => null,
+ 'sender' => [],
+ 'to' => null,
+ 'cc' => null,
+ 'bcc' => null,
+ ];
+ foreach (array_merge($defaults, $values) as $method => $value) {
+ $source->method($method)->willReturn($value);
+ }
+
+ $source->method('keywords')->willReturn($keywords);
+ $source->method('parameter')->willReturn($values['__updatedAt'] ?? null);
+ $source->method('keyword')->willReturnCallback(
+ static fn (string $name): ?bool => $keywords[$name] ?? null,
+ );
+
+ return $source;
+ }
+
+ public function testMapsFlags(): void {
+ $source = $this->source([
+ 'answered' => true,
+ 'draft' => true,
+ 'flagged' => true,
+ 'seen' => true,
+ 'forwarded' => true,
+ 'junk' => true,
+ 'notjunk' => true,
+ 'hasAttachment' => true,
+ ], [
+ '$deleted' => true,
+ '$mdnsent' => true,
+ Tag::LABEL_IMPORTANT => true,
+ ]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ self::assertTrue($message->getFlagAnswered());
+ self::assertTrue($message->getFlagDraft());
+ self::assertTrue($message->getFlagFlagged());
+ self::assertTrue($message->getFlagSeen());
+ self::assertTrue($message->getFlagForwarded());
+ self::assertTrue($message->getFlagJunk());
+ self::assertTrue($message->getFlagNotjunk());
+ self::assertTrue($message->getFlagAttachments());
+ self::assertTrue($message->getFlagDeleted());
+ self::assertTrue($message->getFlagMdnsent());
+ self::assertTrue($message->getFlagImportant());
+ }
+
+ public function testUnsetFlagsDefaultToFalseNotNull(): void {
+ $message = $this->adapter->convertToDatabaseMessage($this->source());
+
+ self::assertFalse($message->getFlagAnswered());
+ self::assertFalse($message->getFlagSeen());
+ self::assertFalse($message->getFlagImportant());
+ self::assertFalse($message->getFlagAttachments());
+ }
+
+ public function testImportantViaLegacyLabel(): void {
+ $source = $this->source([], ['$label1' => true]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ self::assertTrue($message->getFlagImportant());
+ }
+
+ public function testReferencesArrayIsJsonEncoded(): void {
+ $source = $this->source(['references' => ['', '', '']]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ self::assertSame('["",""]', $message->getReferences());
+ }
+
+ public function testReferencesNullStaysNull(): void {
+ $message = $this->adapter->convertToDatabaseMessage($this->source(['references' => null]));
+
+ self::assertNull($message->getReferences());
+ }
+
+ public function testSubjectDefaultsToEmptyString(): void {
+ $message = $this->adapter->convertToDatabaseMessage($this->source(['subject' => null]));
+
+ self::assertSame('', $message->getSubject());
+ }
+
+ public function testConvertsCustomKeywordsToTagsAndSkipsReserved(): void {
+ $source = $this->source([], [
+ '$seen' => true,
+ '$flagged' => true,
+ 'work' => true,
+ 'urgent' => true,
+ 'ignored' => false,
+ ]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ $labels = array_map(static fn (Tag $tag): string => $tag->getImapLabel(), $message->getTags());
+ sort($labels);
+ self::assertSame(['urgent', 'work'], $labels);
+ }
+
+ public function testMapsFromAddress(): void {
+ $source = $this->source([
+ 'from' => [['email' => 'alice@example.com', 'name' => 'Alice']],
+ ]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ $from = $message->getFrom()->first();
+ self::assertNotNull($from);
+ self::assertSame('alice@example.com', $from->getEmail());
+ }
+
+ public function testFallsBackToSenderWhenFromMissing(): void {
+ $source = $this->source([
+ 'from' => null,
+ 'sender' => [['email' => 'sender@example.com', 'name' => 'Sender']],
+ ]);
+
+ $message = $this->adapter->convertToDatabaseMessage($source);
+
+ $from = $message->getFrom()->first();
+ self::assertNotNull($from);
+ self::assertSame('sender@example.com', $from->getEmail());
+ }
+}
diff --git a/tests/Unit/Job/FollowUpClassifierJobTest.php b/tests/Unit/Job/FollowUpClassifierJobTest.php
index 4f5de430dc..0731e228a5 100644
--- a/tests/Unit/Job/FollowUpClassifierJobTest.php
+++ b/tests/Unit/Job/FollowUpClassifierJobTest.php
@@ -12,7 +12,6 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\BackgroundJob\FollowUpClassifierJob;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
@@ -21,6 +20,7 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Utility\ITimeFactory;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -38,7 +38,7 @@ class FollowUpClassifierJobTest extends TestCase {
/** @var AccountService|MockObject */
private $accountService;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var AiIntegrationsService|MockObject */
@@ -53,7 +53,7 @@ protected function setUp(): void {
$this->time = $this->createMock(ITimeFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->accountService = $this->createMock(AccountService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->aiService = $this->createMock(AiIntegrationsService::class);
$this->threadMapper = $this->createMock(ThreadMapper::class);
@@ -115,8 +115,8 @@ public function testRun(): void {
->with('Follow up', '#d77000', 'user')
->willReturn($tag);
$this->mailManager->expects(self::once())
- ->method('tagMessage')
- ->with($account, 'sent', $message, $tag, true);
+ ->method('tagMessages')
+ ->with($account, $mailbox, $tag, true, $message);
$this->job->run($argument);
}
@@ -150,7 +150,7 @@ public function testRunLlmProcessingDisabled(): void {
$this->mailManager->expects(self::never())
->method('createTag');
$this->mailManager->expects(self::never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->job->run($argument);
}
@@ -192,7 +192,7 @@ public function testRunNoMessages(): void {
$this->mailManager->expects(self::never())
->method('createTag');
$this->mailManager->expects(self::never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->job->run($argument);
}
@@ -248,8 +248,8 @@ public function testRunMultipleMessages(): void {
->with('Follow up', '#d77000', 'user')
->willReturn($tag);
$this->mailManager->expects(self::once())
- ->method('tagMessage')
- ->with($account, 'sent', $message, $tag, true);
+ ->method('tagMessages')
+ ->with($account, $mailbox, $tag, true, $message);
$this->job->run($argument);
}
@@ -302,8 +302,8 @@ public function testRunCreateTag(): void {
->with('Follow up', '#d77000', 'user')
->willReturn($tag);
$this->mailManager->expects(self::once())
- ->method('tagMessage')
- ->with($account, 'sent', $message, $tag, true);
+ ->method('tagMessages')
+ ->with($account, $mailbox, $tag, true, $message);
$this->job->run($argument);
}
@@ -354,7 +354,7 @@ public function testRunNoFollowUp(): void {
$this->mailManager->expects(self::never())
->method('createTag');
$this->mailManager->expects(self::never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->job->run($argument);
}
@@ -403,7 +403,7 @@ public function testRunFollowedUp(): void {
$this->mailManager->expects(self::never())
->method('createTag');
$this->mailManager->expects(self::never())
- ->method('tagMessage');
+ ->method('tagMessages');
$this->job->run($argument);
}
@@ -453,7 +453,7 @@ public function testRunServiceFailure(): void {
->method('error')
->with('Failed to classify message for follow-up: AI task processing failed', ['exception' => $exception]);
$this->mailManager->expects(self::never())->method('createTag');
- $this->mailManager->expects(self::never())->method('tagMessage');
+ $this->mailManager->expects(self::never())->method('tagMessages');
$this->job->run($argument);
}
diff --git a/tests/Unit/Job/TrashRetentionJobTest.php b/tests/Unit/Job/TrashRetentionJobTest.php
index 8cc52d6089..d6c837fd5b 100644
--- a/tests/Unit/Job/TrashRetentionJobTest.php
+++ b/tests/Unit/Job/TrashRetentionJobTest.php
@@ -10,10 +10,8 @@
namespace OCA\Mail\Tests\Unit\Job;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\BackgroundJob\TrashRetentionJob;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\Mailbox;
@@ -21,7 +19,7 @@
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Db\MessageRetentionMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\SyncService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -38,9 +36,6 @@ class TrashRetentionJobTest extends TestCase {
/** @var LoggerInterface|MockObject */
private $logger;
- /** @var IMAPClientFactory|MockObject */
- private $clientFactory;
-
/** @var MessageMapper|MockObject */
private $messageMapper;
@@ -53,7 +48,7 @@ class TrashRetentionJobTest extends TestCase {
/** @var MailboxMapper|MockObject */
private $mailboxMapper;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var SyncService|MockObject */
@@ -66,24 +61,21 @@ protected function setUp(): void {
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->messageRetentionMapper = $this->createMock(MessageRetentionMapper::class);
$this->accountMapper = $this->createMock(MailAccountMapper::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->syncService = $this->createMock(SyncService::class);
$this->job = new TrashRetentionJob(
$this->timeFactory,
$this->logger,
- $this->clientFactory,
$this->messageMapper,
$this->messageRetentionMapper,
$this->accountMapper,
$this->mailboxMapper,
$this->mailManager,
- $this->syncService,
);
}
@@ -97,7 +89,6 @@ public function testRun() {
$message = new Message();
$message->setMailboxId(123);
$message->setUid(420);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
$this->accountMapper->expects($this->once())
->method('getAllAccounts')
@@ -115,14 +106,9 @@ public function testRun() {
->method('findMessagesKnownSinceBefore')
->with(42, 1000000 - 24 * 60 * 3600)
->willReturn([$message]);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
$this->mailManager->expects($this->once())
- ->method('deleteMessageWithClient')
- ->with($account, $trash, 420, $client);
- $client->expects($this->once())
- ->method('logout');
+ ->method('deleteMessage')
+ ->with($account, $trash, $message);
$this->job->run(self::ARGUMENT);
}
@@ -137,7 +123,7 @@ public function testRunWithoutRetention() {
$this->syncService->expects($this->never())
->method('syncMailbox');
$this->mailManager->expects($this->never())
- ->method('deleteMessageWithClient');
+ ->method('deleteMessage');
$this->job->run(self::ARGUMENT);
}
@@ -152,7 +138,7 @@ public function testRunWith0DaysRetention() {
$this->syncService->expects($this->never())
->method('syncMailbox');
$this->mailManager->expects($this->never())
- ->method('deleteMessageWithClient');
+ ->method('deleteMessage');
$this->job->run(self::ARGUMENT);
}
@@ -167,7 +153,7 @@ public function testRunWithNegativeRetention() {
$this->syncService->expects($this->never())
->method('syncMailbox');
$this->mailManager->expects($this->never())
- ->method('deleteMessageWithClient');
+ ->method('deleteMessage');
$this->job->run(self::ARGUMENT);
}
@@ -185,7 +171,7 @@ public function testRunWithoutTrash() {
$this->syncService->expects($this->never())
->method('syncMailbox');
$this->mailManager->expects($this->never())
- ->method('deleteMessageWithClient');
+ ->method('deleteMessage');
$this->job->run(self::ARGUMENT);
}
@@ -205,7 +191,7 @@ public function testRunWithNonExistingTrash() {
$this->syncService->expects($this->never())
->method('syncMailbox');
$this->mailManager->expects($this->never())
- ->method('deleteMessageWithClient');
+ ->method('deleteMessage');
$this->job->run(self::ARGUMENT);
}
diff --git a/tests/Unit/Listener/DeleteDraftListenerTest.php b/tests/Unit/Listener/DeleteDraftListenerTest.php
index f98e6aeeba..7f205423b4 100644
--- a/tests/Unit/Listener/DeleteDraftListenerTest.php
+++ b/tests/Unit/Listener/DeleteDraftListenerTest.php
@@ -16,10 +16,10 @@
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Events\DraftSavedEvent;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Listener\DeleteDraftListener;
use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
@@ -28,8 +28,8 @@
use Psr\Log\LoggerInterface;
class DeleteDraftListenerTest extends TestCase {
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
/** @var MailboxMapper|MockObject */
private $mailboxMapper;
@@ -49,14 +49,14 @@ class DeleteDraftListenerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->listener = new DeleteDraftListener(
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->mailboxMapper,
$this->messageMapper,
$this->logger,
@@ -109,8 +109,8 @@ public function testHandleDraftSavedEventNoDraftMailboxSet(): void {
);
/** @var \Horde_Imap_Client_Socket|MockObject $client */
$client = $this->createStub(\Horde_Imap_Client_Socket::class);
- $this->imapClientFactory
- ->method('getClient')
+ $this->protocolFactory
+ ->method('imapClient')
->with($account)
->willReturn($client);
$mailbox = new Mailbox();
@@ -140,8 +140,8 @@ public function testHandleDraftSavedEventDraftMailboxNotFound(): void {
);
/** @var \Horde_Imap_Client_Socket|MockObject $client */
$client = $this->createStub(\Horde_Imap_Client_Socket::class);
- $this->imapClientFactory
- ->method('getClient')
+ $this->protocolFactory
+ ->method('imapClient')
->with($account)
->willReturn($client);
$mailbox = new Mailbox();
diff --git a/tests/Unit/Listener/MoveJunkListenerTest.php b/tests/Unit/Listener/MoveJunkListenerTest.php
index 32cf72018f..557d91de54 100644
--- a/tests/Unit/Listener/MoveJunkListenerTest.php
+++ b/tests/Unit/Listener/MoveJunkListenerTest.php
@@ -11,30 +11,41 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Events\MessageFlaggedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Listener\MoveJunkListener;
+use OCA\Mail\Service\MailManager;
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
class MoveJunkListenerTest extends TestCase {
- private IMailManager $mailManager;
+ private MailManager $mailManager;
+ private MailboxMapper $mailboxMapper;
private LoggerInterface $logger;
private MoveJunkListener $listener;
protected function setUp(): void {
parent::setUp();
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
+ $this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->logger = new TestLogger();
+ $this->mailManager->method('getMessageIdForUid')
+ ->willReturn(1);
+ $this->mailManager->method('getMessage')
+ ->willReturn(new Message());
+ $this->mailboxMapper->method('findSpecialUseMailbox')
+ ->willReturn(new Mailbox());
+
$this->listener = new MoveJunkListener(
$this->mailManager,
+ $this->mailboxMapper,
$this->logger
);
}
diff --git a/tests/Unit/Send/ChainTest.php b/tests/Unit/Send/ChainTest.php
index c580ff4997..233a270ec5 100644
--- a/tests/Unit/Send/ChainTest.php
+++ b/tests/Unit/Send/ChainTest.php
@@ -15,7 +15,7 @@
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MessageMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\Chain;
use OCA\Mail\Send\CopySentMessageHandler;
@@ -35,7 +35,7 @@ class ChainTest extends TestCase {
private MockObject|MessageMapper $messageMapper;
private AttachmentService|MockObject $attachmentService;
private MockObject|LocalMessageMapper $localMessageMapper;
- private MockObject&IMAPClientFactory $clientFactory;
+ private MockObject&ProtocolFactory $protocolFactory;
protected function setUp(): void {
$this->sentMailboxHandler = $this->createMock(SentMailboxHandler::class);
@@ -45,7 +45,7 @@ protected function setUp(): void {
$this->flagRepliedMessageHandler = $this->createMock(FlagRepliedMessageHandler::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->localMessageMapper = $this->createMock(LocalMessageMapper::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->chain = new Chain($this->sentMailboxHandler,
$this->antiAbuseHandler,
$this->sendHandler,
@@ -53,7 +53,7 @@ protected function setUp(): void {
$this->flagRepliedMessageHandler,
$this->attachmentService,
$this->localMessageMapper,
- $this->clientFactory,
+ $this->protocolFactory,
);
}
@@ -74,8 +74,8 @@ public function testProcess(): void {
$this->sentMailboxHandler->expects(self::once())
->method('setNext');
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->willReturn($client);
$this->sentMailboxHandler->expects(self::once())
->method('process')
@@ -110,8 +110,8 @@ public function testProcessNotProcessed() {
$this->sentMailboxHandler->expects(self::once())
->method('setNext');
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->willReturn($client);
$this->sentMailboxHandler->expects(self::once())
->method('process')
diff --git a/tests/Unit/Service/AccountServiceTest.php b/tests/Unit/Service/AccountServiceTest.php
index 1461b14ac8..fa840fc4bd 100644
--- a/tests/Unit/Service/AccountServiceTest.php
+++ b/tests/Unit/Service/AccountServiceTest.php
@@ -17,7 +17,7 @@
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Exception\ClientException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AliasesService;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -54,8 +54,8 @@ class AccountServiceTest extends TestCase {
/** @var IJobList|MockObject */
private $jobList;
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
/** @var Horde_Imap_Client_Socket|MockObject */
private $client;
@@ -71,7 +71,7 @@ protected function setUp(): void {
$this->l10n = $this->createMock(IL10N::class);
$this->aliasesService = $this->createMock(AliasesService::class);
$this->jobList = $this->createMock(IJobList::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->config = $this->createMock(IConfig::class);
$this->time = $this->createMock(ITimeFactory::class);
$this->delegationMapper = $this->createMock(DelegationMapper::class);
@@ -79,7 +79,7 @@ protected function setUp(): void {
$this->mapper,
$this->aliasesService,
$this->jobList,
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->config,
$this->time,
$this->delegationMapper,
@@ -211,8 +211,8 @@ public function testUpdateSignature() {
}
public function testAccountsFailedConnection() {
$accountId = 1;
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('testConnection')
->willThrowException(new ClientException());
$this->mapper->expects($this->once())
->method('find')
@@ -223,12 +223,8 @@ public function testAccountsFailedConnection() {
}
public function testAccountsSuccesfulConnection() {
$accountId = 1;
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($this->client);
- $this->client->expects($this->once())
- ->method('close')
- ->willReturn(null);
+ $this->protocolFactory->expects($this->once())
+ ->method('testConnection');
$this->mapper->expects($this->once())
->method('find')
->with($this->user, $accountId)
diff --git a/tests/Unit/Service/AiIntegrationsServiceTest.php b/tests/Unit/Service/AiIntegrationsServiceTest.php
index d2c4c86e47..9d626cd531 100644
--- a/tests/Unit/Service/AiIntegrationsServiceTest.php
+++ b/tests/Unit/Service/AiIntegrationsServiceTest.php
@@ -10,22 +10,20 @@
namespace OCA\Mail\Tests\Unit\Service;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\ConfigLexicon;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
use OCA\Mail\Service\AiIntegrations\Cache;
use OCA\Mail\Service\AiIntegrations\DefaultPrompts;
+use OCA\Mail\Service\MailManager;
use OCP\IAppConfig;
use OCP\IUser;
use OCP\IUserManager;
@@ -45,8 +43,7 @@ class AiIntegrationsServiceTest extends TestCase {
private NullLogger|MockObject $logger;
private AiIntegrationsService $aiIntegrationsService;
private Cache|MockObject $cache;
- private IMAPClientFactory|MockObject $clientFactory;
- private IMailManager|MockObject $mailManager;
+ private MailManager|MockObject $mailManager;
private TaskProcessingManager|MockObject $taskProcessingManager;
private TaskProcessingProvider|MockObject $taskProcessingProvider;
private IFactory|MockObject $l10nFactory;
@@ -58,15 +55,13 @@ protected function setUp(): void {
$this->logger = $this->createMock(NullLogger::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->cache = $this->createMock(Cache::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->taskProcessingManager = $this->createMock(TaskProcessingManager::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->aiIntegrationsService = new AiIntegrationsService(
$this->logger,
$this->cache,
- $this->clientFactory,
$this->mailManager,
$this->taskProcessingManager,
$this->l10nFactory,
@@ -91,10 +86,6 @@ public function testTaskProcessingExceptionIsMappedToServiceException(): void {
->method('getAvailableTaskTypes')
->willReturn([TextToTextSummary::ID => $this->taskProcessingProvider]);
$this->cache->method('getValue')->willReturn(false);
- $this->clientFactory
- ->method('getClient')
- ->with($account)
- ->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->taskProcessingManager
->method('runTask')
->willThrowException($taskProcessingException);
@@ -131,7 +122,6 @@ public function testSmartReply(): void {
->method('getAvailableTaskTypes')
->willReturn([TextToText::ID => $this->taskProcessingProvider]);
$this->cache->method('getValue')->willReturn(false);
- $this->clientFactory->method('getClient')->with($account)->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
@@ -182,7 +172,6 @@ public function testSmartReplyMarkdownFormat(): void {
->willReturnCallback(function (string $_key, ?string $value) use (&$cachedReplies): void {
$cachedReplies = $value;
});
- $this->clientFactory->method('getClient')->with($account)->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
@@ -229,7 +218,6 @@ public function testSmartReplyInvalidStructure(string $output): void {
->method('getAvailableTaskTypes')
->willReturn([TextToText::ID => $this->taskProcessingProvider]);
$this->cache->method('getValue')->willReturn(false);
- $this->clientFactory->method('getClient')->with($account)->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
@@ -495,8 +483,6 @@ public function testSummarizeMessagesContainsSummary(): void {
$this->taskProcessingManager->expects(self::once())
->method('getAvailableTaskTypes')
->willReturn([TextToText::ID => $this->taskProcessingProvider]);
- $this->clientFactory->expects(self::once())
- ->method('getClient');
$this->aiIntegrationsService->summarizeMessages($account, [$message]);
}
@@ -531,9 +517,6 @@ public function testSummarizeMessagesIsEncrypted(): void {
->method('getUserLanguage')
->with($user)
->willReturn('en');
-
- $imapClient = $this->clientFactory->getClient($account);
-
$imapMessage = $this->createMock(IMAPMessage::class);
$imapMessage->expects(self::never())
->method('getPlainBody')
@@ -547,9 +530,6 @@ public function testSummarizeMessagesIsEncrypted(): void {
$this->taskProcessingManager->expects(self::never())
->method('scheduleTask');
- $this->clientFactory->expects(self::once())
- ->method('getClient');
-
$this->mailManager->expects(self::once())
->method('getMailbox')
->with(
@@ -560,10 +540,9 @@ public function testSummarizeMessagesIsEncrypted(): void {
$this->mailManager->expects(self::once())
->method('getImapMessage')
->with(
- $imapClient,
$account,
$mailBox,
- $message->getUid(),
+ $message,
true
)
->willReturn($imapMessage);
@@ -602,8 +581,6 @@ public function testSummarizeMessages(): void {
->with($user)
->willReturn('de_DE');
- $imapClient = $this->clientFactory->getClient($account);
-
$imapMessage = $this->createMock(IMAPMessage::class);
$imapMessage->expects(self::atMost(2))
->method('getPlainBody')
@@ -625,9 +602,6 @@ public function testSummarizeMessages(): void {
);
});
- $this->clientFactory->expects(self::once())
- ->method('getClient');
-
$this->mailManager->expects(self::once())
->method('getMailbox')
->with(
@@ -638,10 +612,9 @@ public function testSummarizeMessages(): void {
$this->mailManager->expects(self::once())
->method('getImapMessage')
->with(
- $imapClient,
$account,
$mailBox,
- $message->getUid(),
+ $message,
true
)
->willReturn($imapMessage);
@@ -703,7 +676,6 @@ public function testRequiresFollowUp(): void {
$this->taskProcessingManager
->method('getAvailableTaskTypes')
->willReturn([TextToText::ID => $this->taskProcessingProvider]);
- $this->clientFactory->method('getClient')->with($account)->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
@@ -746,7 +718,6 @@ public function testRequiresFollowUpRejectsUnusableOutput(?string $output, int $
$this->taskProcessingManager
->method('getAvailableTaskTypes')
->willReturn([TextToText::ID => $this->taskProcessingProvider]);
- $this->clientFactory->method('getClient')->with($account)->willReturn($this->createMock(Horde_Imap_Client_Socket::class));
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
diff --git a/tests/Unit/Service/AntiSpamServiceTest.php b/tests/Unit/Service/AntiSpamServiceTest.php
index b2ea8533f5..152abbd129 100644
--- a/tests/Unit/Service/AntiSpamServiceTest.php
+++ b/tests/Unit/Service/AntiSpamServiceTest.php
@@ -18,9 +18,9 @@
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\Events\MessageFlaggedEvent;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AntiSpamService;
use OCA\Mail\Service\MailManager;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -32,7 +32,7 @@ class AntiSpamServiceTest extends TestCase {
private AntiSpamService $service;
private IAppConfig|MockObject $appConfig;
private DbMessageMapper|MockObject $dbMessageMapper;
- private IMAPClientFactory|MockObject $imapClientFactory;
+ private ProtocolFactory|MockObject $protocolFactory;
private SmtpClientFactory|MockObject $smtpClientFactory;
private MockObject|ImapMessageMapper $imapMessageMapper;
private LoggerInterface|MockObject $logger;
@@ -46,7 +46,7 @@ protected function setUp(): void {
$this->dbMessageMapper = $this->createMock(DbMessageMapper::class);
$this->transmission = $this->createMock(IMailTransmission::class);
$this->mailManager = $this->createMock(MailManager::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->smtpClientFactory = $this->createMock(SmtpClientFactory::class);
$this->imapMessageMapper = $this->createMock(ImapMessageMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -54,7 +54,7 @@ protected function setUp(): void {
$this->service = new AntiSpamService(
$this->dbMessageMapper,
$this->mailManager,
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->smtpClientFactory,
$this->imapMessageMapper,
$this->logger,
@@ -174,8 +174,8 @@ public function testSendReportEmail(): void {
$this->mailManager->expects(self::exactly(2))
->method('getMailbox')
->willReturn($mailbox);
- $this->imapClientFactory->expects(self::exactly(2))
- ->method('getClient')
+ $this->protocolFactory->expects(self::exactly(2))
+ ->method('imapClient')
->willReturn($client);
$client->expects(self::exactly(2))
->method('logout');
@@ -240,8 +240,8 @@ public function testSendReportEmailNoSentCopy(): void {
$this->mailManager->expects(self::exactly(2))
->method('getMailbox')
->willReturn($mailbox);
- $this->imapClientFactory->expects(self::exactly(2))
- ->method('getClient')
+ $this->protocolFactory->expects(self::exactly(2))
+ ->method('imapClient')
->willReturn($client);
$client->expects(self::exactly(2))
->method('logout');
diff --git a/tests/Unit/Service/Attachment/AttachmentServiceTest.php b/tests/Unit/Service/Attachment/AttachmentServiceTest.php
index 5c5a5e5a8a..960bb9eca6 100644
--- a/tests/Unit/Service/Attachment/AttachmentServiceTest.php
+++ b/tests/Unit/Service/Attachment/AttachmentServiceTest.php
@@ -14,7 +14,6 @@
use OC\Files\Node\File;
use OCA\Files_Sharing\SharedStorage;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalAttachmentMapper;
use OCA\Mail\Db\LocalMessage;
@@ -28,6 +27,7 @@
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Attachment\AttachmentStorage;
use OCA\Mail\Service\Attachment\UploadedFile;
+use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\Folder;
@@ -44,7 +44,7 @@
class AttachmentServiceTest extends TestCase {
private LocalAttachmentMapper&MockObject $mapper;
private AttachmentStorage&MockObject $storage;
- private IMailManager&MockObject $mailManager;
+ private MailManager&MockObject $mailManager;
private MessageMapper&MockObject $messageMapper;
private Folder&MockObject $userFolder;
private ICache&MockObject $cache;
@@ -60,7 +60,7 @@ protected function setUp(): void {
$this->mapper = $this->createMock(LocalAttachmentMapper::class);
$this->storage = $this->createMock(AttachmentStorage::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->userFolder = $this->createMock(Folder::class);
$this->cache = $this->createMock(ICache::class);
@@ -632,13 +632,12 @@ public function testGetAttachmentNamesCacheHit(): void {
$mailbox->setId(2);
$message = new Message();
$message->setUid(3);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$cached = [['id' => '1.2', 'fileName' => 'file.pdf', 'mime' => 'application/pdf', 'downloadUrl' => 'http://example.test/dl', 'mimeUrl' => 'http://example.test/mime']];
$this->cache->expects(self::once())->method('get')->willReturn($cached);
$this->mailManager->expects(self::never())->method('getImapMessage');
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame($cached, $result);
@@ -653,12 +652,11 @@ public function testGetAttachmentNamesEarlyExitNoAttachments(): void {
$message->setUid(3);
$message->setStructureAnalyzed(true);
$message->setFlagAttachments(false);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->cache->expects(self::never())->method('get');
$this->mailManager->expects(self::never())->method('getImapMessage');
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame([], $result);
@@ -671,12 +669,11 @@ public function testGetAttachmentNamesCacheHitEmptyArray(): void {
$mailbox->setId(2);
$message = new Message();
$message->setUid(3);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->cache->expects(self::once())->method('get')->willReturn([]);
$this->mailManager->expects(self::never())->method('getImapMessage');
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame([], $result);
@@ -690,12 +687,11 @@ public function testGetAttachmentNamesCacheMissWithAttachments(): void {
$message = new Message();
$message->setUid(3);
$message->setId(99);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$imapMessage = $this->createMock(IMAPMessage::class);
$this->cache->expects(self::once())->method('get')->willReturn(null);
$this->mailManager->expects(self::once())
->method('getImapMessage')
- ->with($client, $account, $mailbox, 3, true)
+ ->with($account, $mailbox, $message, true)
->willReturn($imapMessage);
$imapMessage->expects(self::once())
->method('getAttachments')
@@ -705,7 +701,7 @@ public function testGetAttachmentNamesCacheMissWithAttachments(): void {
$this->cache->expects(self::once())->method('set');
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertCount(1, $result);
@@ -720,7 +716,6 @@ public function testGetAttachmentNamesCacheMissNoAttachments(): void {
$mailbox->setId(2);
$message = new Message();
$message->setUid(3);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$imapMessage = $this->createMock(IMAPMessage::class);
$this->cache->expects(self::once())->method('get')->willReturn(null);
$this->mailManager->expects(self::once())->method('getImapMessage')->willReturn($imapMessage);
@@ -728,7 +723,7 @@ public function testGetAttachmentNamesCacheMissNoAttachments(): void {
$this->cache->expects(self::once())->method('set')->with(self::anything(), []);
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame([], $result);
@@ -741,7 +736,6 @@ public function testGetAttachmentNamesSmimeDecryptException(): void {
$mailbox->setId(2);
$message = new Message();
$message->setUid(3);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->cache->expects(self::once())->method('get')->willReturn(null);
$this->mailManager->expects(self::once())
->method('getImapMessage')
@@ -750,7 +744,7 @@ public function testGetAttachmentNamesSmimeDecryptException(): void {
$this->cache->expects(self::once())->method('set')->with(self::anything(), []);
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame([], $result);
@@ -763,7 +757,6 @@ public function testGetAttachmentNamesServiceException(): void {
$mailbox->setId(2);
$message = new Message();
$message->setUid(3);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$this->cache->expects(self::once())->method('get')->willReturn(null);
$this->mailManager->expects(self::once())
->method('getImapMessage')
@@ -772,7 +765,7 @@ public function testGetAttachmentNamesServiceException(): void {
$this->cache->expects(self::once())->method('set')->with(self::anything(), []);
// Act
- $result = $this->service->getAttachmentNames($account, $mailbox, $message, $client);
+ $result = $this->service->getAttachmentNames($account, $mailbox, $message);
// Assert
$this->assertSame([], $result);
diff --git a/tests/Unit/Service/DkimServiceTest.php b/tests/Unit/Service/DkimServiceTest.php
index e9f46101e3..44b0c4de23 100644
--- a/tests/Unit/Service/DkimServiceTest.php
+++ b/tests/Unit/Service/DkimServiceTest.php
@@ -13,18 +13,19 @@
use OC\Memcache\ArrayCache;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IDkimValidator;
+use OCA\Mail\Contracts\IMessageConnector;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\Message;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\DkimService;
use OCP\ICache;
use OCP\ICacheFactory;
class DkimServiceTest extends TestCase {
- private IMAPClientFactory $imapClientFactory;
- private MessageMapper $messageMapper;
+ private ProtocolFactory $protocolFactory;
+ private IMessageConnector $messageConnector;
private ICache $cache;
private IDkimValidator $dkimValidator;
private DkimService $dkimService;
@@ -32,18 +33,19 @@ class DkimServiceTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
- $this->messageMapper = $this->createMock(MessageMapper::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
+ $this->messageConnector = $this->createMock(IMessageConnector::class);
$cacheFactory = $this->createMock(ICacheFactory::class);
$this->cache = new ArrayCache('dkim_test');
$this->dkimValidator = $this->createMock(IDkimValidator::class);
$cacheFactory->method('createLocal')
->willReturn($this->cache);
+ $this->protocolFactory->method('messageConnector')
+ ->willReturn($this->messageConnector);
$this->dkimService = new DkimService(
- $this->imapClientFactory,
- $this->messageMapper,
+ $this->protocolFactory,
$cacheFactory,
$this->dkimValidator,
);
@@ -99,10 +101,14 @@ public function testValidateFetchMessageFails(): void {
$mailbox = new Mailbox();
$mailbox->setName('FooBar');
+ $message = new Message();
+ $message->setId(3);
+ $message->setUid(3);
+
$this->dkimService->validate(
$account,
$mailbox,
- 3
+ $message
);
}
@@ -115,8 +121,12 @@ public function testValidate(): void {
$mailbox = new Mailbox();
$mailbox->setName('FooBar');
- $this->messageMapper
- ->method('getFullText')
+ $message = new Message();
+ $message->setId(4);
+ $message->setUid(4);
+
+ $this->messageConnector
+ ->method('fetchMessageRaw')
->willReturn('FooBar');
$this->dkimValidator
@@ -126,7 +136,7 @@ public function testValidate(): void {
$result = $this->dkimService->validate(
$account,
$mailbox,
- 4
+ $message
);
$this->assertTrue($result);
diff --git a/tests/Unit/Service/DraftsServiceTest.php b/tests/Unit/Service/DraftsServiceTest.php
index baf074ac9b..0734c5b00d 100644
--- a/tests/Unit/Service/DraftsServiceTest.php
+++ b/tests/Unit/Service/DraftsServiceTest.php
@@ -12,7 +12,6 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OC\EventDispatcher\EventDispatcher;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
@@ -21,10 +20,11 @@
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\DraftMessageCreatedEvent;
use OCA\Mail\Exception\ClientException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\DraftsService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\OutboxService;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -53,10 +53,10 @@ class DraftsServiceTest extends TestCase {
/** @var AttachmentService|MockObject */
private $attachmentService;
- /** @var IMAPClientFactory|MockObject */
- private $clientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
private IEventDispatcher $eventDispatcher;
/** @var AccountService|MockObject */
@@ -74,8 +74,8 @@ protected function setUp(): void {
$this->transmission = $this->createMock(MailTransmission::class);
$this->mapper = $this->createMock(LocalMessageMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->eventDispatcher = $this->createMock(EventDispatcher::class);
$this->accountService = $this->createMock(AccountService::class);
@@ -85,7 +85,7 @@ protected function setUp(): void {
$this->mapper,
$this->attachmentService,
$this->eventDispatcher,
- $this->clientFactory,
+ $this->protocolFactory,
$this->mailManager,
$this->logger,
$this->accountService,
@@ -179,8 +179,8 @@ public function testSaveMessage(): void {
->method('saveWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$this->attachmentService->expects(self::once())
@@ -228,8 +228,8 @@ public function testSaveMessageNoAttachments(): void {
->method('saveWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::never())
- ->method('getClient');
+ $this->protocolFactory->expects(self::never())
+ ->method('imapClient');
$this->attachmentService->expects(self::never())
->method('handleAttachments');
$this->attachmentService->expects(self::never())
@@ -283,8 +283,8 @@ public function testUpdateMessage(): void {
->method('updateWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$this->attachmentService->expects(self::once())
@@ -341,8 +341,8 @@ public function testConvertToOutboxMessage(): void {
->method('updateWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$this->attachmentService->expects(self::once())
@@ -399,8 +399,8 @@ public function testUpdateMessageNoAttachments(): void {
$this->attachmentService->expects(self::once())
->method('updateLocalMessageAttachments')
->with($this->userId, $message2, $attachments);
- $this->clientFactory->expects(self::never())
- ->method('getClient');
+ $this->protocolFactory->expects(self::never())
+ ->method('imapClient');
$this->attachmentService->expects(self::never())
->method('handleAttachments');
$result = $this->draftsService->updateMessage($account, $message, $to, $cc, $bcc, $attachments);
diff --git a/tests/Unit/Service/IMipServiceTest.php b/tests/Unit/Service/IMipServiceTest.php
index a065359aa5..7e6285dfb6 100644
--- a/tests/Unit/Service/IMipServiceTest.php
+++ b/tests/Unit/Service/IMipServiceTest.php
@@ -198,8 +198,8 @@ public function testNoSchedulingInfo(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->calendarManager->expects(self::never())
->method('handleIMipRequest');
@@ -243,8 +243,8 @@ public function testMessageNotAvailableOnImapServer(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([]);
$this->logger->expects(self::never())
->method('warning');
@@ -288,7 +288,7 @@ public function testImapConnectionServiceException(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
+ ->method('getImapMessages')
->willThrowException(new ServiceException());
$this->logger->expects(self::once())
->method('error');
@@ -332,8 +332,8 @@ public function testIsRequest(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -392,8 +392,8 @@ public function testIsReply(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -450,8 +450,8 @@ public function testIsCancel(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -516,8 +516,8 @@ public function testIsRequestServerVersion33(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -583,8 +583,8 @@ public function testIsReplyServerVersion33(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -650,8 +650,8 @@ public function testIsCancelServerVersion33(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -713,8 +713,8 @@ public function testHandleImipRequestThrowsException(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$imapMessage->expects(self::once())
->method('getUid')
@@ -778,8 +778,8 @@ public function testHandleImipReturnsFalse(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
@@ -849,8 +849,8 @@ public function testOneOfSeveralSchedulingObjectsIsNotProcessed(): void {
->method('findById')
->willReturn($account);
$this->mailManager->expects(self::once())
- ->method('getImapMessagesForScheduleProcessing')
- ->with($account, $mailbox, [$message->getUid()])
+ ->method('getImapMessages')
+ ->with($account, $mailbox, true, $message)
->willReturn([$imapMessage]);
$this->serverVersion->expects(self::once())
->method('getMajorVersion')
diff --git a/tests/Unit/Service/ItineraryServiceTest.php b/tests/Unit/Service/ItineraryServiceTest.php
index f6d8e5ccb0..20e7a65714 100644
--- a/tests/Unit/Service/ItineraryServiceTest.php
+++ b/tests/Unit/Service/ItineraryServiceTest.php
@@ -10,27 +10,25 @@
namespace OCA\Mail\Tests\Service;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Socket;
use Nextcloud\KItinerary\Itinerary;
use OC\Memcache\ArrayCache;
use OCA\Mail\Account;
+use OCA\Mail\Attachment;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
-use OCA\Mail\IMAP\IMAPClientFactory;
-use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Db\Message;
use OCA\Mail\Integration\KItinerary\ItineraryExtractor;
+use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\ItineraryService;
+use OCA\Mail\Service\MailManager;
use OCP\ICache;
use OCP\ICacheFactory;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\NullLogger;
class ItineraryServiceTest extends TestCase {
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
-
- /** @var MessageMapper|MockObject */
- private $messageMapper;
+ /** @var MailManager|MockObject */
+ private $mailManager;
/** @var ItineraryExtractor|MockObject */
private $itineraryExtractor;
@@ -43,8 +41,7 @@ class ItineraryServiceTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
- $this->messageMapper = $this->createMock(MessageMapper::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->itineraryExtractor = $this->createMock(ItineraryExtractor::class);
$cacheFactory = $this->createMock(ICacheFactory::class);
$this->cache = new ArrayCache('itinerary_test');
@@ -53,8 +50,7 @@ protected function setUp(): void {
->willReturn($this->cache);
$this->service = new ItineraryService(
- $this->imapClientFactory,
- $this->messageMapper,
+ $this->mailManager,
$this->itineraryExtractor,
$cacheFactory,
new NullLogger(),
@@ -70,11 +66,24 @@ public function testExtractNoBodyNoAttachments() {
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
+ $message = new Message();
+ $message->setId(13);
+
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->htmlMessage = '';
+ $this->mailManager->expects($this->once())
+ ->method('getImapMessage')
+ ->with($account, $mailbox, $message, true)
+ ->willReturn($imapMessage);
+ $this->mailManager->expects($this->once())
+ ->method('getMailAttachments')
+ ->with($account, $mailbox, $message)
+ ->willReturn([]);
$this->itineraryExtractor->expects($this->once())
->method('extract')
->willReturn(new Itinerary());
- $itinerary = $this->service->extract($account, $mailbox, 13);
+ $itinerary = $this->service->extract($account, $mailbox, $message);
$this->assertEquals([], $itinerary->jsonSerialize());
}
@@ -88,22 +97,26 @@ public function testExtractFromBody() {
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->with($account)
- ->willReturn($client);
+ $message = new Message();
+ $message->setId(13);
+
$body = 'hello';
- $this->messageMapper->expects($this->once())
- ->method('getHtmlBody')
- ->with($client, 'INBOX', 13)
- ->willReturn($body);
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->htmlMessage = $body;
+ $this->mailManager->expects($this->once())
+ ->method('getImapMessage')
+ ->with($account, $mailbox, $message, true)
+ ->willReturn($imapMessage);
+ $this->mailManager->expects($this->once())
+ ->method('getMailAttachments')
+ ->with($account, $mailbox, $message)
+ ->willReturn([]);
$this->itineraryExtractor->expects($this->exactly(2))
->method('extract')
->withConsecutive([$body], ['["datafrombody"]'])
->willReturn(new Itinerary(['datafrombody']));
- $itinerary = $this->service->extract($account, $mailbox, 13);
+ $itinerary = $this->service->extract($account, $mailbox, $message);
$this->assertEquals(['datafrombody'], $itinerary->jsonSerialize());
}
@@ -117,22 +130,29 @@ public function testExtractFromAttachments() {
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->with($account)
- ->willReturn($client);
+ $message = new Message();
+ $message->setId(13);
+
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->htmlMessage = '';
$pdf = '%PDF-1.3.%';
- $this->messageMapper->expects($this->once())
- ->method('getRawAttachments')
- ->with($client, 'INBOX', 13)
- ->willReturn([$pdf]);
+ $attachment = $this->createMock(Attachment::class);
+ $attachment->method('getContent')
+ ->willReturn($pdf);
+ $this->mailManager->expects($this->once())
+ ->method('getImapMessage')
+ ->with($account, $mailbox, $message, true)
+ ->willReturn($imapMessage);
+ $this->mailManager->expects($this->once())
+ ->method('getMailAttachments')
+ ->with($account, $mailbox, $message)
+ ->willReturn([$attachment]);
$this->itineraryExtractor->expects($this->exactly(2))
->method('extract')
->withConsecutive([$pdf], ['["datafrompdf"]'])
->willReturn(new Itinerary(['datafrompdf']));
- $itinerary = $this->service->extract($account, $mailbox, 13);
+ $itinerary = $this->service->extract($account, $mailbox, $message);
$this->assertEquals(['datafrompdf'], $itinerary->jsonSerialize());
}
diff --git a/tests/Unit/Service/MailManagerTest.php b/tests/Unit/Service/MailManagerTest.php
index cfcd6dd28c..844992f60b 100644
--- a/tests/Unit/Service/MailManagerTest.php
+++ b/tests/Unit/Service/MailManagerTest.php
@@ -10,26 +10,24 @@
namespace OCA\Mail\Tests\Unit\Service;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Attachment;
+use OCA\Mail\Contracts\IMailboxConnector;
+use OCA\Mail\Contracts\IMessageConnector;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
+use OCA\Mail\Db\MessageTags;
use OCA\Mail\Db\MessageTagsMapper;
use OCA\Mail\Db\Tag;
use OCA\Mail\Db\TagMapper;
use OCA\Mail\Db\ThreadMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\Folder;
-use OCA\Mail\IMAP\FolderMapper;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\ImapFlag;
-use OCA\Mail\IMAP\MailboxSync;
-use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
@@ -37,21 +35,12 @@
use Psr\Log\LoggerInterface;
class MailManagerTest extends TestCase {
- /** @var IMAPClientFactory|MockObject */
- private $imapClientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
/** @var MailboxMapper|MockObject */
private $mailboxMapper;
- /** @var MailboxSync|MockObject */
- private $mailboxSync;
-
- /** @var FolderMapper|MockObject */
- private $folderMapper;
-
- /** @var ImapMessageMapper|MockObject */
- private $imapMessageMapper;
-
/** @var DbMessageMapper|MockObject */
private $dbMessageMapper;
@@ -76,12 +65,9 @@ class MailManagerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
- $this->folderMapper = $this->createMock(FolderMapper::class);
- $this->imapMessageMapper = $this->createMock(ImapMessageMapper::class);
$this->dbMessageMapper = $this->createMock(DbMessageMapper::class);
- $this->mailboxSync = $this->createMock(MailboxSync::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->tagMapper = $this->createMock(TagMapper::class);
@@ -89,18 +75,15 @@ protected function setUp(): void {
$this->threadMapper = $this->createMock(ThreadMapper::class);
$this->manager = new MailManager(
- $this->imapClientFactory,
$this->mailboxMapper,
- $this->mailboxSync,
- $this->folderMapper,
- $this->imapMessageMapper,
$this->dbMessageMapper,
$this->eventDispatcher,
$this->logger,
$this->tagMapper,
$this->messageTagsMapper,
- $this->threadMapper,
+ $this->protocolFactory,
new ImapFlag(),
+ $this->threadMapper,
);
}
@@ -111,9 +94,14 @@ public function testGetFolders() {
$this->createMock(Mailbox::class),
$this->createMock(Mailbox::class),
];
- $this->mailboxSync->expects($this->once())
- ->method('sync')
- ->with($this->equalTo($account));
+ $mailboxConnector = $this->createMock(IMailboxConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('mailboxConnector')
+ ->with($account)
+ ->willReturn($mailboxConnector);
+ $mailboxConnector->expects($this->once())
+ ->method('syncAll')
+ ->with($account, false);
$this->mailboxMapper->expects($this->once())
->method('findAll')
->with($this->equalTo($account))
@@ -125,26 +113,16 @@ public function testGetFolders() {
}
public function testCreateFolder() {
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$account = $this->createStub(Account::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
- $folder = $this->createStub(Folder::class);
- $this->folderMapper->expects($this->once())
- ->method('createFolder')
- ->with($this->equalTo($client), $this->equalTo('new'))
- ->willReturn($folder);
- $this->folderMapper->expects($this->once())
- ->method('fetchFolderAcls')
- ->with($this->equalTo([$folder]));
- $this->folderMapper->expects($this->once())
- ->method('detectFolderSpecialUse')
- ->with($this->equalTo([$folder]));
$mailbox = new Mailbox();
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'new')
+ $mailboxConnector = $this->createMock(IMailboxConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('mailboxConnector')
+ ->with($account)
+ ->willReturn($mailboxConnector);
+ $mailboxConnector->expects($this->once())
+ ->method('create')
+ ->with($account, 'new', [])
->willReturn($mailbox);
$created = $this->manager->createMailbox($account, 'new');
@@ -152,38 +130,18 @@ public function testCreateFolder() {
$this->assertEquals($mailbox, $created);
}
- public function testDeleteMessageSourceFolderNotFound(): void {
- /** @var Account|MockObject $account */
- $account = $this->createStub(Account::class);
- $this->eventDispatcher->expects($this->never())
- ->method('dispatchTyped');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willThrowException(new DoesNotExistException(''));
- $this->expectException(ServiceException::class);
-
- $this->manager->deleteMessage(
- $account,
- 'INBOX',
- 123
- );
- }
-
public function testDeleteMessageTrashMailboxNotFound(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
$mailAccount->setTrashMailboxId(123);
+ $account->method('getMailAccount')->willReturn($mailAccount);
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
- $account->method('getMailAccount')->willReturn($mailAccount);
- $this->eventDispatcher->expects($this->once())
+ $message = new Message();
+ $message->setUid(123);
+ $this->eventDispatcher->expects($this->never())
->method('dispatchTyped');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($mailbox);
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
@@ -192,8 +150,8 @@ public function testDeleteMessageTrashMailboxNotFound(): void {
$this->manager->deleteMessage(
$account,
- 'INBOX',
- 123
+ $mailbox,
+ $message
);
}
@@ -204,36 +162,35 @@ public function testDeleteMessage(): void {
$mailAccount->setTrashMailboxId(123);
$account->method('getMailAccount')->willReturn($mailAccount);
$inbox = new Mailbox();
+ $inbox->setId(1);
$inbox->setName('INBOX');
$trash = new Mailbox();
+ $trash->setId(123);
$trash->setName('Trash');
+ $message = new Message();
+ $message->setUid(123);
$this->eventDispatcher->expects($this->exactly(2))
->method('dispatchTyped');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($inbox);
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
->willReturn($trash);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
- $this->imapMessageMapper->expects($this->once())
- ->method('move')
- ->with(
- $client,
- 'INBOX',
- 123,
- 'Trash'
- );
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('moveMessages')
+ ->with($account, $trash, $inbox, $message)
+ ->willReturn([$message]);
+ $this->dbMessageMapper->expects($this->once())
+ ->method('updateBulk');
$this->manager->deleteMessage(
$account,
- 'INBOX',
- 123
+ $inbox,
+ $message
);
}
@@ -244,273 +201,125 @@ public function testExpungeMessage(): void {
$mailAccount->setTrashMailboxId(123);
$account->method('getMailAccount')->willReturn($mailAccount);
$source = new Mailbox();
+ $source->setId(123);
$source->setName('Trash');
$trash = new Mailbox();
+ $trash->setId(123);
$trash->setName('Trash');
+ $message = new Message();
+ $message->setUid(123);
$this->eventDispatcher->expects($this->exactly(2))
->method('dispatchTyped');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'Trash')
- ->willReturn($source);
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
->willReturn($trash);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
- $this->imapMessageMapper->expects($this->once())
- ->method('expunge')
- ->with(
- $client,
- 'Trash',
- 123
- );
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('deleteMessages')
+ ->with($account, $source, $message)
+ ->willReturn([$message]);
+ $this->dbMessageMapper->expects($this->once())
+ ->method('deleteByUid')
+ ->with($source, 123);
$this->manager->deleteMessage(
$account,
- 'Trash',
- 123
+ $source,
+ $message
);
}
- public function testSetCustomFlagNoIMAPCapabilities(): void {
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $account = $this->createStub(Account::class);
-
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $this->imapMessageMapper->expects($this->never())
- ->method('addFlag');
- $this->imapMessageMapper->expects($this->never())
- ->method('removeFlag');
-
- $this->manager->flagMessage($account, 'INBOX', 123, Tag::LABEL_IMPORTANT, true);
- $this->manager->flagMessage($account, 'INBOX', 123, Tag::LABEL_IMPORTANT, false);
- }
-
- public function testSetCustomFlagWithIMAPCapabilities(): void {
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $account = $this->createStub(Account::class);
-
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $client->expects($this->once())
- ->method('status')
- ->willReturn([ 'permflags' => [ '11' => "\*" ] ]);
- $this->imapMessageMapper->expects($this->once())
- ->method('addFlag');
-
- $this->manager->flagMessage($account, 'INBOX', 123, Tag::LABEL_IMPORTANT, true);
- }
-
- public function testUnsetCustomFlagWithIMAPCapabilities(): void {
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $account = $this->createStub(Account::class);
-
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $client->expects($this->once())
- ->method('status')
- ->willReturn([ 'permflags' => [ '11' => "\*" ] ]);
- $this->imapMessageMapper->expects($this->once())
- ->method('removeFlag');
-
- $this->manager->flagMessage($account, 'INBOX', 123, Tag::LABEL_IMPORTANT, false);
- }
-
- public function testFilterFlagsWithSystemFlags(): void {
- $account = $this->createStub(Account::class);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $flags = [
- 'seen' => [\Horde_Imap_Client::FLAG_SEEN],
- 'answered' => [\Horde_Imap_Client::FLAG_ANSWERED],
- 'flagged' => [\Horde_Imap_Client::FLAG_FLAGGED],
- 'deleted' => [\Horde_Imap_Client::FLAG_DELETED],
- 'draft' => [\Horde_Imap_Client::FLAG_DRAFT],
- 'recent' => [\Horde_Imap_Client::FLAG_RECENT],
- ];
-
- // test all system flags
- foreach ($flags as $k => $flag) {
- $this->assertEquals($this->manager->filterFlags($client, $account, $k, 'INBOX'), $flags[$k]);
- }
- }
-
- public function testFilterFlagsWithDefinedKeyword() {
+ public function testFlagMessages(): void {
$account = $this->createStub(Account::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
-
- $client->expects($this->exactly(2))
- ->method('status')
- ->willReturn(['permflags' => ['\seen', '$junk', '$notjunk']]);
-
- // test keyword supported
- $this->assertEquals(['$junk'], $this->manager->filterFlags($client, $account, '$junk', 'INBOX'));
- // test keyword unsupported
- $this->assertEquals([], $this->manager->filterFlags($client, $account, '$autojunk', 'INBOX'));
- }
-
- public function testFilterFlagsWithCustomKeyword() {
- $account = $this->createStub(Account::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
-
- $client->expects($this->exactly(2))
- ->method('status')
- ->willReturnOnConsecutiveCalls(
- ['permflags' => ['\seen', '$junk', '$notjunk', '\*']],
- ['permflags' => ['\seen', '$junk', '$notjunk']],
- );
-
- // test custom keyword supported
- $this->assertEquals([Tag::LABEL_IMPORTANT], $this->manager->filterFlags($client, $account, Tag::LABEL_IMPORTANT, 'INBOX'));
- // test custom keyword unsupported
- $this->assertEquals([], $this->manager->filterFlags($client, $account, Tag::LABEL_IMPORTANT, 'INBOX'));
- }
-
- public function testFilterFlagsNoCapabilities() {
- $account = $this->createStub(Account::class);
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
-
- $this->assertEquals([], $this->manager->filterFlags($client, $account, Tag::LABEL_IMPORTANT, 'INBOX'));
- }
-
- public function testIsPermflagsEnabledTrue(): void {
- $account = $this->createStub(Account::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
-
- $client->expects($this->once())
- ->method('status')
- ->willReturn(['permflags' => [ '11' => "\*"] ]);
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $message = new Message();
+ $message->setUid(123);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('flagMessages')
+ ->with($account, $mailbox, 'seen', true, $message)
+ ->willReturn([$message]);
+ $this->dbMessageMapper->expects($this->once())
+ ->method('updateBulk');
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
- $this->assertTrue($this->manager->isPermflagsEnabled($client, $account, 'INBOX'));
+ $this->manager->flagMessages($account, $mailbox, 'seen', true, $message);
}
- public function testIsPermflagsEnabledFalse(): void {
+ public function testIsPermflagsEnabled(): void {
$account = $this->createStub(Account::class);
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
-
- $client->expects($this->once())
- ->method('status')
- ->willReturn([]);
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('isPermflagsEnabled')
+ ->with($account, $mailbox)
+ ->willReturn(true);
- $this->assertFalse($this->manager->isPermflagsEnabled($client, $account, 'INBOX'));
- }
-
- public function testRemoveFlag(): void {
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
- $account = $this->createStub(Account::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
- ->willReturn($client);
- $mb = $this->createStub(Mailbox::class);
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($mb);
- $this->imapMessageMapper->expects($this->never())
- ->method('addFlag');
- $this->imapMessageMapper->expects($this->once())
- ->method('removeFlag')
- ->with($client, $mb, [123], '\\seen');
-
- $this->manager->flagMessage($account, 'INBOX', 123, 'seen', false);
+ $this->assertTrue($this->manager->isPermflagsEnabled($account, $mailbox));
}
public function testTagMessage(): void {
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $account = $this->createMock(Account::class);
+ $account = $this->createStub(Account::class);
$tag = new Tag();
$tag->setImapLabel(Tag::LABEL_IMPORTANT);
- $message = new \OCA\Mail\Db\Message();
+ $message = new Message();
$message->setUid(123);
$message->setMessageId('');
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $mb = new Mailbox();
- $mb->setName('INBOX');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($mb);
- $client->expects($this->once())
- ->method('status')
- ->willReturn(['permflags' => [ '11' => "\*"] ]);
- $this->imapMessageMapper->expects($this->once())
- ->method('addFlag')
- ->with($client, $mb, [123], Tag::LABEL_IMPORTANT);
- $account->expects($this->once())
- ->method('getUserId')
- ->willReturn('test');
- $this->manager->tagMessage($account, 'INBOX', $message, $tag, true);
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('tagMessages')
+ ->with($account, $mailbox, $tag, true, $message)
+ ->willReturn([$message]);
+ $this->dbMessageMapper->expects($this->once())
+ ->method('updateBulk');
+
+ $this->manager->tagMessages($account, $mailbox, $tag, true, $message);
}
public function testUntagMessage(): void {
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $account = $this->createMock(Account::class);
+ $account = $this->createStub(Account::class);
$tag = new Tag();
$tag->setImapLabel(Tag::LABEL_IMPORTANT);
- $message = new \OCA\Mail\Db\Message();
- $message->setUid(123);
- $message->setMessageId('');
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $mb = new Mailbox();
- $mb->setName('INBOX');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($mb);
- $client->expects($this->once())
- ->method('status')
- ->willReturn(['permflags' => [ '11' => "\*"] ]);
- $this->imapMessageMapper->expects($this->once())
- ->method('removeFlag')
- ->with($client, $mb, [123], Tag::LABEL_IMPORTANT);
- $this->imapMessageMapper->expects($this->never())
- ->method('addFlag');
- $account->expects($this->never())
- ->method('getUserId')
- ->willReturn('test');
- $this->manager->tagMessage($account, 'INBOX', $message, $tag, false);
- }
-
- public function testTagNoIMAPCapabilities(): void {
- $client = $this->createMock(Horde_Imap_Client_Socket::class);
- $account = $this->createMock(Account::class);
- $message = new \OCA\Mail\Db\Message();
+ $message = new Message();
$message->setUid(123);
$message->setMessageId('');
- $tag = new Tag();
- $tag->setImapLabel(Tag::LABEL_IMPORTANT);
+ $mailbox = new Mailbox();
+ $mailbox->setName('INBOX');
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('tagMessages')
+ ->with($account, $mailbox, $tag, false, $message)
+ ->willReturn([$message]);
+ $this->dbMessageMapper->expects($this->once())
+ ->method('updateBulk');
- $this->imapClientFactory->expects($this->any())
- ->method('getClient')
- ->willReturn($client);
- $mb = new Mailbox();
- $mb->setName('INBOX');
- $this->mailboxMapper->expects($this->once())
- ->method('find')
- ->with($account, 'INBOX')
- ->willReturn($mb);
- $client->expects($this->once())
- ->method('status')
- ->willReturn([]);
- $this->imapMessageMapper->expects($this->never())
- ->method('removeFlag');
- $this->imapMessageMapper->expects($this->never())
- ->method('addFlag');
- $account->expects($this->once())
- ->method('getUserId')
- ->willReturn('test');
- $this->manager->tagMessage($account, 'INBOX', $message, $tag, true);
+ $this->manager->tagMessages($account, $mailbox, $tag, false, $message);
}
public function testGetThread(): void {
@@ -525,10 +334,7 @@ public function testGetThread(): void {
}
public function testGetMailAttachments(): void {
- $account = $this->createMock(Account::class);
- $account->expects($this->once())
- ->method('getUserId')
- ->willReturn('user');
+ $account = $this->createStub(Account::class);
$attachments = [
new Attachment(
null,
@@ -540,23 +346,22 @@ public function testGetMailAttachments(): void {
null,
),
];
- $client = $this->createStub(Horde_Imap_Client_Socket::class);
$mailbox = new Mailbox();
$mailbox->setName('Inbox');
$message = new Message();
$message->setUid(123);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects($this->once())
+ ->method('messageConnector')
->with($account)
- ->willReturn($client);
- $this->imapMessageMapper->expects($this->once())
- ->method('getAttachments')
- ->with(
- $client,
- $mailbox->getName(),
- $message->getUid()
- )->willReturn($attachments);
+ ->willReturn($messageConnector);
+ $messageConnector->expects($this->once())
+ ->method('fetchAttachments')
+ ->with($account, $mailbox, $message)
+ ->willReturn($attachments);
+
$result = $this->manager->getMailAttachments($account, $mailbox, $message);
+
$this->assertEquals($attachments, $result);
}
@@ -667,8 +472,16 @@ public function testMoveInbox(): void {
$srcMailbox->setId($srcMailboxId);
$srcMailbox->setAccountId($mailAccount->getId());
$srcMailbox->setName('INBOX');
+ $dstMailbox = new Mailbox();
+ $dstMailbox->setId($dstMailboxId);
+ $dstMailbox->setAccountId($mailAccount->getId());
+ $dstMailbox->setName('Trash');
+ $message1 = new Message();
+ $message1->setUid(200);
+ $message2 = new Message();
+ $message2->setUid(300);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('find')
->with($account, $srcMailbox->getName())
->willReturn($srcMailbox);
@@ -680,17 +493,25 @@ public function testMoveInbox(): void {
['messageUid' => 200, 'mailboxName' => 'INBOX'],
['messageUid' => 300, 'mailboxName' => 'INBOX'],
]);
- $dstMailbox = new Mailbox();
- $dstMailbox->setId($dstMailboxId);
- $dstMailbox->setAccountId($mailAccount->getId());
- $dstMailbox->setName('Trash');
-
- $this->imapMessageMapper
- ->expects(self::exactly(2))
- ->method('move');
- $this->eventDispatcher
- ->expects(self::exactly(2))
- ->method('dispatch');
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('findByUids')
+ ->with($srcMailbox, [200, 300])
+ ->willReturn([$message1, $message2]);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory
+ ->expects(self::once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector
+ ->expects(self::once())
+ ->method('moveMessages')
+ ->with($account, $dstMailbox, $srcMailbox, $message1, $message2)
+ ->willReturn([$message1, $message2]);
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('updateBulk');
$this->manager->moveThread(
$account,
@@ -713,8 +534,16 @@ public function testMoveTrash(): void {
$srcMailbox->setId($srcMailboxId);
$srcMailbox->setAccountId($mailAccount->getId());
$srcMailbox->setName('Trash');
+ $dstMailbox = new Mailbox();
+ $dstMailbox->setId($dstMailboxId);
+ $dstMailbox->setAccountId($mailAccount->getId());
+ $dstMailbox->setName('INBOX');
+ $message1 = new Message();
+ $message1->setUid(200);
+ $message2 = new Message();
+ $message2->setUid(300);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('find')
->with($account, $srcMailbox->getName())
->willReturn($srcMailbox);
@@ -726,17 +555,25 @@ public function testMoveTrash(): void {
['messageUid' => 200, 'mailboxName' => 'Trash'],
['messageUid' => 300, 'mailboxName' => 'Trash'],
]);
- $dstMailbox = new Mailbox();
- $dstMailbox->setId($dstMailboxId);
- $dstMailbox->setAccountId($mailAccount->getId());
- $dstMailbox->setName('INBOX');
-
- $this->imapMessageMapper
- ->expects(self::exactly(2))
- ->method('move');
- $this->eventDispatcher
- ->expects(self::exactly(2))
- ->method('dispatch');
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('findByUids')
+ ->with($srcMailbox, [200, 300])
+ ->willReturn([$message1, $message2]);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory
+ ->expects(self::once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector
+ ->expects(self::once())
+ ->method('moveMessages')
+ ->with($account, $dstMailbox, $srcMailbox, $message1, $message2)
+ ->willReturn([$message1, $message2]);
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('updateBulk');
$this->manager->moveThread(
$account,
@@ -759,8 +596,16 @@ public function testDeleteInbox(): void {
$mailbox->setId($mailboxId);
$mailbox->setAccountId($mailAccount->getId());
$mailbox->setName('INBOX');
+ $trashMailbox = new Mailbox();
+ $trashMailbox->setId($trashMailboxId);
+ $trashMailbox->setAccountId($mailAccount->getId());
+ $trashMailbox->setName('Trash');
+ $message1 = new Message();
+ $message1->setUid(200);
+ $message2 = new Message();
+ $message2->setUid(300);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('find')
->with($account, $mailbox->getName())
->willReturn($mailbox);
@@ -772,18 +617,30 @@ public function testDeleteInbox(): void {
['messageUid' => 200, 'mailboxName' => 'INBOX'],
['messageUid' => 300, 'mailboxName' => 'INBOX'],
]);
- $trashMailbox = new Mailbox();
- $trashMailbox->setId($trashMailboxId);
- $trashMailbox->setAccountId($mailAccount->getId());
- $trashMailbox->setName('Trash');
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('findByUids')
+ ->with($mailbox, [200, 300])
+ ->willReturn([$message1, $message2]);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('findById')
->with($trashMailbox->getId())
->willReturn($trashMailbox);
- $this->imapMessageMapper
- ->expects(self::exactly(2))
- ->method('move');
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory
+ ->expects(self::once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector
+ ->expects(self::once())
+ ->method('moveMessages')
+ ->with($account, $trashMailbox, $mailbox, $message1, $message2)
+ ->willReturn([$message1, $message2]);
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('updateBulk');
$this->eventDispatcher
->expects(self::exactly(4))
->method('dispatchTyped');
@@ -806,13 +663,17 @@ public function testDeleteTrash(): void {
$mailbox->setId($mailboxId);
$mailbox->setAccountId($mailAccount->getId());
$mailbox->setName('Trash');
+ $message1 = new Message();
+ $message1->setUid(200);
+ $message2 = new Message();
+ $message2->setUid(300);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('find')
->with($account, $mailbox->getName())
->willReturn($mailbox);
$this->mailboxMapper
- ->expects(self::exactly(2))
+ ->expects(self::once())
->method('findById')
->with($mailbox->getId())
->willReturn($mailbox);
@@ -824,9 +685,26 @@ public function testDeleteTrash(): void {
['messageUid' => 200, 'mailboxName' => 'Trash'],
['messageUid' => 300, 'mailboxName' => 'Trash'],
]);
- $this->imapMessageMapper
- ->expects(self::exactly(2))
- ->method('expunge');
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('findByUids')
+ ->with($mailbox, [200, 300])
+ ->willReturn([$message1, $message2]);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory
+ ->expects(self::once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector
+ ->expects(self::once())
+ ->method('deleteMessages')
+ ->with($account, $mailbox, $message1, $message2)
+ ->willReturn([$message1, $message2]);
+ $this->dbMessageMapper
+ ->expects(self::once())
+ ->method('deleteByUid')
+ ->with($mailbox, 200, 300);
$this->eventDispatcher
->expects(self::exactly(4))
->method('dispatchTyped');
@@ -837,4 +715,122 @@ public function testDeleteTrash(): void {
$threadRootId
);
}
+
+ public function testClearMailboxWithoutMessagesDoesNothing(): void {
+ $account = $this->createMock(Account::class);
+ $mailbox = new Mailbox();
+ $mailbox->setId(1);
+ $this->dbMessageMapper->expects(self::once())
+ ->method('findAllUids')
+ ->with($mailbox)
+ ->willReturn([]);
+ $this->dbMessageMapper->expects(self::never())
+ ->method('findByUids');
+ $this->protocolFactory->expects(self::never())
+ ->method('messageConnector');
+
+ $this->manager->clearMailbox($account, $mailbox);
+ }
+
+ public function testClearMailboxMovesAllMessagesToTrash(): void {
+ $account = $this->createMock(Account::class);
+ $mailAccount = new MailAccount();
+ $mailAccount->setTrashMailboxId(123);
+ $account->method('getMailAccount')->willReturn($mailAccount);
+ $inbox = new Mailbox();
+ $inbox->setId(1);
+ $inbox->setName('INBOX');
+ $trash = new Mailbox();
+ $trash->setId(123);
+ $trash->setName('Trash');
+ $message1 = new Message();
+ $message1->setUid(11);
+ $message2 = new Message();
+ $message2->setUid(12);
+ $this->dbMessageMapper->expects(self::once())
+ ->method('findAllUids')
+ ->with($inbox)
+ ->willReturn([11, 12]);
+ $this->dbMessageMapper->expects(self::once())
+ ->method('findByUids')
+ ->with($inbox, [11, 12])
+ ->willReturn([$message1, $message2]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->with(123)
+ ->willReturn($trash);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->expects(self::once())
+ ->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ $messageConnector->expects(self::once())
+ ->method('moveMessages')
+ ->with($account, $trash, $inbox, $message1, $message2)
+ ->willReturn([$message1, $message2]);
+ $this->dbMessageMapper->expects(self::once())
+ ->method('updateBulk');
+
+ $this->manager->clearMailbox($account, $inbox);
+ }
+
+ public function testDeleteTagUntagsMessagesGroupedByMailbox(): void {
+ $account = $this->createMock(Account::class);
+ $tag = new Tag();
+ $tag->setImapLabel('$label1');
+ $this->tagMapper->expects(self::once())
+ ->method('getTagForUser')
+ ->with(5, 'user')
+ ->willReturn($tag);
+ $messageTag = new MessageTags();
+ $messageTag->setImapMessageId('msg@id');
+ $this->messageTagsMapper->expects(self::once())
+ ->method('getMessagesByTag')
+ ->with(5)
+ ->willReturn([$messageTag]);
+ // two messages of the same tag living in different mailboxes
+ $messageInInbox = new Message();
+ $messageInInbox->setUid(11);
+ $messageInInbox->setMailboxId(1);
+ $messageInArchive = new Message();
+ $messageInArchive->setUid(22);
+ $messageInArchive->setMailboxId(2);
+ $this->dbMessageMapper->expects(self::once())
+ ->method('findByMessageId')
+ ->with($account, 'msg@id')
+ ->willReturn([$messageInInbox, $messageInArchive]);
+ $inbox = new Mailbox();
+ $inbox->setId(1);
+ $archive = new Mailbox();
+ $archive->setId(2);
+ $this->mailboxMapper->method('findById')
+ ->willReturnMap([
+ [1, $inbox],
+ [2, $archive],
+ ]);
+ $messageConnector = $this->createMock(IMessageConnector::class);
+ $this->protocolFactory->method('messageConnector')
+ ->with($account)
+ ->willReturn($messageConnector);
+ // the connector is asked to untag once per mailbox, with that mailbox's messages
+ $tagCalls = [];
+ $messageConnector->expects(self::exactly(2))
+ ->method('tagMessages')
+ ->willReturnCallback(function (Account $a, Mailbox $mailbox, Tag $t, bool $value, Message ...$messages) use (&$tagCalls): array {
+ $tagCalls[$mailbox->getId()] = $messages;
+ return $messages;
+ });
+ $this->messageTagsMapper->expects(self::once())
+ ->method('delete')
+ ->with($messageTag);
+ $this->tagMapper->expects(self::once())
+ ->method('delete')
+ ->with($tag)
+ ->willReturn($tag);
+
+ $this->manager->deleteTag(5, 'user', [$account]);
+
+ self::assertEquals([$messageInInbox], $tagCalls[1]);
+ self::assertEquals([$messageInArchive], $tagCalls[2]);
+ }
}
diff --git a/tests/Unit/Service/MailTransmissionTest.php b/tests/Unit/Service/MailTransmissionTest.php
index c5965a380c..a17e8aaac2 100644
--- a/tests/Unit/Service/MailTransmissionTest.php
+++ b/tests/Unit/Service/MailTransmissionTest.php
@@ -15,7 +15,6 @@
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Alias;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
@@ -25,11 +24,12 @@
use OCA\Mail\Db\Message as DbMessage;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Model\Message;
use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AliasesService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\TransmissionService;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -40,8 +40,8 @@
use Psr\Log\LoggerInterface;
class MailTransmissionTest extends TestCase {
- private IMAPClientFactory|MockObject $imapClientFactory;
- private IMailManager|MockObject $mailManager;
+ private ProtocolFactory|MockObject $protocolFactory;
+ private MailManager|MockObject $mailManager;
private SmtpClientFactory|MockObject $smtpClientFactory;
private IEventDispatcher|MockObject $eventDispatcher;
private MailboxMapper|MockObject $mailboxMapper;
@@ -55,7 +55,7 @@ class MailTransmissionTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->smtpClientFactory = $this->createMock(SmtpClientFactory::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
@@ -64,10 +64,10 @@ protected function setUp(): void {
$this->performanceLogger = $this->createMock(PerformanceLogger::class);
$this->aliasService = $this->createMock(AliasesService::class);
$this->transmissionService = $this->createMock(TransmissionService::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->transmission = new MailTransmission(
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->smtpClientFactory,
$this->eventDispatcher,
$this->mailboxMapper,
@@ -347,8 +347,8 @@ public function testSaveDraft() {
$message = new Message();
$client = $this->createStub(Horde_Imap_Client_Socket::class);
- $this->imapClientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$draftsMailbox = new DbMailbox();
diff --git a/tests/Unit/Service/OutboxServiceTest.php b/tests/Unit/Service/OutboxServiceTest.php
index 5534f8bae5..48086feebd 100644
--- a/tests/Unit/Service/OutboxServiceTest.php
+++ b/tests/Unit/Service/OutboxServiceTest.php
@@ -12,16 +12,16 @@
use ChristophWurst\Nextcloud\Testing\TestCase;
use OC\EventDispatcher\EventDispatcher;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Exception\ClientException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\OutboxService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -45,10 +45,10 @@ class OutboxServiceTest extends TestCase {
/** @var AttachmentService|MockObject */
private $attachmentService;
- /** @var IMAPClientFactory|MockObject */
- private $clientFactory;
+ /** @var ProtocolFactory|MockObject */
+ private $protocolFactory;
- /** @var IMailManager|MockObject */
+ /** @var MailManager|MockObject */
private $mailManager;
/** @var AccountService|MockObject */
@@ -66,8 +66,8 @@ protected function setUp(): void {
$this->mapper = $this->createMock(LocalMessageMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
- $this->mailManager = $this->createMock(IMailManager::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->accountService = $this->createMock(AccountService::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -76,7 +76,7 @@ protected function setUp(): void {
$this->mapper,
$this->attachmentService,
$this->createMock(EventDispatcher::class),
- $this->clientFactory,
+ $this->protocolFactory,
$this->mailManager,
$this->accountService,
$this->timeFactory,
@@ -217,8 +217,8 @@ public function testSaveMessage(): void {
->method('saveWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$this->attachmentService->expects(self::once())
@@ -265,8 +265,8 @@ public function testSaveMessageNoAttachments(): void {
->method('saveWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::never())
- ->method('getClient');
+ $this->protocolFactory->expects(self::never())
+ ->method('imapClient');
$this->attachmentService->expects(self::never())
->method('handleAttachments');
$this->attachmentService->expects(self::never())
@@ -319,8 +319,8 @@ public function testUpdateMessage(): void {
->method('updateWithRecipients')
->with($message, [$rTo], $cc, $bcc)
->willReturn($message2);
- $this->clientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->with($account)
->willReturn($client);
$this->attachmentService->expects(self::once())
@@ -377,8 +377,8 @@ public function testUpdateMessageNoAttachments(): void {
$this->attachmentService->expects(self::once())
->method('updateLocalMessageAttachments')
->with($this->userId, $message2, $attachments);
- $this->clientFactory->expects(self::never())
- ->method('getClient');
+ $this->protocolFactory->expects(self::never())
+ ->method('imapClient');
$this->attachmentService->expects(self::never())
->method('handleAttachments');
diff --git a/tests/Unit/Service/Search/MailSearchTest.php b/tests/Unit/Service/Search/MailSearchTest.php
index 3f7ce83821..66f822a974 100644
--- a/tests/Unit/Service/Search/MailSearchTest.php
+++ b/tests/Unit/Service/Search/MailSearchTest.php
@@ -17,7 +17,7 @@
use OCA\Mail\Exception\MailboxLockedException;
use OCA\Mail\Exception\MailboxNotCachedException;
use OCA\Mail\IMAP\PreviewEnhancer;
-use OCA\Mail\IMAP\Search\Provider;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Search\FilterStringParser;
use OCA\Mail\Service\Search\Flag;
use OCA\Mail\Service\Search\MailSearch;
@@ -32,8 +32,8 @@ class MailSearchTest extends TestCase {
/** @var MailSearch */
private $search;
- /** @var Provider|MockObject */
- private $imapSearchProvider;
+ /** @var MailManager|MockObject */
+ private $mailManager;
/** @var PreviewEnhancer|MockObject */
private $previewEnhancer;
@@ -48,14 +48,14 @@ protected function setUp(): void {
parent::setUp();
$this->filterStringParser = $this->createMock(FilterStringParser::class);
- $this->imapSearchProvider = $this->createMock(Provider::class);
+ $this->mailManager = $this->createMock(MailManager::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->previewEnhancer = $this->createMock(PreviewEnhancer::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->search = new MailSearch(
$this->filterStringParser,
- $this->imapSearchProvider,
+ $this->mailManager,
$this->messageMapper,
$this->previewEnhancer,
$this->timeFactory
@@ -144,8 +144,8 @@ public function testFindFlagsLocally() {
$this->createMock(Message::class),
$this->createMock(Message::class),
]);
- $this->imapSearchProvider->expects($this->never())
- ->method('findMatches');
+ $this->mailManager->expects($this->never())
+ ->method('findMessages');
$this->previewEnhancer->expects($this->once())
->method('process')
->willReturnArgument(2);
@@ -180,8 +180,8 @@ public function testFindText() {
->method('parse')
->with('my search')
->willReturn($query);
- $this->imapSearchProvider->expects($this->once())
- ->method('findMatches')
+ $this->mailManager->expects($this->once())
+ ->method('findMessages')
->with($account, $mailbox, $query)
->willReturn([2, 3]);
$this->messageMapper->expects($this->once())
diff --git a/tests/Unit/Service/SetupServiceTest.php b/tests/Unit/Service/SetupServiceTest.php
index 7f47dd8a85..16b9f9a912 100644
--- a/tests/Unit/Service/SetupServiceTest.php
+++ b/tests/Unit/Service/SetupServiceTest.php
@@ -19,7 +19,7 @@
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\TagMapper;
use OCA\Mail\Exception\CouldNotConnectException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\SetupService;
use OCA\Mail\SMTP\SmtpClientFactory;
@@ -47,7 +47,7 @@ class SetupServiceTest extends TestCase {
private AccountService&MockObject $accountService;
private ICrypto&MockObject $crypto;
private SmtpClientFactory&MockObject $smtpClientFactory;
- private IMAPClientFactory&MockObject $imapClientFactory;
+ private ProtocolFactory&MockObject $protocolFactory;
private LoggerInterface&MockObject $logger;
private TagMapper&MockObject $tagMapper;
private SetupService $setupService;
@@ -58,7 +58,7 @@ protected function setUp(): void {
$this->accountService = $this->createMock(AccountService::class);
$this->crypto = $this->createMock(ICrypto::class);
$this->smtpClientFactory = $this->createMock(SmtpClientFactory::class);
- $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->tagMapper = $this->createMock(TagMapper::class);
@@ -66,7 +66,7 @@ protected function setUp(): void {
$this->accountService,
$this->crypto,
$this->smtpClientFactory,
- $this->imapClientFactory,
+ $this->protocolFactory,
$this->logger,
$this->tagMapper
);
@@ -77,8 +77,8 @@ private function mockSuccessfulImapConnection(): Horde_Imap_Client_Socket&MockOb
$imapClient->expects(self::once())->method('login');
$imapClient->expects(self::once())->method('logout');
- $this->imapClientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->willReturn($imapClient);
return $imapClient;
@@ -209,7 +209,7 @@ public function testCreateNewAccountWithOAuth2(): void {
->method('debug')
->with(self::stringContains('account created '));
- $this->imapClientFactory->expects(self::never())->method('getClient');
+ $this->protocolFactory->expects(self::never())->method('imapClient');
$this->smtpClientFactory->expects(self::never())->method('create');
$this->accountService->expects(self::once())
@@ -272,8 +272,8 @@ public function testCreateNewAccountImapConnectionFailure(): void {
$imapClient->expects(self::once())
->method('logout');
- $this->imapClientFactory->expects(self::once())
- ->method('getClient')
+ $this->protocolFactory->expects(self::once())
+ ->method('imapClient')
->willReturn($imapClient);
$this->setupService->createNewAccount(
diff --git a/tests/Unit/Service/Sync/ImapToDbSynchronizerTest.php b/tests/Unit/Service/Sync/ImapToDbSynchronizerTest.php
index cc11db3ba6..ba800d6e2c 100644
--- a/tests/Unit/Service/Sync/ImapToDbSynchronizerTest.php
+++ b/tests/Unit/Service/Sync/ImapToDbSynchronizerTest.php
@@ -13,17 +13,17 @@
use Horde_Imap_Client_Data_Capability_Imap;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
-use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper as DatabaseMessageMapper;
use OCA\Mail\Db\TagMapper;
use OCA\Mail\Events\SynchronizationEvent;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\IMAP\Sync\Synchronizer;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Classification\NewMessagesClassifier;
+use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCA\Mail\Support\PerformanceLogger;
use OCA\Mail\Support\PerformanceLoggerTask;
@@ -33,7 +33,7 @@
class ImapToDbSynchronizerTest extends TestCase {
private DatabaseMessageMapper&MockObject $dbMapper;
- private IMAPClientFactory&MockObject $clientFactory;
+ private ProtocolFactory&MockObject $protocolFactory;
private ImapMessageMapper&MockObject $imapMapper;
private MailboxMapper&MockObject $mailboxMapper;
private IEventDispatcher&MockObject $dispatcher;
@@ -43,7 +43,7 @@ class ImapToDbSynchronizerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
$this->dbMapper = $this->createMock(DatabaseMessageMapper::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->imapMapper = $this->createMock(ImapMessageMapper::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
@@ -52,7 +52,7 @@ protected function setUp(): void {
->willReturn($this->createStub(PerformanceLoggerTask::class));
$this->synchronizer = new ImapToDbSynchronizer(
$this->dbMapper,
- $this->clientFactory,
+ $this->protocolFactory,
$this->imapMapper,
$this->mailboxMapper,
$this->createStub(DatabaseMessageMapper::class),
@@ -60,7 +60,7 @@ protected function setUp(): void {
$this->dispatcher,
$this->performanceLogger,
$this->createStub(LoggerInterface::class),
- $this->createStub(IMailManager::class),
+ $this->createStub(MailManager::class),
$this->createStub(TagMapper::class),
$this->createStub(NewMessagesClassifier::class),
);
@@ -85,8 +85,8 @@ public function testInitialSyncGetsSyncTokenFromCacheClient(): void {
->method('getSyncToken')
->with('INBOX')
->willReturn('dG9rZW5XaXRoSA==');
- $this->clientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with($account, false)
->willReturn($noCacheClient);
$this->dbMapper->method('findHighestUid')->willReturn(null);
diff --git a/tests/Unit/Service/Sync/SyncServiceTest.php b/tests/Unit/Service/Sync/SyncServiceTest.php
index c79d418a85..24c31b17d1 100644
--- a/tests/Unit/Service/Sync/SyncServiceTest.php
+++ b/tests/Unit/Service/Sync/SyncServiceTest.php
@@ -10,14 +10,16 @@
namespace OCA\Mail\Tests\Unit\Service\Sync;
use OCA\Mail\Account;
+use OCA\Mail\Contracts\IMailboxConnector;
+use OCA\Mail\Contracts\IMessageConnector;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Exception\MailboxNotCachedException;
-use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxStats;
use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\IMAP\PreviewEnhancer;
use OCA\Mail\IMAP\Sync\Response;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\Service\Search\FilterStringParser;
use OCA\Mail\Service\Sync\ImapToDbSynchronizer;
use OCA\Mail\Service\Sync\SyncService;
@@ -26,7 +28,11 @@
final class SyncServiceTest extends TestCase {
- private IMAPClientFactory&MockObject $clientFactory;
+ private ProtocolFactory&MockObject $protocolFactory;
+
+ private IMailboxConnector&MockObject $mailboxConnector;
+
+ private IMessageConnector&MockObject $messageConnector;
/** @var ImapToDbSynchronizer */
private $synchronizer;
@@ -43,13 +49,20 @@ final class SyncServiceTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
+ $this->mailboxConnector = $this->createMock(IMailboxConnector::class);
+ $this->messageConnector = $this->createMock(IMessageConnector::class);
$this->synchronizer = $this->createMock(ImapToDbSynchronizer::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->mailboxSync = $this->createMock(MailboxSync::class);
+ $this->protocolFactory->method('mailboxConnector')
+ ->willReturn($this->mailboxConnector);
+ $this->protocolFactory->method('messageConnector')
+ ->willReturn($this->messageConnector);
+
$this->syncService = new SyncService(
- $this->clientFactory,
+ $this->protocolFactory,
$this->synchronizer,
$this->createStub(FilterStringParser::class),
$this->messageMapper,
@@ -90,28 +103,24 @@ public function testSyncMailboxReturnsFolderStats(): void {
[],
new MailboxStats(42, 10, null)
);
- $this->clientFactory
- ->method('getClient')
- ->with($account)
- ->willReturn($this->createStub(\Horde_Imap_Client_Socket::class));
$this->messageMapper
->method('findUidsForIds')
->with($mailbox, [])
->willReturn([]);
- $this->synchronizer->expects($this->once())
- ->method('sync')
+ $this->mailboxConnector->expects($this->once())
+ ->method('syncOne')
+ ->with($account, $mailbox);
+ $this->messageConnector->expects($this->once())
+ ->method('syncMailbox')
->with(
$account,
- $this->createStub(\Horde_Imap_Client_Socket::class),
$mailbox,
- $this->createStub(\Psr\Log\LoggerInterface::class),
+ $this->isInstanceOf(\Psr\Log\LoggerInterface::class),
0,
[],
true
- );
- $this->mailboxSync->expects($this->once())
- ->method('syncStats')
- ->with($this->createStub(\Horde_Imap_Client_Socket::class), $mailbox);
+ )
+ ->willReturn(new \OCA\Mail\Protocol\SyncResult());
$response = $this->syncService->syncMailbox(
$account,
diff --git a/tests/Unit/SetupChecks/MailConnectionPerformanceTest.php b/tests/Unit/SetupChecks/MailConnectionPerformanceTest.php
index 96a03a1049..9a3fd986dd 100644
--- a/tests/Unit/SetupChecks/MailConnectionPerformanceTest.php
+++ b/tests/Unit/SetupChecks/MailConnectionPerformanceTest.php
@@ -17,7 +17,7 @@
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\ProvisioningMapper;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Protocol\ProtocolFactory;
use OCA\Mail\SetupChecks\MailConnectionPerformance;
use OCA\Mail\SetupChecks\MicroTime;
use OCP\IL10N;
@@ -31,7 +31,7 @@ class MailConnectionPerformanceTest extends TestCase {
private TestLogger $logger;
private ProvisioningMapper&MockObject $provisioningMapper;
private MailAccountMapper&MockObject $accountMapper;
- private IMAPClientFactory&MockObject $clientFactory;
+ private ProtocolFactory&MockObject $protocolFactory;
private MicroTime&MockObject $microtime;
private MailConnectionPerformance $check;
@@ -42,7 +42,7 @@ protected function setUp(): void {
$this->logger = new TestLogger();
$this->provisioningMapper = $this->createMock(ProvisioningMapper::class);
$this->accountMapper = $this->createMock(MailAccountMapper::class);
- $this->clientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->protocolFactory = $this->createMock(ProtocolFactory::class);
$this->microtime = $this->createMock(MicroTime::class);
$this->check = new MailConnectionPerformance(
@@ -50,7 +50,7 @@ protected function setUp(): void {
$this->logger,
$this->provisioningMapper,
$this->accountMapper,
- $this->clientFactory,
+ $this->protocolFactory,
$this->microtime
);
}
@@ -116,8 +116,8 @@ public function testConnectionSuccess(): void {
->with('INBOX')
->willReturn([]);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with(new Account($account))
->willReturn($client);
@@ -166,8 +166,8 @@ public function testConnectionWarning(): void {
->with('INBOX')
->willReturn([]);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with(new Account($account))
->willReturn($client);
@@ -207,8 +207,8 @@ public function testConnectionFailure(): void {
->with(42)
->willReturn($account);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with(new Account($account))
->willReturn($client);
@@ -244,8 +244,8 @@ public function testClientFailure(): void {
->method('findById')
->with(42)
->willReturn($account);
- $this->clientFactory->expects($this->once())
- ->method('getClient')
+ $this->protocolFactory->expects($this->once())
+ ->method('imapClient')
->with(new Account($account))
->willThrowException(new ServiceException('Something about decryption'));