Skip to content

Commit

Permalink
feat: move messages to junk folder
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Kesselberg <[email protected]>
  • Loading branch information
kesselb committed Aug 7, 2023
1 parent 2507be5 commit cc52e74
Show file tree
Hide file tree
Showing 18 changed files with 751 additions and 8 deletions.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Positive:
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]></description>
<version>3.4.0-alpha.2</version>
<version>3.4.0-alpha.3</version>
<licence>agpl</licence>
<author>Greta Doçi</author>
<author homepage="https://github.com/nextcloud/groupware">Nextcloud Groupware Team</author>
Expand Down
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion lib/Controller/AccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
);
Expand Down
11 changes: 11 additions & 0 deletions lib/Db/MailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -181,6 +185,9 @@ class MailAccount extends Entity {
/** @var int|null */
protected $trashRetentionDays;

protected ?int $junkMailboxId = null;
protected bool $moveJunk = false;

/**
* @param array $params
*/
Expand Down Expand Up @@ -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');
}

/**
Expand Down Expand Up @@ -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())) {
Expand Down
9 changes: 9 additions & 0 deletions lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
102 changes: 102 additions & 0 deletions lib/Listener/MoveJunkListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Daniel Kesselberg <[email protected]>
*
* @author Daniel Kesselberg <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>
*
*/

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<Event|MessageFlaggedEvent>
*/
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(),
]);
}
}
}
}
52 changes: 52 additions & 0 deletions lib/Migration/Version3001Date20230307113544.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Daniel Kesselberg <[email protected]>
*
* @author Daniel Kesselberg <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Mail\Migration;

use 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;
}
}
32 changes: 32 additions & 0 deletions src/components/AccountDefaultsSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
</p>

<MailboxInlinePicker v-model="archiveMailbox" :account="account" :disabled="saving" />

<p>
{{ t('mail', 'Junk messages are saved in:') }}
</p>
<MailboxInlinePicker v-model="junkMailbox" :account="account" :disabled="saving" />
</div>
</template>

Expand Down Expand Up @@ -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
}
},
},
},
}
</script>
Expand Down
7 changes: 6 additions & 1 deletion src/components/AccountSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<AppSettingsSection id="default-folders" :title=" t('mail', 'Default folders')">
<p class="settings-hint">
{{
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.')
}}
</p>
<AccountDefaultsSettings :account="account" />
Expand All @@ -65,6 +65,9 @@
</p>
<TrashRetentionSettings :account="account" />
</AppSettingsSection>
<AppSettingsSection id="junk-settings" :title="t('mail', 'Junk settings')">
<JunkSettings :account="account" />
</AppSettingsSection>
<AppSettingsSection
v-if="account"
id="out-of-office-replies"
Expand Down Expand Up @@ -128,6 +131,7 @@ import SieveFilterForm from './SieveFilterForm'
import OutOfOfficeForm from './OutOfOfficeForm'
import CertificateSettings from './CertificateSettings'
import TrashRetentionSettings from './TrashRetentionSettings'
import JunkSettings from './JunkSettings'
export default {
name: 'AccountSettings',
Expand All @@ -145,6 +149,7 @@ export default {
OutOfOfficeForm,
CertificateSettings,
TrashRetentionSettings,
JunkSettings,
},
props: {
account: {
Expand Down
26 changes: 24 additions & 2 deletions src/components/Envelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -586,8 +586,30 @@ export default {
onToggleSeen() {
this.$store.dispatch('toggleEnvelopeSeen', { envelope: this.data })
},
onToggleJunk() {
this.$store.dispatch('toggleEnvelopeJunk', this.data)
async onToggleJunk() {
const removeEnvelope = await this.$store.dispatch('moveEnvelopeToJunk', this.data)
/**
* 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 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.data.databaseId)
}
await this.$store.dispatch('toggleEnvelopeJunk', {
envelope: this.data,
removeEnvelope,
})
},
async onDelete() {
// Remove from selection first
Expand Down
Loading

0 comments on commit cc52e74

Please sign in to comment.