From d15711ef25838e61db69d19252285769bc5cefa9 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Tue, 9 Jul 2024 11:08:24 -0400 Subject: [PATCH] feat: mail provider backend Signed-off-by: SebastianKrupinski feat: mail provider backend Signed-off-by: SebastianKrupinski feat: mail provider backend Signed-off-by: SebastianKrupinski --- lib/AppInfo/Application.php | 5 + lib/Db/MailAccountMapper.php | 23 +++ lib/Provider/Command/MessageSend.php | 87 +++++++++ lib/Provider/MailProvider.php | 176 ++++++++++++++++++ lib/Provider/MailService.php | 201 ++++++++++++++++++++ lib/Service/AccountService.php | 40 ++++ tests/Unit/Provider/MailProviderTest.php | 226 +++++++++++++++++++++++ tests/Unit/Provider/MailServiceTest.php | 91 +++++++++ 8 files changed, 849 insertions(+) create mode 100644 lib/Provider/Command/MessageSend.php create mode 100644 lib/Provider/MailProvider.php create mode 100644 lib/Provider/MailService.php create mode 100644 tests/Unit/Provider/MailProviderTest.php create mode 100644 tests/Unit/Provider/MailServiceTest.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 6e4ae2dfde..1aa3b20f36 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -55,6 +55,7 @@ use OCA\Mail\Listener\SpamReportListener; use OCA\Mail\Listener\UserDeletedListener; use OCA\Mail\Notification\Notifier; +use OCA\Mail\Provider\MailProvider; use OCA\Mail\Search\FilteringProvider; use OCA\Mail\Search\Provider; use OCA\Mail\Service\Attachment\AttachmentService; @@ -164,6 +165,10 @@ public function register(IRegistrationContext $context): void { $context->registerSearchProvider(Provider::class); } + // Added in version 4.0.0 + // register mail provider + $context->registerMailProvider(MailProvider::class); + $context->registerNotifierService(Notifier::class); // bypass Horde Translation system diff --git a/lib/Db/MailAccountMapper.php b/lib/Db/MailAccountMapper.php index b42c1908c4..c37ab04319 100644 --- a/lib/Db/MailAccountMapper.php +++ b/lib/Db/MailAccountMapper.php @@ -83,6 +83,29 @@ public function findByUserId(string $userId): array { return $this->findEntities($query); } + /** + * Finds a mail account(s) by user id and mail address + * + * @since 4.0.0 + * + * @param string $userId system user id + * @param string $address mail address (e.g. test@example.com) + * + * @return MailAccount[] + * + * @throws DoesNotExistException + */ + public function findByUserIdAndAddress(string $userId, string $address): array { + $qb = $this->db->getQueryBuilder(); + $query = $qb + ->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('email', $qb->createNamedParameter($address))); + + return $this->findEntities($query); + } + /** * @throws DoesNotExistException * @throws MultipleObjectsReturnedException diff --git a/lib/Provider/Command/MessageSend.php b/lib/Provider/Command/MessageSend.php new file mode 100644 index 0000000000..35a3abfcfe --- /dev/null +++ b/lib/Provider/Command/MessageSend.php @@ -0,0 +1,87 @@ +accountService->find($userId, (int) $serviceId); + // convert mail provider message to local message + $localMessage = new LocalMessage(); + $localMessage->setType($localMessage::TYPE_OUTGOING); + $localMessage->setAccountId($account->getId()); + $localMessage->setSubject($message->getSubject()); + $localMessage->setBody($message->getBody()); + $localMessage->setHtml(true); + $localMessage->setSendAt(time()); + + // convert all mail provider attachments to local attachments + $attachments = []; + if (count($message->getAttachments()) > 0) { + // iterate attachments and save them + foreach ($message->getAttachments() as $entry) { + $attachments[] = $this->attachmentService->addFileFromString( + $userId, + $entry->getName(), + $entry->getType(), + $entry->getContents() + )->jsonSerialize(); + } + } + // convert recipiant addresses + $to = $this->convertAddressArray($message->getTo()); + $cc = $this->convertAddressArray($message->getCc()); + $bcc = $this->convertAddressArray($message->getBcc()); + // save message for sending + $localMessage = $this->outboxService->saveMessage( + $account, + $localMessage, + $to, + $cc, + $bcc, + $attachments + ); + + // evaluate if job scheduler is NOT cron, send message right away otherwise let cron job handle it + if ($this->config->getAppValue('core', 'backgroundjobs_mode', 'ajax') !== 'cron') { + $localMessage = $this->outboxService->sendMessage($localMessage, $account); + } + + } + + protected function convertAddressArray(array|null $in) { + // construct place holder + $out = []; + // convert format + foreach ($in as $entry) { + $out[] = (!empty($entry->getLabel())) ? ['email' => $entry->getAddress(), 'label' => $entry->getLabel()] : ['email' => $entry->getAddress()]; + } + // return converted addressess + return $out; + } + +} diff --git a/lib/Provider/MailProvider.php b/lib/Provider/MailProvider.php new file mode 100644 index 0000000000..605fae5a49 --- /dev/null +++ b/lib/Provider/MailProvider.php @@ -0,0 +1,176 @@ +listServices($userId)) > 0); + } + + /** + * retrieve collection of services for a specific user + * + * @since 4.0.0 + * + * @param string $userId user id + * + * @return array collection of service id and object ['1' => IServiceObject] + */ + public function listServices(string $userId): array { + + try { + // retrieve service(s) details from data store + $accounts = $this->accountService->findByUserId($userId); + } catch (\Throwable $th) { + return []; + } + // construct temporary collection + $services = []; + // add services to collection + foreach ($accounts as $entry) { + // extract values + $serviceId = (string) $entry->getId(); + $label = $entry->getName(); + $address = new MailAddress($entry->getEmail(), $entry->getName()); + // add service to collection + $services[$serviceId] = new MailService($this->container, $userId, $serviceId, $label, $address); + } + // return list of services for user + return $services; + + } + + /** + * retrieve a service with a specific id + * + * @since 4.0.0 + * + * @param string $userId user id + * @param string $serviceId service id + * + * @return IService|null returns service object or null if none found + */ + public function findServiceById(string $userId, string $serviceId): IService | null { + + // evaluate if id is a number + if (is_numeric($serviceId)) { + try { + // retrieve service details from data store + $account = $this->accountService->find($userId, (int) $serviceId); + } catch(\Throwable $th) { + return null; + } + } + // evaluate if service details where found + if ($account instanceof Account) { + // extract values + $serviceId = (string) $account->getId(); + $label = $account->getName(); + $address = new MailAddress($account->getEmail(), $account->getName()); + // return mail service object + return new MailService($this->container, $userId, $serviceId, $label, $address); + } + + return null; + + } + + /** + * retrieve a service for a specific mail address + * + * @since 4.0.0 + * + * @param string $userId user id + * @param string $address mail address (e.g. test@example.com) + * + * @return IService returns service object or null if none found + */ + public function findServiceByAddress(string $userId, string $address): IService | null { + + try { + // retrieve service details from data store + $accounts = $this->accountService->findByUserIdAndAddress($userId, $address); + } catch(\Throwable $th) { + return null; + } + // evaliate if service details where found + if (is_array($accounts) && count($accounts) > 0 && $accounts[0] instanceof Account) { + // extract values + $serviceId = (string) $accounts[0]->getId(); + $label = $accounts[0]->getName(); + $address = new MailAddress($accounts[0]->getEmail(), $accounts[0]->getName()); + // return mail service object + return new MailService($this->container, $userId, $serviceId, $label, $address); + } + + return null; + + } + + /** + * construct a new empty service object + * + * @since 30.0.0 + * + * @return IService blank service object + */ + public function initiateService(): IService { + + return new MailService($this->container); + + } + +} diff --git a/lib/Provider/MailService.php b/lib/Provider/MailService.php new file mode 100644 index 0000000000..ebdb9cf531 --- /dev/null +++ b/lib/Provider/MailService.php @@ -0,0 +1,201 @@ +serviceAbilities = ['MessageSend' => true]; + } + + /** + * arbitrary unique text string identifying this service + * + * @since 4.0.0 + * + * @return string id of this service (e.g. 1 or service1 or anything else) + */ + public function id(): string { + + return $this->serviceId; + + } + + /** + * checks if a service is able of performing an specific action + * + * @since 4.0.0 + * + * @param string $value required ability e.g. 'MessageSend' + * + * @return bool true/false if ability is supplied and found in collection + */ + public function capable(string $value): bool { + + // evaluate if required ability exists + if (isset($this->serviceAbilities[$value])) { + return (bool) $this->serviceAbilities[$value]; + } + + return false; + + } + + /** + * retrieves a collection of what actions a service can perfrom + * + * @since 4.0.0 + * + * @return array collection of abilities otherwise empty collection + */ + public function capabilities(): array { + + return $this->serviceAbilities; + + } + + /** + * gets the localized human frendly name of this service + * + * @since 4.0.0 + * + * @return string label/name of service (e.g. ACME Company Mail Service) + */ + public function getLabel(): string { + + return $this->serviceLabel; + + } + + /** + * sets the localized human frendly name of this service + * + * @since 4.0.0 + * + * @param string $value label/name of service (e.g. ACME Company Mail Service) + * + * @return self return this object for command chaining + */ + public function setLabel(string $value): self { + + $this->serviceLabel = $value; + return $this; + + } + + /** + * gets the primary mailing address for this service + * + * @since 4.0.0 + * + * @return IAddress mail address object + */ + public function getPrimaryAddress(): IAddress { + + // retrieve and return primary service address + return $this->servicePrimaryAddress; + + } + + /** + * sets the primary mailing address for this service + * + * @since 4.0.0 + * + * @param IAddress $value mail address object + * + * @return self return this object for command chaining + */ + public function setPrimaryAddress(IAddress $value): self { + + $this->servicePrimaryAddress = $value; + return $this; + + } + + /** + * gets the secondary mailing addresses (aliases) collection for this service + * + * @since 4.0.0 + * + * @return array collection of mail address object [IAddress, IAddress] + */ + public function getSecondaryAddresses(): array { + + // retrieve and return secondary service addressess (aliases) collection + return $this->serviceSecondaryAddresses; + + } + + /** + * sets the secondary mailing addresses (aliases) for this service + * + * @since 4.0.0 + * + * @param IAddress ...$value collection of one or more mail address object + * + * @return self return this object for command chaining + */ + public function setSecondaryAddresses(IAddress ...$value): self { + + $this->serviceSecondaryAddresses = $value; + return $this; + + } + + /** + * construct a new empty message object + * + * @since 30.0.0 + * + * @return IMessage blank message object + */ + public function initiateMessage(): IMessage { + + return new Message(); + + } + + /** + * sends an outbound message + * + * @since 4.0.0 + * + * @param IMessage $message mail message object with all required parameters to send a message + * + * @param array $options array of options reserved for future use + */ + public function sendMessage(IMessage $message, array $option = []): void { + + // load action + $cmd = $this->container->get(\OCA\Mail\Provider\Command\MessageSend::class); + // perform action + $cmd->perform($this->userId, $this->serviceId, $message, $option); + + } + +} diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 3f62da29f5..82d819f59d 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -78,6 +78,46 @@ public function findById(int $id): Account { return new Account($this->mapper->findById($id)); } + /** + * Finds a mail account by user id and mail address + * + * @since 4.0.0 + * + * @param string $userId system user id + * @param string $address mail address (e.g. test@example.com) + * + * @return Account[] + * + * @throws ClientException + */ + public function findByUserIdAndAddress(string $userId, string $address): array { + // evaluate if cached accounts collection already exists + if (isset($this->accounts[$userId])) { + // initialize tempory collection + $list = []; + // iterate through accounts and find accounts matching mail address + foreach ($this->accounts[$userId] as $account) { + if ($account->getEmail() === $address) { + $list[] = $account; + } + } + // evaluate if any accounts where found and return them + if (count($list) > 0) { + return $list; + } + // if no accounts where found thrown an error + throw new ClientException("Account with address $address does not exist or you don\'t have permission to access it"); + } + // if cached accounts collection did not exist retrieve account details directly from the data store + try { + return array_map(static function ($a) { + return new Account($a); + }, $this->mapper->findByUserIdAndAddress($userId, $address)); + } catch (DoesNotExistException $e) { + throw new ClientException("Account with address $address does not exist or you don\'t have permission to access it"); + } + } + /** * @param string $userId * @param int $id diff --git a/tests/Unit/Provider/MailProviderTest.php b/tests/Unit/Provider/MailProviderTest.php new file mode 100644 index 0000000000..d18297bdeb --- /dev/null +++ b/tests/Unit/Provider/MailProviderTest.php @@ -0,0 +1,226 @@ +containerInterface = $this->createMock(ContainerInterface::class); + $this->accountService = $this->createMock(AccountService::class); + + } + + public function testId(): void { + + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test set by constructor + $this->assertEquals('mail-application', $mailProvider->id()); + + } + + public function testLabel(): void { + + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test set by constructor + $this->assertEquals('Mail Application', $mailProvider->label()); + + } + + public function testHasServices(): void { + + // construct dummy mail account + $mailAccount = new Account(new MailAccount([ + 'accountId' => 100, + 'accountName' => 'User One', + 'emailAddress' => 'user1@testing.com', + 'imapHost' => '', + 'imapPort' => '', + 'imapSslMode' => false, + 'imapUser' => '', + 'smtpHost' => '', + 'smtpPort' => '', + 'smtpSslMode' => false, + 'smtpUser' => '', + ])); + // define account services find + $this->accountService + ->expects($this->any()) + ->method('findByUserId') + ->will( + $this->returnValueMap( + [ + ['user0', []], + ['user1', [100 => $mailAccount]] + ] + ) + ); + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test result with no services found + $this->assertFalse($mailProvider->hasServices('user0')); + // test result with services found + $this->assertTrue($mailProvider->hasServices('user1')); + + } + + public function testListServices(): void { + + // construct dummy mail account + $mailAccount = new Account(new MailAccount([ + 'accountId' => 100, + 'accountName' => 'User One', + 'emailAddress' => 'user1@testing.com', + 'imapHost' => '', + 'imapPort' => '', + 'imapSslMode' => false, + 'imapUser' => '', + 'smtpHost' => '', + 'smtpPort' => '', + 'smtpSslMode' => false, + 'smtpUser' => '', + ])); + // construct dummy mail service + $mailService = new MailService( + $this->containerInterface, + 'user1', + '100', + 'User One', + new MailAddress('user1@testing.com', 'User One') + ); + // define account services find + $this->accountService + ->expects($this->any()) + ->method('findByUserId') + ->will( + $this->returnValueMap( + [ + ['user0', []], + ['user1', [$mailAccount]] + ] + ) + ); + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test result with no services found + $this->assertEquals([], $mailProvider->listServices('user0')); + // test result with services found + $this->assertEquals([100 => $mailService], $mailProvider->listServices('user1')); + + } + + public function testFindServiceById(): void { + + // construct dummy mail account + $mailAccount = new Account(new MailAccount([ + 'accountId' => 100, + 'accountName' => 'User One', + 'emailAddress' => 'user1@testing.com', + 'imapHost' => '', + 'imapPort' => '', + 'imapSslMode' => false, + 'imapUser' => '', + 'smtpHost' => '', + 'smtpPort' => '', + 'smtpSslMode' => false, + 'smtpUser' => '', + ])); + // construct dummy mail service + $mailService = new MailService( + $this->containerInterface, + 'user1', + '100', + 'User One', + new MailAddress('user1@testing.com', 'User One') + ); + // define account services find + $this->accountService + ->expects($this->any()) + ->method('find') + ->will( + $this->returnValueMap( + [ + ['user0', 100, $this->throwException(new ClientException())], + ['user1', 100, $mailAccount] + ] + ) + ); + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test result with no services found + $this->assertEquals(null, $mailProvider->findServiceById('user0', '100')); + // test result with services found + $this->assertEquals($mailService, $mailProvider->findServiceById('user1', '100')); + + } + + public function testFindServiceByAddress(): void { + + // construct dummy mail account + $mailAccount = new Account(new MailAccount([ + 'accountId' => 100, + 'accountName' => 'User One', + 'emailAddress' => 'user1@testing.com', + 'imapHost' => '', + 'imapPort' => '', + 'imapSslMode' => false, + 'imapUser' => '', + 'smtpHost' => '', + 'smtpPort' => '', + 'smtpSslMode' => false, + 'smtpUser' => '', + ])); + // construct dummy mail service + $mailService = new MailService( + $this->containerInterface, + 'user1', + '100', + 'User One', + new MailAddress('user1@testing.com', 'User One') + ); + // define account services find + $this->accountService + ->expects($this->any()) + ->method('findByUserIdAndAddress') + ->will( + $this->returnValueMap( + [ + ['user0', 'user0@testing.com', $this->throwException(new ClientException())], + ['user1', 'user1@testing.com', [$mailAccount]] + ] + ) + ); + // construct mail provider + $mailProvider = new MailProvider($this->containerInterface, $this->accountService); + // test result with no services found + $this->assertEquals(null, $mailProvider->findServiceByAddress('user0', 'user0@testing.com')); + // test result with services found + $this->assertEquals($mailService, $mailProvider->findServiceByAddress('user1', 'user1@testing.com')); + + } + +} diff --git a/tests/Unit/Provider/MailServiceTest.php b/tests/Unit/Provider/MailServiceTest.php new file mode 100644 index 0000000000..cf77583d8b --- /dev/null +++ b/tests/Unit/Provider/MailServiceTest.php @@ -0,0 +1,91 @@ +createMock(ContainerInterface::class); + + $this->primaryAddress = new Address('test@testing.com', 'Tester'); + + $this->mailService = new MailService( + $container, + 'user1', + 'service1', + 'Mail Service', + $this->primaryAddress + ); + } + + public function testId(): void { + + $this->assertEquals('service1', $this->mailService->id()); + + } + + public function testCapable(): void { + + // test matched result + $this->assertEquals(true, $this->mailService->capable('MessageSend')); + + } + + public function testCapabilities(): void { + + // test collection result + $this->assertEquals([ + 'MessageSend' => true, + ], $this->mailService->capabilities()); + + } + + public function testLabel(): void { + + // test set by constructor + $this->assertEquals('Mail Service', $this->mailService->getLabel()); + // test set by setter + $this->mailService->setLabel('Mail Service 2'); + $this->assertEquals('Mail Service 2', $this->mailService->getLabel()); + + } + + public function testPrimaryAddress(): void { + + // test set by constructor + $this->assertEquals($this->primaryAddress, $this->mailService->getPrimaryAddress()); + // test set by setter + $address = new Address('tester@testing.com'); + $this->mailService->setPrimaryAddress($address); + $this->assertEquals($address, $this->mailService->getPrimaryAddress()); + + } + + public function testSecondaryAddresses(): void { + + // test set by setter + $address1 = new Address('test1@testing.com'); + $address2 = new Address('test2@testing.com'); + $this->mailService->setSecondaryAddresses($address1, $address2); + $this->assertEquals([$address1, $address2], $this->mailService->getSecondaryAddresses()); + + } + +}