diff --git a/appinfo/info.xml b/appinfo/info.xml index d964279f3d271a169a00dd8ee13f299ed0d6ca71..c7037f13f4cd1f29f71d80bcbfd3b41c094840b1 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ - **🙈 We’re not reinventing the wheel!** Based on the great [Horde](http://horde.org) libraries. - **📬 Want to host your own mail server?** We don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)! ]]></description> - <version>1.3.3</version> + <version>1.4.0</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Roeland Jago Douma</author> diff --git a/img/important.svg b/img/important.svg new file mode 100644 index 0000000000000000000000000000000000000000..026d5dfb7e10ea9aba4d89149bf6955517fd3f84 --- /dev/null +++ b/img/important.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M2 13h7.8a1.4 1.4 0 001.1-.6L14 8l-3.1-4.4A1.4 1.4 0 009.8 3H2l3.4 5z" stroke="#000" stroke-width="2"/></svg> diff --git a/lib/AppInfo/BootstrapSingleton.php b/lib/AppInfo/BootstrapSingleton.php index 1e605b7942ea37929ab32c58acfad8583b2163a3..9d074e3c4e0b15532dbb09e4475ee99373636086 100644 --- a/lib/AppInfo/BootstrapSingleton.php +++ b/lib/AppInfo/BootstrapSingleton.php @@ -36,6 +36,7 @@ use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\MessageFlaggedEvent; use OCA\Mail\Events\MessageSentEvent; +use OCA\Mail\Events\NewMessagesSynchronized; use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\Http\Middleware\ErrorMiddleware; use OCA\Mail\Http\Middleware\ProvisioningMiddleware; @@ -45,6 +46,7 @@ use OCA\Mail\Listener\DraftMailboxCreatorListener; use OCA\Mail\Listener\FlagRepliedMessageListener; use OCA\Mail\Listener\InteractionListener; use OCA\Mail\Listener\MessageCacheUpdaterListener; +use OCA\Mail\Listener\NewMessageClassificationListener; use OCA\Mail\Listener\SaveSentMessageListener; use OCA\Mail\Listener\TrashMailboxCreatorListener; use OCA\Mail\Service\Attachment\AttachmentService; @@ -135,6 +137,7 @@ class BootstrapSingleton { $dispatcher->addServiceListener(MessageSentEvent::class, FlagRepliedMessageListener::class); $dispatcher->addServiceListener(MessageSentEvent::class, InteractionListener::class); $dispatcher->addServiceListener(MessageSentEvent::class, SaveSentMessageListener::class); + $dispatcher->addServiceListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class); $dispatcher->addServiceListener(SaveDraftEvent::class, DraftMailboxCreatorListener::class); } } diff --git a/lib/Db/Message.php b/lib/Db/Message.php index e2c0bbb4bb0ea59c90b97c45527ea931e55c81bb..55e096315447aa6a2e039c1dea83f0eb40a7fbd0 100644 --- a/lib/Db/Message.php +++ b/lib/Db/Message.php @@ -61,6 +61,8 @@ use function in_array; * @method bool getStructureAnalyzed() * @method void setFlagAttachments(?bool $hasAttachments) * @method null|bool getFlagAttachments() + * @method void setFlagImportant(bool $important) + * @method bool getFlagImportant() * @method void setPreviewText(?string $subject) * @method null|string getPreviewText() * @method void setUpdatedAt(int $time) @@ -76,6 +78,7 @@ class Message extends Entity implements JsonSerializable { 'forwarded', 'junk', 'notjunk', + 'important', ]; protected $uid; @@ -94,6 +97,7 @@ class Message extends Entity implements JsonSerializable { protected $updatedAt; protected $structureAnalyzed; protected $flagAttachments; + protected $flagImportant = false; protected $previewText; /** @var AddressList */ @@ -126,6 +130,7 @@ class Message extends Entity implements JsonSerializable { $this->addType('flagNotjunk', 'bool'); $this->addType('structureAnalyzed', 'bool'); $this->addType('flagAttachments', 'bool'); + $this->addType('flagImportant', 'bool'); $this->addType('updatedAt', 'integer'); } @@ -210,6 +215,7 @@ class Message extends Entity implements JsonSerializable { 'draft' => $this->getFlagDraft(), 'forwarded' => $this->getFlagForwarded(), 'hasAttachments' => $this->getFlagAttachments() ?? false, + 'important' => $this->getFlagImportant(), ], 'from' => $this->getFrom()->jsonSerialize(), 'to' => $this->getTo()->jsonSerialize(), diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 1e0bde2adf2c1c016901c51673453b8b2e67a2de..f45ee0d7830d6f247141dc517ce6e9605da4e353 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -104,6 +104,7 @@ class MessageMapper extends QBMapper { $qb1->setValue('flag_forwarded', $qb1->createParameter('flag_forwarded')); $qb1->setValue('flag_junk', $qb1->createParameter('flag_junk')); $qb1->setValue('flag_notjunk', $qb1->createParameter('flag_notjunk')); + $qb1->setValue('flag_important', $qb1->createParameter('flag_important')); $qb2 = $this->db->getQueryBuilder(); $qb2->insert('mail_recipients') @@ -126,6 +127,7 @@ class MessageMapper extends QBMapper { $qb1->setParameter('flag_forwarded', $message->getFlagForwarded(), IQueryBuilder::PARAM_BOOL); $qb1->setParameter('flag_junk', $message->getFlagJunk(), IQueryBuilder::PARAM_BOOL); $qb1->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL); + $qb1->setParameter('flag_important', $message->getFlagImportant(), IQueryBuilder::PARAM_BOOL); $qb1->execute(); diff --git a/lib/Events/NewMessagesSynchronized.php b/lib/Events/NewMessagesSynchronized.php new file mode 100644 index 0000000000000000000000000000000000000000..beda723a111383165491342b9d726b6de69141d9 --- /dev/null +++ b/lib/Events/NewMessagesSynchronized.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Events; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; +use OCP\EventDispatcher\Event; + +class NewMessagesSynchronized extends Event { + + /** @var Account */ + private $account; + + /** @var Mailbox */ + private $mailbox; + + /** @var array|Message[] */ + private $messages; + + /** + * @param Account $account + * @param Mailbox $mailbox + * @param Message[] $messages + */ + public function __construct(Account $account, + Mailbox $mailbox, + array $messages) { + parent::__construct(); + $this->account = $account; + $this->mailbox = $mailbox; + $this->messages = $messages; + } + + public function getAccount(): Account { + return $this->account; + } + + public function getMailbox(): Mailbox { + return $this->mailbox; + } + + /** + * @return Message[] + */ + public function getMessages() { + return $this->messages; + } +} diff --git a/lib/Listener/NewMessageClassificationListener.php b/lib/Listener/NewMessageClassificationListener.php new file mode 100644 index 0000000000000000000000000000000000000000..1605a60b2693247b5c5849cb12c83711ba74419f --- /dev/null +++ b/lib/Listener/NewMessageClassificationListener.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Listener; + +use OCA\Mail\Events\NewMessagesSynchronized; +use OCA\Mail\Service\Classification\MessageClassifier; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class NewMessageClassificationListener implements IEventListener { + + /** @var MessageClassifier */ + private $classifier; + + public function __construct(MessageClassifier $classifier) { + $this->classifier = $classifier; + } + + public function handle(Event $event): void { + if (!($event instanceof NewMessagesSynchronized)) { + return; + } + + foreach ($event->getMessages() as $message) { + if ($this->classifier->isImportant($event->getAccount(), $event->getMailbox(), $message)) { + $message->setFlagImportant(true); + } + } + } +} diff --git a/lib/Migration/Version1040Date20200422130220.php b/lib/Migration/Version1040Date20200422130220.php new file mode 100644 index 0000000000000000000000000000000000000000..17377e460292bee65a89a621bac3716553b629f0 --- /dev/null +++ b/lib/Migration/Version1040Date20200422130220.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1040Date20200422130220 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * + * @return ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $schema->dropTable('mail_messages'); + + return $schema; + } + + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + // Reset locks and sync tokens + $qb1 = $this->connection->getQueryBuilder(); + $updateMailboxes = $qb1->update('mail_mailboxes') + ->set('sync_new_lock', $qb1->createNamedParameter(null)) + ->set('sync_new_token', $qb1->createNamedParameter(null)) + ->set('sync_changed_lock', $qb1->createNamedParameter(null)) + ->set('sync_changed_token', $qb1->createNamedParameter(null)) + ->set('sync_vanished_lock', $qb1->createNamedParameter(null)) + ->set('sync_vanished_token', $qb1->createNamedParameter(null)); + $updateMailboxes->execute(); + + // Clean up some orphaned data + $qb2 = $this->connection->getQueryBuilder(); + $deleteRecipients = $qb2->delete('mail_recipients'); + $deleteRecipients->execute(); + } +} diff --git a/lib/Migration/Version1040Date20200422142920.php b/lib/Migration/Version1040Date20200422142920.php new file mode 100644 index 0000000000000000000000000000000000000000..ee8a39be4dfa16c37c08539470150a7bfeffd886 --- /dev/null +++ b/lib/Migration/Version1040Date20200422142920.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1040Date20200422142920 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $messagesTable = $schema->createTable('mail_messages'); + $messagesTable->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + ]); + $messagesTable->addColumn('uid', 'integer', [ + 'notnull' => true, + 'length' => 4, + ]); + $messagesTable->addColumn('message_id', 'string', [ + 'notnull' => false, + 'length' => 255, + ]); + $messagesTable->addColumn('mailbox_id', 'integer', [ + 'notnull' => true, + 'length' => 20, + ]); + $messagesTable->addColumn('subject', 'string', [ + 'notnull' => true, + 'length' => 255, + 'default' => '', + ]); + $messagesTable->addColumn('sent_at', 'integer', [ + 'notnull' => true, + 'length' => 4, + ]); + $messagesTable->addColumn('flag_answered', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_deleted', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_draft', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_flagged', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_seen', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_forwarded', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_junk', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_notjunk', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('flag_attachments', 'boolean', [ + 'notnull' => false, + ]); + $messagesTable->addColumn('flag_important', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('structure_analyzed', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $messagesTable->addColumn('preview_text', 'string', [ + 'notnull' => false, + 'length' => 255, + ]); + $messagesTable->addColumn('updated_at', 'integer', [ + 'notnull' => false, + 'length' => 4, + ]); + $messagesTable->setPrimaryKey(['id']); + // We allow each UID just once + $messagesTable->addUniqueIndex( + [ + 'uid', + 'mailbox_id', + ], + 'mail_msg_mb_uid_idx' + ); + $messagesTable->addIndex(['sent_at'], 'mail_msg_sent_idx'); + + return $schema; + } +} diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 3b7be1060692c78c73cf634f54e1a3d89f8690cb..3e055f2ccccf8a6f640fa7338c02b441d8b41db1 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -646,6 +646,7 @@ class IMAPMessage implements IMessage, JsonSerializable { $msg->setFlagForwarded(in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags, true)); $msg->setFlagJunk(in_array(Horde_Imap_Client::FLAG_JUNK, $flags, true)); $msg->setFlagNotjunk(in_array(Horde_Imap_Client::FLAG_NOTJUNK, $flags, true)); + $msg->setFlagImportant(false); $msg->setFlagAttachments(false); return $msg; diff --git a/lib/Service/Classification/AClassifier.php b/lib/Service/Classification/AClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..a9d4ec50889f61b6e0c7525ec3028ac4c1fb2f5d --- /dev/null +++ b/lib/Service/Classification/AClassifier.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; + +abstract class AClassifier { + abstract public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool; + + final public function or(AClassifier $next): AClassifier { + return new class($this, $next) extends AClassifier { + /** @var AClassifier */ + private $outer; + + /** @var AClassifier */ + private $next; + + public function __construct(AClassifier $outer, AClassifier $next) { + $this->outer = $outer; + $this->next = $next; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + return $this->outer->isImportant($account, $mailbox, $message) || $this->next->isImportant($account, $mailbox, $message); + } + }; + } +} diff --git a/lib/Service/Classification/MessageClassifier.php b/lib/Service/Classification/MessageClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..8efe202abc26272169c9bf31e9ae8e35e2c5fb0c --- /dev/null +++ b/lib/Service/Classification/MessageClassifier.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; + +class MessageClassifier { + + /** @var AClassifier */ + private $oftenImportantSenderClassifier; + + /** @var AClassifier */ + private $oftenContactedSenderClassifier; + + /** @var AClassifier */ + private $oftenReadSenderClassifier; + + /** @var AClassifier */ + private $oftenRepliedSenderClassifier; + + public function __construct(OftenImportantSenderClassifier $oftenImportantSenderClassifier, + OftenContactedSenderClassifier $oftenContactedSenderClassifier, + OftenReadSenderClassifier $oftenReadSenderClassifier, + OftenRepliedSenderClassifier $oftenRepliedSenderClassifier) { + $this->oftenImportantSenderClassifier = $oftenImportantSenderClassifier; + $this->oftenContactedSenderClassifier = $oftenContactedSenderClassifier; + $this->oftenReadSenderClassifier = $oftenReadSenderClassifier; + $this->oftenRepliedSenderClassifier = $oftenRepliedSenderClassifier; + } + + public function isImportant(Account $account, + Mailbox $mailbox, + Message $message): bool { + return $this->oftenImportantSenderClassifier + ->or($this->oftenContactedSenderClassifier) + ->or($this->oftenReadSenderClassifier) + ->or($this->oftenRepliedSenderClassifier) + ->isImportant($account, $mailbox, $message); + } +} diff --git a/lib/Service/Classification/OftenContactedSenderClassifier.php b/lib/Service/Classification/OftenContactedSenderClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..fd54124e9478d98143466e97248f34ff16e18410 --- /dev/null +++ b/lib/Service/Classification/OftenContactedSenderClassifier.php @@ -0,0 +1,102 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenContactedSenderClassifier extends AClassifier { + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'sent'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getMessagesSentTo($mb, $sender->getEmail()), + $this->getMessagesSentTotal($mb), + 0.1, + true // The very first message is important + ); + } + + private function getMessagesSentTotal(Mailbox $mb): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.id', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getMessagesSentTo(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.id', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } +} diff --git a/lib/Service/Classification/OftenImportantSenderClassifier.php b/lib/Service/Classification/OftenImportantSenderClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..fb95cda04b21ba953d8a7242c53123b04d8e04bd --- /dev/null +++ b/lib/Service/Classification/OftenImportantSenderClassifier.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenImportantSenderClassifier extends AClassifier { + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfImportantMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.3 + ); + } + + private function getNrOfImportantMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_important', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } +} diff --git a/lib/Service/Classification/OftenReadSenderClassifier.php b/lib/Service/Classification/OftenReadSenderClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..7eaf86e5766d044921b3136c71461d0c8a0119bf --- /dev/null +++ b/lib/Service/Classification/OftenReadSenderClassifier.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenReadSenderClassifier extends AClassifier { + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfReadMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.9 + ); + } + + private function getNrOfReadMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_seen', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } +} diff --git a/lib/Service/Classification/OftenRepliedSenderClassifier.php b/lib/Service/Classification/OftenRepliedSenderClassifier.php new file mode 100644 index 0000000000000000000000000000000000000000..2f3f8692703cb4517e09bfe34ab229414ec365b9 --- /dev/null +++ b/lib/Service/Classification/OftenRepliedSenderClassifier.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenRepliedSenderClassifier extends AClassifier { + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfRepliedMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.1 + ); + } + + private function getNrOfRepliedMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_answered', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id', IQueryBuilder::PARAM_INT)) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', $qb->expr()->castColumn('m.mailbox_id', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } +} diff --git a/lib/Service/Classification/SafeRatio.php b/lib/Service/Classification/SafeRatio.php new file mode 100644 index 0000000000000000000000000000000000000000..f3fdf3df02eb011e83f3f7fd9131a76fc3514268 --- /dev/null +++ b/lib/Service/Classification/SafeRatio.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Service\Classification; + +trait SafeRatio { + protected function greater(int $num, + int $of, + float $threshold, + bool $default = false): bool { + if ($of === 0) { + return $default; + } + return $num / $of > $threshold; + } +} diff --git a/lib/Service/Sync/ImapToDbSynchronizer.php b/lib/Service/Sync/ImapToDbSynchronizer.php index c0f19e96a86a0d4def19ad37ce3fd9a973ea2508..4378270c6e735b8d4026c5e7f8b7b586a77d032f 100644 --- a/lib/Service/Sync/ImapToDbSynchronizer.php +++ b/lib/Service/Sync/ImapToDbSynchronizer.php @@ -31,6 +31,7 @@ use OCA\Mail\Account; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\MessageMapper as DatabaseMessageMapper; +use OCA\Mail\Events\NewMessagesSynchronized; use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\IncompleteSyncException; use OCA\Mail\Exception\MailboxLockedException; @@ -43,6 +44,7 @@ use OCA\Mail\IMAP\Sync\Synchronizer; use OCA\mail\lib\Exception\UidValidityChangedException; use OCA\Mail\Model\IMAPMessage; use OCA\Mail\Support\PerformanceLogger; +use OCP\EventDispatcher\IEventDispatcher; use OCP\ILogger; use Throwable; use function array_chunk; @@ -71,6 +73,9 @@ class ImapToDbSynchronizer { /** @var Synchronizer */ private $synchronizer; + /** @var IEventDispatcher */ + private $dispatcher; + /** @var PerformanceLogger */ private $performanceLogger; @@ -83,6 +88,7 @@ class ImapToDbSynchronizer { MailboxMapper $mailboxMapper, DatabaseMessageMapper $messageMapper, Synchronizer $synchronizer, + IEventDispatcher $dispatcher, PerformanceLogger $performanceLogger, ILogger $logger) { $this->dbMapper = $dbMapper; @@ -91,6 +97,7 @@ class ImapToDbSynchronizer { $this->mailboxMapper = $mailboxMapper; $this->messageMapper = $messageMapper; $this->synchronizer = $synchronizer; + $this->dispatcher = $dispatcher; $this->performanceLogger = $performanceLogger; $this->logger = $logger; } @@ -275,9 +282,17 @@ class ImapToDbSynchronizer { $perf->step('get new messages via Horde'); foreach (array_chunk($response->getNewMessages(), 500) as $chunk) { - $this->dbMapper->insertBulk(...array_map(function (IMAPMessage $imapMessage) use ($mailbox) { + $dbMessages = array_map(function (IMAPMessage $imapMessage) use ($mailbox) { return $imapMessage->toDbMessage($mailbox->getId()); - }, $chunk)); + }, $chunk); + + $this->dispatcher->dispatch( + NewMessagesSynchronized::class, + new NewMessagesSynchronized($account, $mailbox, $dbMessages) + ); + $perf->step('classified a chunk of new messages'); + + $this->dbMapper->insertBulk(...$dbMessages); } $perf->step('persist new messages'); diff --git a/package-lock.json b/package-lock.json index 6b32d9bcc865c38169543d718409c6d13c165218..ade7787f67209b079d59a8b9da015504761146d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11360,6 +11360,12 @@ "simple-concat": "^1.0.0" } }, + "simple-html-tokenizer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", + "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", + "dev": true + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -11865,6 +11871,17 @@ "has-flag": "^3.0.0" } }, + "svg-inline-loader": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz", + "integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "object-assign": "^4.0.1", + "simple-html-tokenizer": "^0.1.1" + } + }, "svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", diff --git a/package.json b/package.json index caf30f00615e713727ca6f02009056cc9bac70ef..adc59a250e1633dd2e6397cea3f79560c29c5031 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "sass-loader": "^8.0.2", "sinon": "^9.0.2", "sinon-chai": "^3.4.0", + "svg-inline-loader": "^0.8.2", "url-loader": "^4.1.0", "vue-loader": "^15.9.1", "vue-server-renderer": "^2.6.11", diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index 5b76fa32a7ba983d8a47e15406230f8229bfb373..a8073649eed7e18d5dc6274ddb1a677150abd99b 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -11,6 +11,13 @@ :data-starred="data.flags.flagged ? 'true' : 'false'" @click.prevent="onToggleFlagged" ></div> + <div + v-if="data.flags.important" + class="app-content-list-item-star icon-important" + :data-starred="data.flags.important ? 'true' : 'false'" + @click.prevent="onToggleImportant" + v-html="importantSvg" + ></div> <div class="app-content-list-item-icon"> <Avatar :display-name="addresses" :email="avatarEmail" /> </div> @@ -32,6 +39,9 @@ <ActionButton icon="icon-starred" @click.prevent="onToggleFlagged">{{ data.flags.flagged ? t('mail', 'Unfavorite') : t('mail', 'Favorite') }}</ActionButton> + <ActionButton icon="icon-info" @click.prevent="onToggleImportant">{{ + data.flags.important ? t('mail', 'Mark unimportant') : t('mail', 'Mark important') + }}</ActionButton> <ActionButton icon="icon-mail" @click.prevent="onToggleSeen">{{ data.flags.unseen ? t('mail', 'Mark read') : t('mail', 'Mark unread') }}</ActionButton> @@ -44,6 +54,7 @@ import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import Moment from './Moment' +import importantSvg from '../../img/important.svg' import Avatar from './Avatar' import {calculateAccountColor} from '../util/AccountColor' @@ -66,6 +77,11 @@ export default { required: true, }, }, + data() { + return { + importantSvg, + } + }, computed: { accountColor() { return calculateAccountColor(this.$store.getters.getAccount(this.data.accountId).emailAddress) @@ -134,6 +150,9 @@ export default { onToggleFlagged() { this.$store.dispatch('toggleEnvelopeFlagged', this.data) }, + onToggleImportant() { + this.$store.dispatch('toggleEnvelopeImportant', this.data) + }, onToggleSeen() { this.$store.dispatch('toggleEnvelopeSeen', this.data) }, @@ -158,6 +177,18 @@ export default { z-index: 1; } +.app-content-list-item-star.icon-important { + left: 7px; + top: 13px; + opacity: 1; + &:hover { + opacity: 0.5; + } + ::v-deep path { + fill: #ffcc00; + stroke: var(--color-main-background); + } +} .app-content-list-item.unseen { font-weight: bold; } diff --git a/src/store/actions.js b/src/store/actions.js index 12d586eb18b78b96401a02f9cf58c2c2b0ca6707..6063c9ab6d8ada68200fc821abc5d25319fada68 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -493,6 +493,26 @@ export default { }) }) }, + toggleEnvelopeImportant({commit, getters}, envelope) { + // Change immediately and switch back on error + const oldState = envelope.flags.important + commit('flagEnvelope', { + envelope, + flag: 'important', + value: !oldState, + }) + + setEnvelopeFlag(envelope.accountId, envelope.folderId, envelope.id, 'important', !oldState).catch((e) => { + console.error('could not toggle message important state', e) + + // Revert change + commit('flagEnvelope', { + envelope, + flag: 'important', + value: oldState, + }) + }) + }, toggleEnvelopeSeen({commit, getters}, envelope) { // Change immediately and switch back on error const oldState = envelope.flags.unseen diff --git a/tests/Unit/Service/Classification/ClassifierTest.php b/tests/Unit/Service/Classification/ClassifierTest.php new file mode 100644 index 0000000000000000000000000000000000000000..22cc85037e53617f2730a70dfd0388cf9f29ab5f --- /dev/null +++ b/tests/Unit/Service/Classification/ClassifierTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Tests\Unit\Service\Classification; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; +use OCA\Mail\Service\Classification\AClassifier; + +class ClassifierTest extends TestCase { + public function testShortcut(): void { + $c1 = $this->createMock(AClassifier::class); + $c1->method('isImportant')->willReturn(true); + $c2 = $this->createMock(AClassifier::class); + $c2->expects($this->never()) + ->method('isImportant'); + $account = $this->createMock(Account::class); + $mailbox = $this->createMock(Mailbox::class); + $message = $this->createMock(Message::class); + + $result = $c1->or($c2)->isImportant($account, $mailbox, $message); + + $this->assertTrue($result); + } + + public function testOr(): void { + $c1 = $this->createMock(AClassifier::class); + $c2 = $this->createMock(AClassifier::class); + $c2->method('isImportant')->willReturn(true); + $account = $this->createMock(Account::class); + $mailbox = $this->createMock(Mailbox::class); + $message = $this->createMock(Message::class); + + $result = $c1->or($c2)->isImportant($account, $mailbox, $message); + + $this->assertTrue($result); + } + + public function testNone(): void { + $c1 = $this->createMock(AClassifier::class); + $c2 = $this->createMock(AClassifier::class); + $account = $this->createMock(Account::class); + $mailbox = $this->createMock(Mailbox::class); + $message = $this->createMock(Message::class); + + $result = $c1->or($c2)->isImportant($account, $mailbox, $message); + + $this->assertFalse($result); + } +} diff --git a/webpack.common.js b/webpack.common.js index 42a0777c04402ef9d3e8a2735dfcdea0802ea197..010f19b084633f739deca4f74370abac6c39ee3c 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -62,7 +62,7 @@ module.exports = { test: /\.(svg)$/i, use: [ { - loader: 'url-loader' + loader: 'svg-inline-loader' } ], exclude: path.join(__dirname, 'node_modules', '@ckeditor')