diff --git a/.gitignore b/.gitignore index 3aa1bffb30..16aa28585a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ translations.js # JS coverage coverage/ + +# vim +.*.sw? diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index dc2f4fbd8c..619f33a533 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -33,6 +33,8 @@ use OCA\Mail\Service\MailManager; use OCA\Mail\Service\MailTransmission; use OCA\Mail\Service\UserPreferenceSevice; +use OCA\Mail\Service\Group\IGroupService; +use OCA\Mail\Service\Group\NextcloudGroupService; use OCP\AppFramework\App; use OCP\Util; @@ -71,6 +73,8 @@ private function initializeAppContainer() { $container->registerAlias('ErrorMiddleware', ErrorMiddleware::class); $container->registerMiddleWare('ErrorMiddleware'); + + $container->registerAlias(IGroupService::class, NextcloudGroupService::class); } } diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index d27d1dfa37..c44462e4d4 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -7,6 +7,7 @@ * @author Lukas Reschke * @author Robin McCorkell * @author Thomas Müller + * @author Matthias Rella * * Mail * @@ -38,6 +39,7 @@ use OCA\Mail\Service\AliasesService; use OCA\Mail\Service\Logger; use OCA\Mail\Service\SetupService; +use OCA\Mail\Service\GroupsIntegration; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\IL10N; @@ -49,6 +51,9 @@ class AccountsController extends Controller { /** @var AccountService */ private $accountService; + /** @var GroupsIntegration */ + private $groupsIntegration; + /** @var string */ private $currentUserId; @@ -80,10 +85,11 @@ class AccountsController extends Controller { * @param ICrypto $crypto * @param SetupService $setup */ - public function __construct($appName, IRequest $request, AccountService $accountService, $UserId, Logger $logger, IL10N $l10n, ICrypto $crypto, AliasesService $aliasesService, IMailTransmission $mailTransmission, SetupService $setup + public function __construct($appName, IRequest $request, AccountService $accountService, GroupsIntegration $groupsIntegration, $UserId, Logger $logger, IL10N $l10n, ICrypto $crypto, AliasesService $aliasesService, IMailTransmission $mailTransmission, SetupService $setup ) { parent::__construct($appName, $request); $this->accountService = $accountService; + $this->groupsIntegration = $groupsIntegration; $this->currentUserId = $UserId; $this->logger = $logger; $this->l10n = $l10n; @@ -253,7 +259,11 @@ public function send($accountId, $folderId, $subject, $body, $to, $cc, $bcc, $dr $account = $this->accountService->find($this->currentUserId, $accountId); $alias = $aliasId ? $this->aliasesService->find($aliasId, $this->currentUserId) : null; - $messageData = NewMessageData::fromRequest($account, $to, $cc, $bcc, $subject, $body, $attachments); + $expandedTo = $this->groupsIntegration->expand($to); + $expandedCc = $this->groupsIntegration->expand($cc); + $expandedBcc = $this->groupsIntegration->expand($bcc); + + $messageData = NewMessageData::fromRequest($account, $expandedTo, $expandedCc, $expandedBcc, $subject, $body, $attachments); $repliedMessageData = new RepliedMessageData($account, $folderId, $messageId); try { diff --git a/lib/Service/AutoCompletion/AutoCompleteService.php b/lib/Service/AutoCompletion/AutoCompleteService.php index 0f8cbe6b64..e85915b3aa 100644 --- a/lib/Service/AutoCompletion/AutoCompleteService.php +++ b/lib/Service/AutoCompletion/AutoCompleteService.php @@ -2,6 +2,7 @@ /** * @author Christoph Wurst + * @author Matthias Rella * * Mail * @@ -23,22 +24,28 @@ use OCA\Mail\Db\CollectedAddress; use OCA\Mail\Service\ContactsIntegration; +use OCA\Mail\Service\GroupsIntegration; class AutoCompleteService { /** @var ContactsIntegration */ private $contactsIntegration; + /** @var GroupsIntegration */ + private $groupsIntegration; + /** @var AddressCollector */ private $addressCollector; - public function __construct(ContactsIntegration $ci, AddressCollector $ac) { + public function __construct(ContactsIntegration $ci, GroupsIntegration $gi, AddressCollector $ac) { $this->contactsIntegration = $ci; + $this->groupsIntegration = $gi; $this->addressCollector = $ac; } public function findMatches($term) { $recipientsFromContacts = $this->contactsIntegration->getMatchingRecipient($term); + $recipientGroups = $this->groupsIntegration->getMatchingGroups($term); $fromCollector = $this->addressCollector->searchAddress($term); // Convert collected addresses into same format as CI creates @@ -50,7 +57,7 @@ public function findMatches($term) { ]; }, $fromCollector); - return array_merge($recipientsFromContacts, $recipientsFromCollector); + return array_merge($recipientsFromContacts, $recipientsFromCollector, $recipientGroups); } } diff --git a/lib/Service/Group/IGroupService.php b/lib/Service/Group/IGroupService.php new file mode 100644 index 0000000000..ff3bc31ef3 --- /dev/null +++ b/lib/Service/Group/IGroupService.php @@ -0,0 +1,50 @@ + + * + * Mail + * + * 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\Service\Group; + +interface IGroupService { + + /** + * Search the service's groups. + * + * @param string $term + * @return array of matched groups as associative arrays + */ + public function search($term); + + /** + * Get the group's namespace. + * + * @return string + */ + public function getNamespace(); + + /** + * Get the group's users. + * + * @param string $groupId + * @return array with group's users as associative arrays + */ + public function getUsers($groupId); + +} + diff --git a/lib/Service/Group/NextcloudGroupService.php b/lib/Service/Group/NextcloudGroupService.php new file mode 100644 index 0000000000..79a95fafd7 --- /dev/null +++ b/lib/Service/Group/NextcloudGroupService.php @@ -0,0 +1,81 @@ + + * + * Mail + * + * 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\Service\Group; + +use OCP\IGroupManager; +use OCA\Mail\Exception\ServiceException; + +class NextcloudGroupService implements IGroupService { + + /** + * Nextcloud's group manager + * + * @var IGroupManager + */ + private $groupManager; + + /** + * Group's namespace + * + * @var string + */ + private $namespace = "Nextcloud"; + + public function __construct(IGroupManager $groupManager) { + $this->groupManager = $groupManager; + } + + public function getNamespace() { + return $this->namespace; + } + + public function search($term) { + $groups = $this->groupManager->search($term); + + return array_map( + function($g) { + return [ + 'id' => $g->getGID(), + 'name' => $g->getDisplayName() + ]; + }, + $groups + ); + } + + public function getUsers($groupId) { + if(!$this->groupManager->groupExists($groupId)) { + throw new ServiceException("$groupId ({$this->getNamespace()}) does not exist"); + } + $users = $this->groupManager->get($groupId)->getUsers(); + return array_map( + function($user) { + return [ + 'id' => $user->getUID(), + 'name' => $user->getDisplayName(), + 'email' => $user->getEMailAddress() + ]; + }, + $users + ); + } +} diff --git a/lib/Service/GroupsIntegration.php b/lib/Service/GroupsIntegration.php new file mode 100644 index 0000000000..f91deca6b6 --- /dev/null +++ b/lib/Service/GroupsIntegration.php @@ -0,0 +1,108 @@ + + * + * Mail + * + * 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\Service; + +use OCA\Mail\Service\Group\IGroupService; +use OCA\Mail\Exception\ServiceException; + +class GroupsIntegration { + + /** + * The services to get groups from + * + * @var IGroupService[] + */ + private $groupServices = []; + + public function __construct(IGroupService $groupService) { + $this->groupServices[] = $groupService; + } + + /** + * Extracts all matching contacts with email address and name + * + * @param string $term + * @return array + */ + public function getMatchingGroups($term) { + $receivers = []; + foreach ($this->groupServices as $gs) { + $result = $gs->search($term); + foreach ($result as $g) { + $gid = $this->servicePrefix($gs) . $g['id']; + $receivers[] = [ + 'id' => $gid, + 'label' => $g['name'] . " (" . $gs->getNamespace() . ")", + 'value' => $gid, + 'photo' => null, + ]; + } + } + + return $receivers; + } + + /** + * Returns the prefix for the group service. + * + * @param IGroupService $gs + * @return string + */ + public function servicePrefix(IGroupService $gs) { + if (empty($gs->getNamespace())) { + throw new ServiceException('GroupService has no namespace'); + } + return strtolower($gs->getNamespace()) . ":"; + } + + /** + * Expands a string of group names to its members email addresses. + * + * @param string $recipients + * @return string + */ + public function expand($recipients) { + return array_reduce($this->groupServices, + function($carry, $service) { + return preg_replace_callback( + '/' . preg_quote($this->servicePrefix($service)) . '([\w\d ]+)(,?)/', + function($matches) use ($service) { + if (empty($matches[1])) { + return ''; + } + $members = $service->getUsers($matches[1]); + if (empty($members)) { + throw new ServiceException($matches[1] . " ({$service->getNamespace()}) has no members"); + } + $addresses = []; + foreach ($members as $m) { + if (!empty($m['email'])) { + $addresses[] = $m['email']; + } + } + return implode(',', $addresses) + . (!empty($matches[2]) && !empty($addresses) ? ',' : ''); + }, $carry); + }, $recipients); + } + +} diff --git a/tests/Controller/AccountsControllerTest.php b/tests/Controller/AccountsControllerTest.php index 7524739698..ed0fb5dd96 100644 --- a/tests/Controller/AccountsControllerTest.php +++ b/tests/Controller/AccountsControllerTest.php @@ -37,6 +37,7 @@ use OCA\Mail\Service\AutoConfig\AutoConfig; use OCA\Mail\Service\Logger; use OCA\Mail\Service\SetupService; +use OCA\Mail\Service\GroupsIntegration; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\IL10N; @@ -55,6 +56,9 @@ class AccountsControllerTest extends TestCase { /** @var AccountService|PHPUnit_Framework_MockObject_MockObject */ private $accountService; + /** @var GroupsIntegration|PHPUnit_Framework_MockObject_MockObject */ + private $groupsIntegration; + /** @var string */ private $userId; @@ -94,6 +98,10 @@ protected function setUp() { $this->appName = 'mail'; $this->request = $this->createMock(IRequest::class); $this->accountService = $this->createMock(AccountService::class); + $this->groupsIntegration = $this->createMock(GroupsIntegration::class); + $this->groupsIntegration->expects($this->any()) + ->method('expand') + ->will($this->returnArgument(0)); $this->userId = 'manfred'; $this->autoConfig = $this->createMock(AutoConfig::class); $this->logger = $this->createMock(Logger::class); @@ -103,7 +111,7 @@ protected function setUp() { $this->transmission = $this->createMock(IMailTransmission::class); $this->setupService = $this->createMock(SetupService::class); - $this->controller = new AccountsController($this->appName, $this->request, $this->accountService, $this->userId, + $this->controller = new AccountsController($this->appName, $this->request, $this->accountService, $this->groupsIntegration, $this->userId, $this->logger, $this->l10n, $this->crypto, $this->aliasesService, $this->transmission, $this->setupService); $this->account = $this->createMock(Account::class); $this->accountId = 123; diff --git a/tests/Service/Autocompletion/AutoCompleteServiceTest.php b/tests/Service/Autocompletion/AutoCompleteServiceTest.php index 256469b0b5..c75b504b0a 100644 --- a/tests/Service/Autocompletion/AutoCompleteServiceTest.php +++ b/tests/Service/Autocompletion/AutoCompleteServiceTest.php @@ -2,6 +2,7 @@ /** * @author Christoph Wurst + * @author Matthias Rella * * Mail * @@ -24,24 +25,26 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Db\CollectedAddress; use OCA\Mail\Service\AutoCompletion\AutoCompleteService; +use OCA\Mail\Service\AutoCompletion\AddressCollector; +use OCA\Mail\Service\ContactsIntegration; +use OCA\Mail\Service\GroupsIntegration; class AutoCompleteServiceTest extends TestCase { private $contactsIntegration; + private $groupsIntegration; private $addressCollector; private $service; protected function setUp() { parent::setUp(); - $this->contactsIntegration = $this->getMockBuilder('\OCA\Mail\Service\ContactsIntegration') - ->disableOriginalConstructor() - ->getMock(); - $this->addressCollector = $this->getMockBuilder('\OCA\Mail\Service\AutoCompletion\AddressCollector') - ->disableOriginalConstructor() - ->getMock(); + $this->contactsIntegration = $this->createMock(ContactsIntegration::class); + $this->groupsIntegration = $this->createMock(GroupsIntegration::class); + $this->addressCollector = $this->createMock(AddressCollector::class); $this->service = new AutoCompleteService($this->contactsIntegration, + $this->groupsIntegration, $this->addressCollector); } @@ -61,10 +64,18 @@ public function testFindMatches() { $john, ]; + $groupsResult = [ + ['id' => 20, 'label' => 'Journalists', 'value' => 'Journalists'] + ]; + $this->contactsIntegration->expects($this->once()) ->method('getMatchingRecipient') ->with($term) ->will($this->returnValue($contactsResult)); + $this->groupsIntegration->expects($this->once()) + ->method('getMatchingGroups') + ->with($term) + ->will($this->returnValue($groupsResult)); $this->addressCollector->expects($this->once()) ->method('searchAddress') ->with($term) @@ -76,6 +87,7 @@ public function testFindMatches() { ['id' => 12, 'label' => '"john doe" ', 'value' => '"john doe" '], ['id' => 13, 'label' => '"joe doe" ', 'value' => '"joe doe" '], ['id' => 1234, 'label' => 'John Doe', 'value' => '"John Doe" '], + ['id' => 20, 'label' => 'Journalists', 'value' => 'Journalists'], ]; $this->assertEquals($expected, $response); } diff --git a/tests/Service/Group/NextcloudGroupServiceTest.php b/tests/Service/Group/NextcloudGroupServiceTest.php new file mode 100644 index 0000000000..18461e342f --- /dev/null +++ b/tests/Service/Group/NextcloudGroupServiceTest.php @@ -0,0 +1,146 @@ + + * + * Mail + * + * 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\Tests\Service\Group; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Service\Group\NextcloudGroupService; +use OCA\Mail\Exception\ServiceException; +use OCP\IGroupManager; +use OCP\IGroup; +use OCP\IUser; + +class NextcloudGroupServiceTest extends TestCase { + + private $groupsManager; + private $groupService; + + protected function setUp() { + parent::setUp(); + + $this->groupsManager = $this->createMock(IGroupManager::class); + $this->groupService = new NextcloudGroupService($this->groupsManager); + } + + private function createTestGroup($id, $name, $users = []) { + $mockGroup = $this->createMock(IGroup::class); + $mockGroup->expects($this->any()) + ->method('getGID') + ->will($this->returnValue($id)); + $mockGroup->expects($this->any()) + ->method('getDisplayName') + ->will($this->returnValue($name)); + $mockGroup->expects($this->any()) + ->method('getUsers') + ->willReturn($users); + return $mockGroup; + } + + private function createTestUser($id, $name, $email) { + $mockUser = $this->createMock(IUser::class); + $mockUser->expects($this->any()) + ->method('getUID') + ->will($this->returnValue($id)); + $mockUser->expects($this->any()) + ->method('getDisplayName') + ->will($this->returnValue($name)); + $mockUser->expects($this->any()) + ->method('getEMailAddress') + ->willReturn($email); + return $mockUser; + } + + + public function testSearch() { + $term = 'te'; // searching for: John Doe + $searchResult = [ + $this->createTestGroup('testgroup', 'first test group'), + $this->createTestGroup('testgroup2', 'second test group'), + ]; + + $this->groupsManager->expects($this->once()) + ->method('search') + ->with($term) + ->will($this->returnValue($searchResult)); + + $expected = [ + [ + 'id' => 'testgroup', + 'name' => 'first test group', + ], + [ + 'id' => 'testgroup2', + 'name' => 'second test group', + ] + ]; + $actual = $this->groupService->search($term); + + $this->assertEquals($expected, $actual); + } + + public function testGetUsers() { + $users = [ + $this->createTestUser('bob', 'Bobby', 'bob@smith.net'), + $this->createTestUser('alice', 'Alice', 'alice@smith.net') + ]; + $group = + $this->createTestGroup('testgroup', 'first test group', $users); + + $this->groupsManager->expects($this->once()) + ->method('groupExists') + ->willReturn(true); + + $this->groupsManager->expects($this->once()) + ->method('get') + ->with('testgroup') + ->willReturn($group); + + $actual = $this->groupService->getUsers('testgroup'); + + $expected = [ + [ + 'id' => 'bob', + 'name' => 'Bobby', + 'email' => 'bob@smith.net' + ], + [ + 'id' => 'alice', + 'name' => 'Alice', + 'email' => 'alice@smith.net' + ] + ]; + + $this->assertEquals($expected, $actual); + + } + + public function testGetUsersWrong() { + $this->expectException(ServiceException::class); + + $this->groupsManager->expects($this->once()) + ->method('groupExists') + ->willReturn(false); + + $this->groupService->getUsers('nogroup'); + } + +} + diff --git a/tests/Service/GroupsIntegrationTest.php b/tests/Service/GroupsIntegrationTest.php new file mode 100644 index 0000000000..ca0aa282bc --- /dev/null +++ b/tests/Service/GroupsIntegrationTest.php @@ -0,0 +1,178 @@ + + * + * Mail + * + * 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\Tests\Service; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Service\GroupsIntegration; +use OCA\Mail\Service\Group\NextcloudGroupService; +use OCA\Mail\Exception\ServiceException; + +class GroupsIntegrationTest extends TestCase { + + private $groupService1; + private $groupsIntegration; + + protected function setUp() { + parent::setUp(); + + $this->groupService1 = $this->createMock(NextcloudGroupService::class); + $this->groupService1->expects($this->any()) + ->method('getNamespace') + ->willReturn('Namespace1'); + $this->groupsIntegration = new GroupsIntegration($this->groupService1); + } + + public function testGetMatchingGroups() { + $term = 'te'; // searching for: John Doe + $searchResult1 = [ + [ + 'id' => 'testgroup', + 'name' => "first test group" + ] + ]; + + $this->groupService1->expects($this->once()) + ->method('search') + ->with($term) + ->will($this->returnValue($searchResult1)); + + $expected = [ + [ + 'id' => 'namespace1:testgroup', + 'label' => 'first test group (Namespace1)', + 'value' => 'namespace1:testgroup', + 'photo' => null, + ] + ]; + $actual = $this->groupsIntegration->getMatchingGroups($term); + + $this->assertEquals($expected, $actual); + } + + public function testExpandNone() { + $recipients = "john@doe.com,alice@smith.net"; + $members = [ + [ + 'id' => 'bob', + 'name' => "Bobby", + 'email' => "bob@smith.net" + ], + [ + 'id' => 'mary', + 'name' => 'Mary', + 'email' => 'mary@smith.net' + ] + ]; + $this->groupService1->expects($this->never()) + ->method('getUsers') + ->willReturn($members); + + $expected = $recipients; + + $actual = $this->groupsIntegration->expand($recipients); + + $this->assertEquals($expected, $actual); + + } + + public function testExpand() { + $recipients = "john@doe.com,namespace1:testgroup,alice@smith.net"; + $members = [ + [ + 'id' => 'bob', + 'name' => "Bobby", + 'email' => "bob@smith.net" + ], + [ + 'id' => 'mary', + 'name' => 'Mary', + 'email' => 'mary@smith.net' + ] + ]; + $this->groupService1->expects($this->once()) + ->method('getUsers') + ->willReturn($members); + + $expected = "john@doe.com,bob@smith.net,mary@smith.net,alice@smith.net"; + + $actual = $this->groupsIntegration->expand($recipients); + + $this->assertEquals($expected, $actual); + + } + + public function testExpand2() { + $recipients = "john@doe.com,namespace1:testgroup,alice@smith.net"; + $members = [ + [ + 'id' => 'bob', + 'name' => "Bobby", + 'email' => "bob@smith.net" + ], + [ + 'id' => 'mary', + 'name' => 'Mary', + 'email' => 'mary@smith.net' + ] + ]; + $this->groupService1->expects($this->once()) + ->method('getUsers') + ->willReturn($members); + + $expected = "john@doe.com,bob@smith.net,mary@smith.net,alice@smith.net"; + + $actual = $this->groupsIntegration->expand($recipients); + + $this->assertEquals($expected, $actual); + + } + + public function testExpandEmpty() { + $this->expectException(ServiceException::class); + $recipients = "john@doe.com,namespace1:testgroup,alice@smith.net"; + $members = [ + ]; + $this->groupService1->expects($this->once()) + ->method('getUsers') + ->willReturn($members); + $this->groupsIntegration->expand($recipients); + } + + public function testExpandWrong() { + $recipients = "john@doe.com,nons:testgroup,alice@smith.net"; + $expected = "john@doe.com,nons:testgroup,alice@smith.net"; + + $actual = $this->groupsIntegration->expand($recipients); + + $this->assertEquals($expected, $actual); + + } + + public function testExpandWrong2() { + $this->expectException(ServiceException::class); + $recipients = "john@doe.com,namespace1:nogroup,alice@smith.net"; + + $this->groupsIntegration->expand($recipients); + + } + +}