From 45c5ab9277ba50b2499ca05e7bf3e568d3ea1a44 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg Date: Tue, 7 Mar 2023 21:56:18 +0100 Subject: [PATCH] feat: move messages to junk folder Signed-off-by: Daniel Kesselberg --- appinfo/info.xml | 2 +- lib/AppInfo/Application.php | 2 + lib/Controller/AccountsController.php | 11 +- lib/Db/MailAccount.php | 11 + ...xesSynchronizedSpecialMailboxesUpdater.php | 9 + lib/Listener/MoveJunkListener.php | 102 ++++++++ .../Version3001Date20230307113544.php | 52 ++++ src/components/AccountDefaultsSettings.vue | 32 +++ src/components/AccountSettings.vue | 7 +- src/components/Envelope.vue | 26 +- src/components/JunkSettings.vue | 75 ++++++ src/components/MenuEnvelope.vue | 26 +- src/components/NavigationMailbox.vue | 4 + src/store/actions.js | 34 ++- src/store/getters.js | 6 + src/tests/unit/store/actions.spec.js | 85 +++++++ src/tests/unit/store/getters.spec.js | 49 ++++ tests/Integration/Db/MailAccountTest.php | 4 + tests/Unit/Listener/MoveJunkListenerTest.php | 226 ++++++++++++++++++ 19 files changed, 755 insertions(+), 8 deletions(-) create mode 100644 lib/Listener/MoveJunkListener.php create mode 100644 lib/Migration/Version3001Date20230307113544.php create mode 100644 src/components/JunkSettings.vue create mode 100644 tests/Unit/Listener/MoveJunkListenerTest.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 6fdbee9847..2a3df1ff08 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Positive: Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/). ]]> - 3.4.0-alpha.2 + 3.4.0-alpha.3 agpl Greta Doçi Nextcloud Groupware Team diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 018728b7a3..39216789b3 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -60,6 +60,7 @@ use OCA\Mail\Listener\MailboxesSynchronizedSpecialMailboxesUpdater; use OCA\Mail\Listener\MessageCacheUpdaterListener; use OCA\Mail\Listener\MessageKnownSinceListener; +use OCA\Mail\Listener\MoveJunkListener; use OCA\Mail\Listener\NewMessageClassificationListener; use OCA\Mail\Listener\OauthTokenRefreshListener; use OCA\Mail\Listener\SaveSentMessageListener; @@ -123,6 +124,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(MessageFlaggedEvent::class, MessageCacheUpdaterListener::class); $context->registerEventListener(MessageFlaggedEvent::class, SpamReportListener::class); $context->registerEventListener(MessageFlaggedEvent::class, HamReportListener::class); + $context->registerEventListener(MessageFlaggedEvent::class, MoveJunkListener::class); $context->registerEventListener(MessageDeletedEvent::class, MessageCacheUpdaterListener::class); $context->registerEventListener(MessageSentEvent::class, AddressCollectionListener::class); $context->registerEventListener(MessageSentEvent::class, FlagRepliedMessageListener::class); diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index ff9ae9122f..e5a4c895f3 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -242,7 +242,9 @@ public function patchAccount(int $id, int $trashMailboxId = null, int $archiveMailboxId = null, bool $signatureAboveQuote = null, - int $trashRetentionDays = null): JSONResponse { + int $trashRetentionDays = null, + int $junkMailboxId = null, + bool $moveJunk = null): JSONResponse { $account = $this->accountService->find($this->currentUserId, $id); $dbAccount = $account->getMailAccount(); @@ -279,6 +281,13 @@ public function patchAccount(int $id, // Passing 0 (or lower) disables retention $dbAccount->setTrashRetentionDays($trashRetentionDays <= 0 ? null : $trashRetentionDays); } + if ($junkMailboxId !== null) { + $this->mailManager->getMailbox($this->currentUserId, $junkMailboxId); + $dbAccount->setJunkMailboxId($junkMailboxId); + } + if ($moveJunk !== null) { + $dbAccount->setMoveJunk($moveJunk); + } return new JSONResponse( $this->accountService->save($dbAccount) ); diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index ce6422eb1c..b3fd9122e3 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -110,6 +110,10 @@ * @method void setQuotaPercentage(int $quota); * @method int|null getTrashRetentionDays() * @method void setTrashRetentionDays(int|null $trashRetentionDays) + * @method int|null getJunkMailboxId() + * @method void setJunkMailboxId(?int $id) + * @method bool isMoveJunk() + * @method void setMoveJunk(bool $moveJunk) */ class MailAccount extends Entity { public const SIGNATURE_MODE_PLAIN = 0; @@ -181,6 +185,9 @@ class MailAccount extends Entity { /** @var int|null */ protected $trashRetentionDays; + protected ?int $junkMailboxId = null; + protected bool $moveJunk = false; + /** * @param array $params */ @@ -254,6 +261,8 @@ public function __construct(array $params = []) { $this->addType('smimeCertificateId', 'integer'); $this->addType('quotaPercentage', 'integer'); $this->addType('trashRetentionDays', 'integer'); + $this->addType('junkMailboxId', 'integer'); + $this->addType('moveJunk', 'boolean'); } /** @@ -285,6 +294,8 @@ public function toJson() { 'smimeCertificateId' => $this->getSmimeCertificateId(), 'quotaPercentage' => $this->getQuotaPercentage(), 'trashRetentionDays' => $this->getTrashRetentionDays(), + 'junkMailboxId' => $this->getJunkMailboxId(), + 'moveJunk' => ($this->isMoveJunk() === true), ]; if (!is_null($this->getOutboundHost())) { diff --git a/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php b/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php index 34b8d5718d..41bfb89728 100644 --- a/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php +++ b/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php @@ -112,6 +112,15 @@ public function handle(Event $event): void { $mailAccount->setArchiveMailboxId(null); } } + if ($mailAccount->getJunkMailboxId() === null || !array_key_exists($mailAccount->getJunkMailboxId(), $mailboxes)) { + try { + $junkMailbox = $this->findSpecial($mailboxes, 'junk'); + $mailAccount->setJunkMailboxId($junkMailbox->getId()); + } catch (DoesNotExistException) { + $this->logger->info("Account " . $account->getId() . " does not have an junk mailbox"); + $mailAccount->setJunkMailboxId(null); + } + } $this->mailAccountMapper->update($mailAccount); } diff --git a/lib/Listener/MoveJunkListener.php b/lib/Listener/MoveJunkListener.php new file mode 100644 index 0000000000..44dfeb8aed --- /dev/null +++ b/lib/Listener/MoveJunkListener.php @@ -0,0 +1,102 @@ + + * + * @author Daniel Kesselberg + * + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Listener; + +use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Events\MessageFlaggedEvent; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\ServiceException; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * @template-implements IEventListener + */ +class MoveJunkListener implements IEventListener { + public function __construct( + private IMailManager $mailManager, + private LoggerInterface $logger, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof MessageFlaggedEvent || $event->getFlag() !== '$junk') { + return; + } + + $account = $event->getAccount(); + $mailAccount = $account->getMailAccount(); + + if (!$mailAccount->isMoveJunk()) { + return; + } + + $mailbox = $event->getMailbox(); + + if ($event->isSet() && $mailAccount->getJunkMailboxId() !== $mailbox->getId()) { + try { + $junkMailbox = $this->mailManager->getMailbox($account->getUserId(), $mailAccount->getJunkMailboxId()); + } catch (ClientException) { + $this->logger->debug('move to junk enabled, but junk mailbox does not exist. account_id: {account_id}, junk_mailbox_id: {junk_mailbox_id}', [ + 'account_id' => $account->getId(), + 'junk_mailbox_id' => $mailAccount->getJunkMailboxId(), + ]); + return; + } + + try { + $this->mailManager->moveMessage( + $account, + $mailbox->getName(), + $event->getUid(), + $account, + $junkMailbox->getName(), + ); + } catch (ServiceException $e) { + $this->logger->error('move message to junk mailbox failed. account_id: {account_id}', [ + 'exception' => $e, + 'account_id' => $account->getId(), + ]); + } + } elseif (!$event->isSet() && 'INBOX' !== $mailbox->getName()) { + try { + $this->mailManager->moveMessage( + $account, + $mailbox->getName(), + $event->getUid(), + $account, + 'INBOX', + ); + } catch (ServiceException $e) { + $this->logger->error('move message to inbox failed. account_id: {account_id}', [ + 'exception' => $e, + 'account_id' => $account->getId(), + ]); + } + } + } +} diff --git a/lib/Migration/Version3001Date20230307113544.php b/lib/Migration/Version3001Date20230307113544.php new file mode 100644 index 0000000000..3e072cf1f4 --- /dev/null +++ b/lib/Migration/Version3001Date20230307113544.php @@ -0,0 +1,52 @@ + + * + * @author Daniel Kesselberg + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version3001Date20230307113544 extends SimpleMigrationStep { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $accountsTable = $schema->getTable('mail_accounts'); + $accountsTable->addColumn('junk_mailbox_id', 'integer', [ + 'notnull' => false, + 'default' => null, + 'length' => 20, + ]); + $accountsTable->addColumn('move_junk', 'boolean', [ + 'notnull' => false, + 'default' => false, + ]); + + return $schema; + } +} diff --git a/src/components/AccountDefaultsSettings.vue b/src/components/AccountDefaultsSettings.vue index 9f002a44ae..0aee963216 100644 --- a/src/components/AccountDefaultsSettings.vue +++ b/src/components/AccountDefaultsSettings.vue @@ -41,6 +41,11 @@

+ +

+ {{ t('mail', 'Junk messages are saved in:') }} +

+ @@ -173,6 +178,33 @@ export default { } }, }, + junkMailbox: { + get() { + const mb = this.$store.getters.getMailbox(this.account.junkMailboxId) + if (!mb) { + return + } + return mb.databaseId + }, + async set(junkMailboxId) { + logger.debug('setting junk mailbox to ' + junkMailboxId) + this.saving = true + try { + await this.$store.dispatch('patchAccount', { + account: this.account, + data: { + junkMailboxId, + }, + }) + } catch (error) { + logger.error('could not set junk mailbox', { + error, + }) + } finally { + this.saving = false + } + }, + }, }, } diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index d1e22377f6..9c923a84c5 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -54,7 +54,7 @@

