diff --git a/appinfo/info.xml b/appinfo/info.xml index ec82f5f1a5..4a50ddb465 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -29,7 +29,7 @@ The rating depends on the installed text processing backend. See [the rating ove Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/). ]]> - 3.5.0-beta.3 + 3.5.0-beta.4 agpl Christoph Wurst Nextcloud Groupware Team diff --git a/lib/Db/Mailbox.php b/lib/Db/Mailbox.php index bf3c24cf13..37862c6118 100644 --- a/lib/Db/Mailbox.php +++ b/lib/Db/Mailbox.php @@ -70,6 +70,8 @@ * @method void setMyAcls(string|null $acls) * @method bool|null isShared() * @method void setShared(bool $shared) + * @method string getNameHash() + * @method void setNameHash(string $nameHash) */ class Mailbox extends Entity implements JsonSerializable { protected $name; @@ -89,6 +91,7 @@ class Mailbox extends Entity implements JsonSerializable { protected $syncInBackground; protected $myAcls; protected $shared; + protected $nameHash; /** * @var int diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php index f7464beaf0..820d2b4d42 100644 --- a/lib/Db/MailboxMapper.php +++ b/lib/Db/MailboxMapper.php @@ -92,7 +92,7 @@ public function find(Account $account, string $name): Mailbox { ->from($this->getTableName()) ->where( $qb->expr()->eq('account_id', $qb->createNamedParameter($account->getId())), - $qb->expr()->eq('name', $qb->createNamedParameter($name)) + $qb->expr()->eq('name_hash', $qb->createNamedParameter(md5($name))) ); try { diff --git a/lib/IMAP/MailboxSync.php b/lib/IMAP/MailboxSync.php index 3ff6375e70..9e41946fda 100644 --- a/lib/IMAP/MailboxSync.php +++ b/lib/IMAP/MailboxSync.php @@ -254,6 +254,7 @@ private function createMailboxFromFolder(Account $account, Folder $folder, ?Hord $mailbox->setSpecialUse(json_encode($folder->getSpecialUse())); $mailbox->setMyAcls($folder->getMyAcls()); $mailbox->setShared($this->isMailboxShared($namespaces, $mailbox)); + $mailbox->setNameHash(md5($folder->getMailbox())); return $this->mailboxMapper->insert($mailbox); } diff --git a/lib/Migration/Version3500Date20231115182612.php b/lib/Migration/Version3500Date20231115182612.php new file mode 100644 index 0000000000..238a4b5563 --- /dev/null +++ b/lib/Migration/Version3500Date20231115182612.php @@ -0,0 +1,115 @@ + + * + * @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\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version3500Date20231115182612 extends SimpleMigrationStep { + + public function __construct( + private IDBConnection $connection + ) { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $mailboxesTable = $schema->getTable('mail_mailboxes'); + if (!$mailboxesTable->hasColumn('name_hash')) { + $mailboxesTable->addColumn('name_hash', Types::STRING); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + // Round 1: Hash common mailbox names + + $this->connection->beginTransaction(); + + foreach (['INBOX', 'Drafts', 'Sent', 'Trash', 'Junk', 'Spam', 'Archive', 'Archives'] as $name) { + $qb = $this->connection->getQueryBuilder(); + $qb->update('mail_mailboxes') + ->set('name_hash', $qb->createNamedParameter(md5($name))) + ->where($qb->expr()->like('name', $qb->createNamedParameter($name), Types::STRING)) + ->executeStatement(); + } + + $this->connection->commit(); + + // Round 2: Hash everything else + + $qb = $this->connection->getQueryBuilder(); + $qb->select(['id', 'name']) + ->from('mail_mailboxes') + ->where($qb->expr()->emptyString('name_hash')); + $mailboxes = $qb->executeQuery(); + + $updateQb = $this->connection->getQueryBuilder(); + $updateQb->update('mail_mailboxes') + ->set('name_hash', $updateQb->createParameter('name_hash')) + ->where($updateQb->expr()->eq('id', $updateQb->createParameter('id'))); + + $this->connection->beginTransaction(); + + $queryCount = 0; + while (($row = $mailboxes->fetch()) !== false) { + $queryCount++; + + $updateQb->setParameter('id', $row['id']); + $updateQb->setParameter('name_hash', md5($row['name'])); + $updateQb->executeStatement(); + + if ($queryCount === 50000) { + $this->connection->commit(); + $this->connection->beginTransaction(); + $queryCount = 0; + } + } + + $mailboxes->closeCursor(); + + $this->connection->commit(); + } +} diff --git a/lib/Migration/Version3500Date20231115184458.php b/lib/Migration/Version3500Date20231115184458.php new file mode 100644 index 0000000000..cb5c82187e --- /dev/null +++ b/lib/Migration/Version3500Date20231115184458.php @@ -0,0 +1,61 @@ + + * + * @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 Version3500Date20231115184458 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $mailboxesTable = $schema->getTable('mail_mailboxes'); + + $indexOld = 'UNIQ_22DEBD839B6B5FBA5E237E06'; + $indexNew = 'mail_mb_account_id_name_hash'; + + if ($mailboxesTable->hasIndex($indexOld)) { + $mailboxesTable->dropIndex($indexOld); + } + + if (!$mailboxesTable->hasIndex($indexNew)) { + $mailboxesTable->addUniqueIndex(['account_id', 'name_hash'], $indexNew); + } + + return $schema; + } +} diff --git a/tests/Integration/Db/MailboxMapperTest.php b/tests/Integration/Db/MailboxMapperTest.php index d78a2517dc..b5f45bf71b 100644 --- a/tests/Integration/Db/MailboxMapperTest.php +++ b/tests/Integration/Db/MailboxMapperTest.php @@ -88,6 +88,7 @@ public function testFindAll() { 'messages' => $qb->createNamedParameter($i * 100, IQueryBuilder::PARAM_INT), 'unseen' => $qb->createNamedParameter($i, IQueryBuilder::PARAM_INT), 'selectable' => $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), + 'name_hash' => $qb->createNamedParameter(md5("folder$i")), ]); $insert->executeStatement(); } @@ -122,6 +123,7 @@ public function testFindInbox() { 'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), 'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), 'selectable' => $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), + 'name_hash' => $qb->createNamedParameter(md5('INBOX')), ]); $insert->executeStatement(); @@ -129,4 +131,48 @@ public function testFindInbox() { $this->assertSame('INBOX', $result->getName()); } + + public function testMailboxesWithTrailingSpace() { + /** @var Account|MockObject $account */ + $account = $this->createMock(Account::class); + $account->method('getId')->willReturn(13); + + $qb = $this->db->getQueryBuilder(); + $insert = $qb->insert($this->mapper->getTableName()) + ->values([ + 'name' => $qb->createNamedParameter('Test'), + 'account_id' => $qb->createNamedParameter(13, IQueryBuilder::PARAM_INT), + 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'delimiter' => $qb->createNamedParameter('.'), + 'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'selectable' => $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), + 'name_hash' => $qb->createNamedParameter(md5('Test')), + ]); + $insert->executeStatement(); + + $qb = $this->db->getQueryBuilder(); + $insert = $qb->insert($this->mapper->getTableName()) + ->values([ + 'name' => $qb->createNamedParameter('Test '), + 'account_id' => $qb->createNamedParameter(13, IQueryBuilder::PARAM_INT), + 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), + 'delimiter' => $qb->createNamedParameter('.'), + 'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'selectable' => $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), + 'name_hash' => $qb->createNamedParameter(md5('Test ')), + ]); + $insert->executeStatement(); + + $resultA = $this->mapper->find($account, 'Test'); + $this->assertSame('Test', $resultA->getName()); + + $resultB = $this->mapper->find($account, 'Test '); + $this->assertSame('Test ', $resultB->getName()); + } }