{{ - t('mail', 'The folders to use for drafts, sent messages, deleted messages and archived messages.') + t('mail', 'The folders to use for drafts, sent messages, deleted messages, archived messages and junk messages.') }}

@@ -65,6 +65,9 @@

+ + + + - + - @author 2023 Daniel Kesselberg + - + - @license AGPL-3.0-or-later + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see . + --> + + + + diff --git a/src/components/MenuEnvelope.vue b/src/components/MenuEnvelope.vue index aad5b4461e..84aa5d7507 100644 --- a/src/components/MenuEnvelope.vue +++ b/src/components/MenuEnvelope.vue @@ -381,8 +381,30 @@ export default { onToggleSeen() { this.$store.dispatch('toggleEnvelopeSeen', { envelope: this.envelope }) }, - onToggleJunk() { - this.$store.dispatch('toggleEnvelopeJunk', this.envelope) + async onToggleJunk() { + const removeEnvelope = await this.$store.dispatch('moveEnvelopeToJunk', this.envelope) + + /** + * moveEnvelopeToJunk returns true if the envelope should be moved to a different mailbox. + * + * Our backend (MessageMapper.move) implemented move as copy and delete. + * The message is copied to another mailbox and gets a new UID; the message in the current folder is deleted. + * + * Trigger the delete event here to open the next envelope and remove the current envelope from the list. + * The delete event bubbles up to MailboxThread.deleteMessage and is forwarded to Mailbox.onDelete to the actual implementation. + * + * In Mailbox.onDelete, fetchNextEnvelopes requires the current envelope to find the next envelope. + * Therefore, it must run before removing the envelope. + */ + + if (removeEnvelope) { + await this.$emit('delete', this.envelope.databaseId) + } + + await this.$store.dispatch('toggleEnvelopeJunk', { + envelope: this.envelope, + removeEnvelope, + }) }, toggleSelected() { this.$emit('update:selected') diff --git a/src/components/NavigationMailbox.vue b/src/components/NavigationMailbox.vue index ec1c6dc719..81e16a6aa5 100644 --- a/src/components/NavigationMailbox.vue +++ b/src/components/NavigationMailbox.vue @@ -56,6 +56,8 @@ :size="20" /> + { // Change immediately and switch back on error const oldState = envelope.flags.$junk @@ -976,6 +976,10 @@ export default { value: oldState, }) + if (removeEnvelope) { + commit('removeEnvelope', { id: envelope.databaseId }) + } + try { await setEnvelopeFlags(envelope.databaseId, { $junk: !oldState, @@ -984,6 +988,10 @@ export default { } catch (error) { console.error('could not toggle message junk state', error) + if (removeEnvelope) { + commit('addEnvelope', envelope) + } + // Revert change commit('flagEnvelope', { envelope, @@ -1361,4 +1369,28 @@ export default { commit('patchAccount', { account, data: { smimeCertificateId } }) }) }, + + /** + * Should the envelope moved to the junk (or back to inbox) + * + * @param {object} context Vuex store context + * @param {object} context.getters Vuex store getters + * @param {object} envelope envelope object@ + * @return {boolean} + */ + async moveEnvelopeToJunk({ getters }, envelope) { + const account = getters.getAccount(envelope.accountId) + if (account.moveJunk === false) { + return false + } + + if (!envelope.flags.$junk) { + // move message to junk + return account.junkMailboxId && envelope.mailboxId !== account.junkMailboxId + } + + const inbox = getters.getInbox(account.id) + // move message to inbox + return inbox && envelope.mailboxId !== inbox.databaseId + }, } diff --git a/src/store/getters.js b/src/store/getters.js index ec8ac153fb..01eecc1bab 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -144,4 +144,10 @@ export const getters = { }, getNcVersion: (state) => state.preferences?.ncVersion, getAppVersion: (state) => state.preferences?.version, + findMailboxBySpecialRole: (state, getters) => (accountId, specialRole) => { + return getters.getMailboxes(accountId).find(mailbox => mailbox.specialRole === specialRole) + }, + getInbox: (state, getters) => (accountId) => { + return getters.findMailboxBySpecialRole(accountId, 'inbox') + }, } diff --git a/src/tests/unit/store/actions.spec.js b/src/tests/unit/store/actions.spec.js index 1acc9f3abc..5853f69ab2 100644 --- a/src/tests/unit/store/actions.spec.js +++ b/src/tests/unit/store/actions.spec.js @@ -47,6 +47,8 @@ describe('Vuex store actions', () => { dispatch: jest.fn(), getters: { accounts: [], + getAccount: jest.fn(), + getInbox: jest.fn(), getMailbox: jest.fn(), getMailboxes: jest.fn(), getEnvelope: jest.fn(), @@ -513,4 +515,87 @@ describe('Vuex store actions', () => { expect(NotificationService.showNewMessagesNotification).toHaveBeenCalled }) }) + + it('should move message to junk', async() => { + context.getters.getAccount.mockReturnValueOnce({ + moveJunk: true, + junkMailboxId: 10 + }) + + const removeEnvelope = await actions.moveEnvelopeToJunk(context, { + flags: { + $junk: false + }, + mailboxId: 1 + }) + + expect(removeEnvelope).toBeTruthy() + }) + + it('should move message to junk, no mailbox configured', async() => { + context.getters.getAccount.mockReturnValueOnce({ + moveJunk: true, + junkMailboxId: null + }) + + const removeEnvelope = await actions.moveEnvelopeToJunk(context, { + flags: { + $junk: false + }, + mailboxId: 1 + }) + + expect(removeEnvelope).toBeFalsy() + }) + + it('should move message to inbox', async() => { + context.getters.getAccount.mockReturnValueOnce({ + moveJunk: true, + junkMailboxId: 10 + }) + context.getters.getInbox.mockReturnValueOnce({ + databaseId: 1 + }) + + const removeEnvelope = await actions.moveEnvelopeToJunk(context, { + flags: { + $junk: true + }, + mailboxId: 10 + }) + + expect(removeEnvelope).toBeTruthy() + }) + + it('should move message to inbox, inbox not found', async() => { + context.getters.getAccount.mockReturnValueOnce({ + moveJunk: true, + junkMailboxId: 10 + }) + context.getters.getInbox.mockReturnValueOnce(undefined) + + const removeEnvelope = await actions.moveEnvelopeToJunk(context, { + flags: { + $junk: true + }, + mailboxId: 10 + }) + + expect(removeEnvelope).toBeFalsy() + }) + + it('should not move messages', async() => { + context.getters.getAccount.mockReturnValueOnce({ + moveJunk: false + }) + + const removeEnvelope = await actions.moveEnvelopeToJunk(context, { + flags: { + $junk: true + }, + mailboxId: 10 + }) + + expect(removeEnvelope).toBeFalsy() + }) }) diff --git a/src/tests/unit/store/getters.spec.js b/src/tests/unit/store/getters.spec.js index f8af1ecf61..419c44c29a 100644 --- a/src/tests/unit/store/getters.spec.js +++ b/src/tests/unit/store/getters.spec.js @@ -199,4 +199,53 @@ describe('Vuex store getters', () => { const envelopesB = getters.getEnvelopesByThreadRootId('345-678-901') expect(envelopesB.length).toEqual(0) }) + + it('find mailbox by special role: inbox', () => { + const mockedGetters = { + getMailboxes: () => [ + { + name: 'Test', + specialRole: 0, + }, + { + name: 'INBOX', + specialRole: 'inbox', + }, + { + name: 'Trash', + specialRole: 'trash', + } + ] + } + + const result = getters.findMailboxBySpecialRole(state, mockedGetters)('100', 'inbox') + + expect(result).toEqual({ + name: 'INBOX', + specialRole: 'inbox' + }); + }) + + it('find mailbox by special role: undefined', () => { + const mockedGetters = { + getMailboxes: () => [ + { + name: 'Test', + specialRole: 0, + }, + { + name: 'INBOX', + specialRole: 'inbox', + }, + { + name: 'Trash', + specialRole: 'trash', + } + ] + } + + const result = getters.findMailboxBySpecialRole(state, mockedGetters)('100', 'drafts') + + expect(result).toEqual(undefined); + }) }) diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php index 323f616d70..51b9d70181 100644 --- a/tests/Integration/Db/MailAccountTest.php +++ b/tests/Integration/Db/MailAccountTest.php @@ -78,6 +78,8 @@ public function testToAPI() { 'smimeCertificateId' => null, 'quotaPercentage' => 10, 'trashRetentionDays' => 60, + 'junkMailboxId' => null, + 'moveJunk' => false ], $a->toJson()); } @@ -111,6 +113,8 @@ public function testMailAccountConstruct() { 'smimeCertificateId' => null, 'quotaPercentage' => null, 'trashRetentionDays' => 60, + 'junkMailboxId' => null, + 'moveJunk' => false, ]; $a = new MailAccount($expected); // TODO: fix inconsistency diff --git a/tests/Unit/Listener/MoveJunkListenerTest.php b/tests/Unit/Listener/MoveJunkListenerTest.php new file mode 100644 index 0000000000..55f505ab0b --- /dev/null +++ b/tests/Unit/Listener/MoveJunkListenerTest.php @@ -0,0 +1,226 @@ + + * + * @author Daniel Kesselberg + * + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace Unit\Listener; + +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\Events\MessageFlaggedEvent; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\ServiceException; +use OCA\Mail\Listener\MoveJunkListener; +use Psr\Log\LoggerInterface; +use Psr\Log\Test\TestLogger; + +class MoveJunkListenerTest extends TestCase { + private IMailManager $mailManager; + private LoggerInterface $logger; + private MoveJunkListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->mailManager = $this->createMock(IMailManager::class); + $this->logger = new TestLogger(); + + $this->listener = new MoveJunkListener( + $this->mailManager, + $this->logger + ); + } + + public function testIgnoreOtherFlags(): void { + $event = $this->createMock(MessageFlaggedEvent::class); + $event->method('getFlag') + ->willReturn('test'); + + $event->expects($this->never()) + ->method('getAccount'); + + $this->listener->handle($event); + } + + public function testMoveJunkDisabled(): void { + $mailAccount = new MailAccount(); + $mailAccount->setMoveJunk(false); + $account = new Account($mailAccount); + + $event = $this->createMock(MessageFlaggedEvent::class); + $event->method('getFlag') + ->willReturn('$junk'); + $event->method('getAccount') + ->willReturn($account); + + $event->expects($this->never()) + ->method('getMailbox'); + + $this->listener->handle($event); + } + + public function testMoveJunkMailboxNotFound(): void { + $mailAccount = new MailAccount(); + $mailAccount->setJunkMailboxId(200); + $mailAccount->setMoveJunk(true); + $mailAccount->setUserId('bob'); + $account = new Account($mailAccount); + + $mailbox = new Mailbox(); + $mailbox->setId(100); + + $this->mailManager->method('getMailbox') + ->willThrowException(new ClientException('Computer says no')); + + $event = new MessageFlaggedEvent( + $account, + $mailbox, + 100, + '$junk', + true + ); + + $this->listener->handle($event); + + $this->assertCount(1, $this->logger->records); + } + + public function testMoveJunkAlreadyInJunk(): void { + $mailAccount = new MailAccount(); + $mailAccount->setJunkMailboxId(200); + $mailAccount->setMoveJunk(true); + $mailAccount->setUserId('bob'); + $account = new Account($mailAccount); + + $mailbox = new Mailbox(); + $mailbox->setId(200); + + $this->mailManager->expects($this->never()) + ->method('moveMessage'); + + $event = new MessageFlaggedEvent( + $account, + $mailbox, + 100, + '$junk', + true + ); + + $this->listener->handle($event); + } + + public function testMoveJunkFailed(): void { + $mailAccount = new MailAccount(); + $mailAccount->setJunkMailboxId(200); + $mailAccount->setMoveJunk(true); + $mailAccount->setUserId('bob'); + $account = new Account($mailAccount); + + $mailbox = new Mailbox(); + $mailbox->setId(100); + $mailbox->setName('INBOX'); + + $junkMailbox = new Mailbox(); + $junkMailbox->setId(200); + $junkMailbox->setName('Junk'); + + $this->mailManager->method('getMailbox') + ->willReturn($junkMailbox); + + $this->mailManager->method('moveMessage') + ->willThrowException(new ServiceException('Computer says no')); + + $event = new MessageFlaggedEvent( + $account, + $mailbox, + 100, + '$junk', + true + ); + + $this->listener->handle($event); + + $this->assertCount(1, $this->logger->records); + } + + public function testMoveJunkAlreadyInInbox(): void { + $mailAccount = new MailAccount(); + $mailAccount->setJunkMailboxId(200); + $mailAccount->setMoveJunk(true); + $mailAccount->setUserId('bob'); + $account = new Account($mailAccount); + + $mailbox = new Mailbox(); + $mailbox->setId(100); + $mailbox->setName('INBOX'); + + $this->mailManager->expects($this->never()) + ->method('moveMessage'); + + $event = new MessageFlaggedEvent( + $account, + $mailbox, + 100, + '$junk', + false + ); + + $this->listener->handle($event); + } + + public function testMoveJunkToInboxFailed(): void { + $mailAccount = new MailAccount(); + $mailAccount->setJunkMailboxId(200); + $mailAccount->setMoveJunk(true); + $mailAccount->setUserId('bob'); + $account = new Account($mailAccount); + + $mailbox = new Mailbox(); + $mailbox->setId(200); + $mailbox->setName('Junk'); + + $junkMailbox = new Mailbox(); + $junkMailbox->setId(200); + $junkMailbox->setName('Junk'); + + $this->mailManager->method('getMailbox') + ->willReturn($junkMailbox); + + $this->mailManager->method('moveMessage') + ->willThrowException(new ServiceException('Computer says no')); + + $event = new MessageFlaggedEvent( + $account, + $mailbox, + 100, + '$junk', + false + ); + + $this->listener->handle($event); + + $this->assertCount(1, $this->logger->records); + } +}