From e7d0dfc55bb8874abe3b6c03b6f5e3e3917c43bf Mon Sep 17 00:00:00 2001 From: SebastianKrupinski <krupinskis05@gmail.com> Date: Thu, 16 May 2024 19:06:34 -0400 Subject: [PATCH] feat: mail provider backend Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com> --- apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 82 +++-- apps/dav/lib/Server.php | 3 +- .../unit/CalDAV/Schedule/IMipPluginTest.php | 9 +- lib/composer/composer/autoload_classmap.php | 13 + lib/composer/composer/autoload_static.php | 13 + .../Bootstrap/RegistrationContext.php | 24 ++ lib/private/Mail/Provider/Manager.php | 255 +++++++++++++ lib/private/Server.php | 3 + .../Bootstrap/IRegistrationContext.php | 11 + lib/public/Mail/Provider/Address.php | 85 +++++ lib/public/Mail/Provider/Attachment.php | 139 +++++++ lib/public/Mail/Provider/IAddress.php | 61 ++++ lib/public/Mail/Provider/IAttachment.php | 101 ++++++ lib/public/Mail/Provider/IManager.php | 106 ++++++ lib/public/Mail/Provider/IMessage.php | 232 ++++++++++++ lib/public/Mail/Provider/IMessageSend.php | 31 ++ lib/public/Mail/Provider/IProvider.php | 130 +++++++ lib/public/Mail/Provider/IService.php | 150 ++++++++ lib/public/Mail/Provider/IServiceIdentity.php | 30 ++ lib/public/Mail/Provider/IServiceLocation.php | 30 ++ lib/public/Mail/Provider/Message.php | 338 ++++++++++++++++++ tests/lib/Mail/Provider/AddressTest.php | 46 +++ tests/lib/Mail/Provider/AttachmentTest.php | 71 ++++ tests/lib/Mail/Provider/ManagerTest.php | 189 ++++++++++ tests/lib/Mail/Provider/MessageTest.php | 163 +++++++++ 25 files changed, 2288 insertions(+), 27 deletions(-) create mode 100644 lib/private/Mail/Provider/Manager.php create mode 100644 lib/public/Mail/Provider/Address.php create mode 100644 lib/public/Mail/Provider/Attachment.php create mode 100644 lib/public/Mail/Provider/IAddress.php create mode 100644 lib/public/Mail/Provider/IAttachment.php create mode 100644 lib/public/Mail/Provider/IManager.php create mode 100644 lib/public/Mail/Provider/IMessage.php create mode 100644 lib/public/Mail/Provider/IMessageSend.php create mode 100644 lib/public/Mail/Provider/IProvider.php create mode 100644 lib/public/Mail/Provider/IService.php create mode 100644 lib/public/Mail/Provider/IServiceIdentity.php create mode 100644 lib/public/Mail/Provider/IServiceLocation.php create mode 100644 lib/public/Mail/Provider/Message.php create mode 100644 tests/lib/Mail/Provider/AddressTest.php create mode 100644 tests/lib/Mail/Provider/AttachmentTest.php create mode 100644 tests/lib/Mail/Provider/ManagerTest.php create mode 100644 tests/lib/Mail/Provider/MessageTest.php diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index ef506d1593c..1958531630a 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -15,6 +15,8 @@ use OCP\Defaults; use OCP\IConfig; use OCP\IUserSession; use OCP\Mail\IMailer; +use OCP\Mail\Provider\IManager as IMailManager; +use OCP\Mail\Provider\IMessageSend; use OCP\Util; use Psr\Log\LoggerInterface; use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; @@ -55,6 +57,7 @@ class IMipPlugin extends SabreIMipPlugin { public const METHOD_CANCEL = 'cancel'; public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages private EventComparisonService $eventComparisonService; + private IMailManager $mailManager; public function __construct(IConfig $config, IMailer $mailer, @@ -63,7 +66,8 @@ class IMipPlugin extends SabreIMipPlugin { Defaults $defaults, IUserSession $userSession, IMipService $imipService, - EventComparisonService $eventComparisonService) { + EventComparisonService $eventComparisonService, + IMailManager $mailManager) { parent::__construct(''); $this->userSession = $userSession; $this->config = $config; @@ -73,6 +77,7 @@ class IMipPlugin extends SabreIMipPlugin { $this->defaults = $defaults; $this->imipService = $imipService; $this->eventComparisonService = $eventComparisonService; + $this->mailManager = $mailManager; } public function initialize(DAV\Server $server): void { @@ -212,21 +217,6 @@ class IMipPlugin extends SabreIMipPlugin { $fromEMail = Util::getDefaultEmailAddress('invitations-noreply'); $fromName = $this->imipService->getFrom($senderName, $this->defaults->getName()); - $message = $this->mailer->createMessage() - ->setFrom([$fromEMail => $fromName]); - - if ($recipientName !== null) { - $message->setTo([$recipient => $recipientName]); - } else { - $message->setTo([$recipient]); - } - - if ($senderName !== null) { - $message->setReplyTo([$sender => $senderName]); - } else { - $message->setReplyTo([$sender]); - } - $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); $template->addHeader(); @@ -268,18 +258,60 @@ class IMipPlugin extends SabreIMipPlugin { } $template->addFooter(); - - $message->useTemplate($template); - + // convert iTip Message to string $itip_msg = $iTipMessage->message->serialize(); - $message->attachInline( - $itip_msg, - 'event.ics', - 'text/calendar; method=' . $iTipMessage->method, - ); + + $user = null; + $mailService = null; try { - $failed = $this->mailer->send($message); + // retrieve user object + $user = $this->userSession->getUser(); + // evaluate if user object exist + if ($user !== null) { + // retrieve appropriate service with the same address as sender + $mailService = $this->mailManager->findServiceByAddress($user->getUID(), $sender); + } + // evaluate if a mail service was found and has sending capabilities + if ($mailService !== null && $mailService instanceof IMessageSend) { + // construct mail message and set required parameters + $message = $mailService->initiateMessage(); + $message->setFrom( + (new \OCP\Mail\Provider\Address($sender, $fromName)) + ); + $message->setTo( + (new \OCP\Mail\Provider\Address($recipient, $recipientName)) + ); + $message->setSubject($template->renderSubject()); + $message->setBodyPlain($template->renderText()); + $message->setBodyHtml($template->renderHtml()); + $message->setAttachments((new \OCP\Mail\Provider\Attachment( + $itip_msg, + 'event.ics', + 'text/calendar; method=' . $iTipMessage->method, + true + ))); + // send message + $mailService->sendMessage($message); + } else { + // construct symfony mailer message and set required parameters + $message = $this->mailer->createMessage(); + $message->setFrom([$fromEMail => $fromName]); + $message->setTo( + (($recipientName !== null) ? [$recipient => $recipientName] : [$recipient]) + ); + $message->setReplyTo( + (($senderName !== null) ? [$sender => $senderName] : [$sender]) + ); + $message->useTemplate($template); + $message->attachInline( + $itip_msg, + 'event.ics', + 'text/calendar; method=' . $iTipMessage->method + ); + $failed = $this->mailer->send($message); + } + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; if (!empty($failed)) { $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index b2ffef27a9d..b09d47376e8 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -290,7 +290,8 @@ class Server { \OC::$server->get(\OCP\Defaults::class), $userSession, \OC::$server->get(\OCA\DAV\CalDAV\Schedule\IMipService::class), - \OC::$server->get(\OCA\DAV\CalDAV\EventComparisonService::class) + \OC::$server->get(\OCA\DAV\CalDAV\EventComparisonService::class), + \OC::$server->get(\OCP\Mail\Provider\IManager::class) )); } $this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin()); diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index eb6bd204bdd..783e83ebc93 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -18,6 +18,7 @@ use OCP\Mail\IAttachment; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Mail\IMessage; +use OCP\Mail\Provider\IManager as IMailManager; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Sabre\VObject\Component\VCalendar; @@ -67,6 +68,9 @@ class IMipPluginTest extends TestCase { /** @var EventComparisonService|MockObject */ private $eventComparisonService; + /** @var MailManager|MockObject */ + private $mailManager; + protected function setUp(): void { $this->mailMessage = $this->createMock(IMessage::class); $this->mailMessage->method('setFrom')->willReturn($this->mailMessage); @@ -107,6 +111,8 @@ class IMipPluginTest extends TestCase { $this->eventComparisonService = $this->createMock(EventComparisonService::class); + $this->mailManager = $this->createMock(IMailManager::class); + $this->plugin = new IMipPlugin( $this->config, $this->mailer, @@ -115,7 +121,8 @@ class IMipPluginTest extends TestCase { $this->defaults, $this->userSession, $this->service, - $this->eventComparisonService + $this->eventComparisonService, + $this->mailManager, ); } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 4ee453f0260..c64dba8c4c1 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -566,6 +566,18 @@ return array( 'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => $baseDir . '/lib/public/Mail/IMessage.php', + 'OCP\\Mail\\Provider\\Address' => $baseDir . '/lib/public/Mail/Provider/Address.php', + 'OCP\\Mail\\Provider\\Attachment' => $baseDir . '/lib/public/Mail/Provider/Attachment.php', + 'OCP\\Mail\\Provider\\IAddress' => $baseDir . '/lib/public/Mail/Provider/IAddress.php', + 'OCP\\Mail\\Provider\\IAttachment' => $baseDir . '/lib/public/Mail/Provider/IAttachment.php', + 'OCP\\Mail\\Provider\\IManager' => $baseDir . '/lib/public/Mail/Provider/IManager.php', + 'OCP\\Mail\\Provider\\IMessage' => $baseDir . '/lib/public/Mail/Provider/IMessage.php', + 'OCP\\Mail\\Provider\\IMessageSend' => $baseDir . '/lib/public/Mail/Provider/IMessageSend.php', + 'OCP\\Mail\\Provider\\IProvider' => $baseDir . '/lib/public/Mail/Provider/IProvider.php', + 'OCP\\Mail\\Provider\\IService' => $baseDir . '/lib/public/Mail/Provider/IService.php', + 'OCP\\Mail\\Provider\\IServiceIdentity' => $baseDir . '/lib/public/Mail/Provider/IServiceIdentity.php', + 'OCP\\Mail\\Provider\\IServiceLocation' => $baseDir . '/lib/public/Mail/Provider/IServiceLocation.php', + 'OCP\\Mail\\Provider\\Message' => $baseDir . '/lib/public/Mail/Provider/Message.php', 'OCP\\Migration\\BigIntMigration' => $baseDir . '/lib/public/Migration/BigIntMigration.php', 'OCP\\Migration\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php', @@ -1605,6 +1617,7 @@ return array( 'OC\\Mail\\EMailTemplate' => $baseDir . '/lib/private/Mail/EMailTemplate.php', 'OC\\Mail\\Mailer' => $baseDir . '/lib/private/Mail/Mailer.php', 'OC\\Mail\\Message' => $baseDir . '/lib/private/Mail/Message.php', + 'OC\\Mail\\Provider\\Manager' => $baseDir . '/lib/private/Mail/Provider/Manager.php', 'OC\\Memcache\\APCu' => $baseDir . '/lib/private/Memcache/APCu.php', 'OC\\Memcache\\ArrayCache' => $baseDir . '/lib/private/Memcache/ArrayCache.php', 'OC\\Memcache\\CADTrait' => $baseDir . '/lib/private/Memcache/CADTrait.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d6cbb1ac886..7c340449fcd 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -599,6 +599,18 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/IMessage.php', + 'OCP\\Mail\\Provider\\Address' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Address.php', + 'OCP\\Mail\\Provider\\Attachment' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Attachment.php', + 'OCP\\Mail\\Provider\\IAddress' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IAddress.php', + 'OCP\\Mail\\Provider\\IAttachment' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IAttachment.php', + 'OCP\\Mail\\Provider\\IManager' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IManager.php', + 'OCP\\Mail\\Provider\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IMessage.php', + 'OCP\\Mail\\Provider\\IMessageSend' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IMessageSend.php', + 'OCP\\Mail\\Provider\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IProvider.php', + 'OCP\\Mail\\Provider\\IService' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IService.php', + 'OCP\\Mail\\Provider\\IServiceIdentity' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IServiceIdentity.php', + 'OCP\\Mail\\Provider\\IServiceLocation' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IServiceLocation.php', + 'OCP\\Mail\\Provider\\Message' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Message.php', 'OCP\\Migration\\BigIntMigration' => __DIR__ . '/../../..' . '/lib/public/Migration/BigIntMigration.php', 'OCP\\Migration\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php', @@ -1638,6 +1650,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Mail\\EMailTemplate' => __DIR__ . '/../../..' . '/lib/private/Mail/EMailTemplate.php', 'OC\\Mail\\Mailer' => __DIR__ . '/../../..' . '/lib/private/Mail/Mailer.php', 'OC\\Mail\\Message' => __DIR__ . '/../../..' . '/lib/private/Mail/Message.php', + 'OC\\Mail\\Provider\\Manager' => __DIR__ . '/../../..' . '/lib/private/Mail/Provider/Manager.php', 'OC\\Memcache\\APCu' => __DIR__ . '/../../..' . '/lib/private/Memcache/APCu.php', 'OC\\Memcache\\ArrayCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/ArrayCache.php', 'OC\\Memcache\\CADTrait' => __DIR__ . '/../../..' . '/lib/private/Memcache/CADTrait.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index df03d59ebfa..f59d5b55706 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -26,6 +26,7 @@ use OCP\Dashboard\IWidget; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\Http\WellKnown\IHandler; +use OCP\Mail\Provider\IProvider as IMailProvider; use OCP\Notification\INotifier; use OCP\Profile\ILinkAction; use OCP\Search\IProvider; @@ -148,6 +149,9 @@ class RegistrationContext { /** @var ServiceRegistration<\OCP\TaskProcessing\ITaskType>[] */ private array $taskProcessingTaskTypes = []; + + /** @var ServiceRegistration<IMailProvider>[] */ + private $mailProviders = []; public function __construct(LoggerInterface $logger) { $this->logger = $logger; @@ -411,6 +415,13 @@ class RegistrationContext { $taskProcessingTaskTypeClass ); } + + public function registerMailProvider(string $class): void { + $this->context->registerMailProvider( + $this->appId, + $class + ); + } }; } @@ -603,6 +614,12 @@ class RegistrationContext { public function registerTaskProcessingTaskType(string $appId, string $taskProcessingTaskTypeClass) { $this->taskProcessingTaskTypes[] = new ServiceRegistration($appId, $taskProcessingTaskTypeClass); } + /** + * @psalm-param class-string<IMailProvider> $migratorClass + */ + public function registerMailProvider(string $appId, string $class): void { + $this->mailProviders[] = new ServiceRegistration($appId, $class); + } /** * @param App[] $apps @@ -948,4 +965,11 @@ class RegistrationContext { public function getTaskProcessingTaskTypes(): array { return $this->taskProcessingTaskTypes; } + + /** + * @return ServiceRegistration<IMailProvider>[] + */ + public function getMailProviders(): array { + return $this->mailProviders; + } } diff --git a/lib/private/Mail/Provider/Manager.php b/lib/private/Mail/Provider/Manager.php new file mode 100644 index 00000000000..244aa86d68d --- /dev/null +++ b/lib/private/Mail/Provider/Manager.php @@ -0,0 +1,255 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Mail\Provider; + +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\Mail\Provider\IManager; +use OCP\Mail\Provider\IProvider; +use OCP\Mail\Provider\IService; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +class Manager implements IManager { + + protected ?array $providersCollection = null; + + public function __construct( + private Coordinator $coordinator, + private ContainerInterface $container, + private LoggerInterface $logger, + ) { + } + + /** + * Determine if any mail providers are registered + * + * @since 30.0.0 + * + * @return bool + */ + public function has(): bool { + + // return true if collection has any providers + return !empty($this->providers()); + + } + + /** + * Retrieve a count of how many mail providers are registered + * + * @since 30.0.0 + * + * @return int + */ + public function count(): int { + + // return count of providers in collection + return count($this->providers()); + + } + + /** + * Retrieve which mail providers are registered + * + * @since 30.0.0 + * + * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector'] + */ + public function types(): array { + + // construct types collection + $types = []; + // extract id and name from providers collection + foreach ($this->providers() as $entry) { + $types[$entry->id()] = $entry->label(); + } + // return types collection + return $types; + + } + + /** + * Retrieve all registered mail providers + * + * @since 30.0.0 + * + * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject] + */ + public function providers(): array { + + // evaluate if we already have a cached collection of providers and return the collection if we do + if (is_array($this->providersCollection)) { + return $this->providersCollection; + } + // retrieve server registration context + $context = $this->coordinator->getRegistrationContext(); + // evaluate if registration context was returned + if ($context === null) { + return []; + } + // initilize cached collection + $this->providersCollection = []; + // iterate through all registered mail providers + foreach ($context->getMailProviders() as $entry) { + try { + /** @var IProvider $provider */ + // object provider + $provider = $this->container->get($entry->getService()); + // add provider to cache collection + $this->providersCollection[$provider->id()] = $provider; + } catch (Throwable $e) { + $this->logger->error( + 'Could not load mail provider ' . $entry->getService() . ': ' . $e->getMessage(), + ['exception' => $e] + ); + } + } + // return mail provider collection + return $this->providersCollection; + + } + + /** + * Retrieve a provider with a specific id + * + * @since 30.0.0 + * + * @param string $providerId provider id + * + * @return IProvider|null + */ + public function findProviderById(string $providerId): IProvider | null { + + // evaluate if we already have a cached collection of providers + if (!is_array($this->providersCollection)) { + $this->providers(); + } + + if (isset($this->providersCollection[$providerId])) { + return $this->providersCollection[$providerId]; + } + // return null if provider was not found + return null; + + } + + /** + * Retrieve all services for all registered mail providers + * + * @since 30.0.0 + * + * @param string $userId user id + * + * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]] + */ + public function services(string $userId): array { + + // initilize collection + $services = []; + // retrieve and iterate through mail providers + foreach ($this->providers() as $entry) { + // retrieve collection of services + $mailServices = $entry->listServices($userId); + // evaluate if mail services collection is not empty and add results to services collection + if (!empty($mailServices)) { + $services[$entry->id()] = $mailServices; + } + } + // return collection + return $services; + + } + + /** + * Retrieve a service with a specific id + * + * @since 30.0.0 + * + * @param string $userId user id + * @param string $serviceId service id + * @param string $providerId provider id + * + * @return IService|null returns service object or null if none found + */ + public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService | null { + + // evaluate if provider id was specified + if ($providerId !== null) { + // find provider + $provider = $this->findProviderById($providerId); + // evaluate if provider was found + if ($provider instanceof IProvider) { + // find service with specific id + $service = $provider->findServiceById($userId, $serviceId); + // evaluate if mail service was found + if ($service instanceof IService) { + return $service; + } + } + } else { + // retrieve and iterate through mail providers + foreach ($this->providers() as $provider) { + // find service with specific id + $service = $provider->findServiceById($userId, $serviceId); + // evaluate if mail service was found + if ($service instanceof IService) { + return $service; + } + } + } + + // return null if no match was found + return null; + + } + + /** + * Retrieve a service for a specific mail address + * returns first service with specific primary address + * + * @since 30.0.0 + * + * @param string $userId user id + * @param string $address mail address (e.g. test@example.com) + * @param string $providerId provider id + * + * @return IService|null returns service object or null if none found + */ + public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService | null { + + // evaluate if provider id was specified + if ($providerId !== null) { + // find provider + $provider = $this->findProviderById($providerId); + // evaluate if provider was found + if ($provider instanceof IProvider) { + // find service with specific mail address + $service = $provider->findServiceByAddress($userId, $address); + // evaluate if mail service was found + if ($service instanceof IService) { + return $service; + } + } + } else { + // retrieve and iterate through mail providers + foreach ($this->providers() as $provider) { + // find service with specific mail address + $service = $provider->findServiceByAddress($userId, $address); + // evaluate if mail service was found + if ($service instanceof IService) { + return $service; + } + } + } + // return null if no match was found + return null; + + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 4d549918a8f..fd12850a636 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -1023,6 +1023,9 @@ class Server extends ServerContainer implements IServerContainer { /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('Mailer', IMailer::class); + /** @since 30.0.0 */ + $this->registerAlias(\OCP\Mail\Provider\IManager::class, \OC\Mail\Provider\Manager::class); + /** @deprecated 21.0.0 */ $this->registerDeprecatedAlias('LDAPProvider', ILDAPProvider::class); diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index b86f7bcd76d..57e76f268d9 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -17,6 +17,7 @@ use OCP\Collaboration\Reference\IReferenceProvider; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\IContainer; +use OCP\Mail\Provider\IProvider as IMailProvider; use OCP\Notification\INotifier; use OCP\Preview\IProviderV2; use OCP\SpeechToText\ISpeechToTextProvider; @@ -412,4 +413,14 @@ interface IRegistrationContext { * @since 30.0.0 */ public function registerTaskProcessingTaskType(string $taskProcessingTaskTypeClass): void; + + /** + * Register a mail provider + * + * @param string $class + * @psalm-param class-string<IMailProvider> $class + * @since 30.0.0 + */ + public function registerMailProvider(string $class): void; + } diff --git a/lib/public/Mail/Provider/Address.php b/lib/public/Mail/Provider/Address.php new file mode 100644 index 00000000000..9cd2859a8a5 --- /dev/null +++ b/lib/public/Mail/Provider/Address.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Address Object + * + * This object is used to define the address and label of a email address + * + * @since 30.0.0 + * + */ +class Address implements \OCP\Mail\Provider\IAddress { + + /** + * initialize the mail address object + * + * @since 30.0.0 + * + * @param string|null $address mail address (e.g test@example.com) + * @param string|null $label mail address label/name + */ + public function __construct( + protected ?string $address = null, + protected ?string $label = null + ) { + } + + /** + * sets the mail address + * + * @since 30.0.0 + * + * @param string $value mail address (e.g. test@example.com) + * + * @return self return this object for command chaining + */ + public function setAddress(string $value): self { + $this->address = $value; + return $this; + } + + /** + * gets the mail address + * + * @since 30.0.0 + * + * @return string|null returns the mail address or null if one is not set + */ + public function getAddress(): string | null { + return $this->address; + } + + /** + * sets the mail address label/name + * + * @since 30.0.0 + * + * @param string $value mail address label/name + * + * @return self return this object for command chaining + */ + public function setLabel(string $value): self { + $this->label = $value; + return $this; + } + + /** + * gets the mail address label/name + * + * @since 30.0.0 + * + * @return string|null returns the mail address label/name or null if one is not set + */ + public function getLabel(): string | null { + return $this->label; + } + +} diff --git a/lib/public/Mail/Provider/Attachment.php b/lib/public/Mail/Provider/Attachment.php new file mode 100644 index 00000000000..d7790a3bbc6 --- /dev/null +++ b/lib/public/Mail/Provider/Attachment.php @@ -0,0 +1,139 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Attachment Object + * + * This object is used to define the parameters of a mail attachment + * + * @since 30.0.0 + * + */ +class Attachment implements \OCP\Mail\Provider\IAttachment { + + /** + * initialize the mail attachment object + * + * @since 30.0.0 + * + * @param string|null $contents binary contents of file + * @param string|null $name file name (e.g example.txt) + * @param string|null $type mime type (e.g. text/plain) + * @param bool $embedded embedded status of the attachment, default is false + */ + public function __construct( + protected ?string $contents, + protected ?string $name, + protected ?string $type, + protected bool $embedded = false + ) { + } + + /** + * sets the attachment file name + * + * @since 30.0.0 + * + * @param string $value file name (e.g example.txt) + * + * @return self return this object for command chaining + */ + public function setName(string $value): self { + $this->name = $value; + return $this; + } + + /** + * gets the attachment file name + * + * @since 30.0.0 + * + * @return string | null returns the attachment file name or null if not set + */ + public function getName(): string | null { + return $this->name; + } + + /** + * sets the attachment mime type + * + * @since 30.0.0 + * + * @param string $value mime type (e.g. text/plain) + * + * @return self return this object for command chaining + */ + public function setType(string $value): self { + $this->type = $value; + return $this; + } + + /** + * gets the attachment mime type + * + * @since 30.0.0 + * + * @return string | null returns the attachment mime type or null if not set + */ + public function getType(): string | null { + return $this->type; + } + + /** + * sets the attachment contents (actual data) + * + * @since 30.0.0 + * + * @param string $value binary contents of file + * + * @return self return this object for command chaining + */ + public function setContents(string $value): self { + $this->contents = $value; + return $this; + } + + /** + * gets the attachment contents (actual data) + * + * @since 30.0.0 + * + * @return string | null returns the attachment contents or null if not set + */ + public function getContents(): string | null { + return $this->contents; + } + + /** + * sets the embedded status of the attachment + * + * @since 30.0.0 + * + * @param bool $value true - embedded / false - not embedded + * + * @return self return this object for command chaining + */ + public function setEmbedded(bool $value): self { + $this->embedded = $value; + return $this; + } + + /** + * gets the embedded status of the attachment + * + * @since 30.0.0 + * + * @return bool embedded status of the attachment + */ + public function getEmbedded(): bool { + return $this->embedded; + } + +} diff --git a/lib/public/Mail/Provider/IAddress.php b/lib/public/Mail/Provider/IAddress.php new file mode 100644 index 00000000000..b980f31150b --- /dev/null +++ b/lib/public/Mail/Provider/IAddress.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Address Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail address object + * + * @since 30.0.0 + * + */ +interface IAddress { + + /** + * sets the mail address + * + * @since 30.0.0 + * + * @param string $value mail address (test@example.com) + * + * @return self return this object for command chaining + */ + public function setAddress(string $value): self; + + /** + * gets the mail address + * + * @since 30.0.0 + * + * @return string returns the mail address + */ + public function getAddress(): string | null; + + /** + * sets the mail address label/name + * + * @since 30.0.0 + * + * @param string $value mail address label/name + * + * @return self return this object for command chaining + */ + public function setLabel(string $value): self; + + /** + * gets the mail address label/name + * + * @since 30.0.0 + * + * @return string returns the mail address label/name + */ + public function getLabel(): string | null; + +} diff --git a/lib/public/Mail/Provider/IAttachment.php b/lib/public/Mail/Provider/IAttachment.php new file mode 100644 index 00000000000..b5bdffc0e81 --- /dev/null +++ b/lib/public/Mail/Provider/IAttachment.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Attachment Interface + * + * This interface is used for defining individual attachments that are attached to a message + * + * @since 30.0.0 + * + */ +interface IAttachment { + + /** + * sets the attachment file name + * + * @since 30.0.0 + * + * @param string $value file name (e.g example.txt) + * + * @return self return this object for command chaining + */ + public function setName(string $value): self; + + /** + * gets the attachment file name + * + * @since 30.0.0 + * + * @return string | null returns the attachment file name or null if one is not set + */ + public function getName(): string | null; + + /** + * sets the attachment mime type + * + * @since 30.0.0 + * + * @param string $value mime type (e.g. text/plain) + * + * @return self return this object for command chaining + */ + public function setType(string $value): self; + + /** + * gets the attachment mime type + * + * @since 30.0.0 + * + * @return string | null returns the attachment mime type or null if not set + */ + public function getType(): string | null; + + /** + * sets the attachment contents (actual data) + * + * @since 30.0.0 + * + * @param string $value binary contents of file + * + * @return self return this object for command chaining + */ + public function setContents(string $value): self; + + /** + * gets the attachment contents (actual data) + * + * @since 30.0.0 + * + * @return string | null returns the attachment contents or null if not set + */ + public function getContents(): string | null; + + /** + * sets the embedded status of the attachment + * + * @since 30.0.0 + * + * @param bool $value true - embedded / false - not embedded + * + * @return self return this object for command chaining + */ + public function setEmbedded(bool $value): self; + + /** + * gets the embedded status of the attachment + * + * @since 30.0.0 + * + * @return bool embedded status of the attachment + */ + public function getEmbedded(): bool; + +} diff --git a/lib/public/Mail/Provider/IManager.php b/lib/public/Mail/Provider/IManager.php new file mode 100644 index 00000000000..4c06cfff87c --- /dev/null +++ b/lib/public/Mail/Provider/IManager.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Provider Manager Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail provider manager object + * + * @since 30.0.0 + * + */ +interface IManager { + + /** + * determine if any mail providers are registered + * + * @since 30.0.0 + * + * @return bool + */ + public function has(): bool; + + /** + * retrieve a count of how many mail providers are registered + * + * @since 30.0.0 + * + * @return int + */ + public function count(): int; + + /** + * retrieve which mail providers are registered + * + * @since 30.0.0 + * + * @return array<string,String> collection of provider id and label ['jmap' => 'JMap Connector'] + */ + public function types(): array; + + /** + * retrieve all registered mail providers + * + * @since 30.0.0 + * + * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject] + */ + public function providers(): array; + + /** + * retrieve a provider with a specific id + * + * @since 30.0.0 + * + * @param string $providerId provider id + * + * @return IProvider|null + */ + public function findProviderById(string $providerId): IProvider | null; + + /** + * retrieve all services for all registered mail providers + * + * @since 30.0.0 + * + * @param string $userId user id + * + * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]] + */ + public function services(string $userId): array; + + /** + * retrieve a service with a specific id + * + * @since 30.0.0 + * + * @param string $userId user id + * @param string $serviceId service id + * @param string $providerId provider id + * + * @return IService|null returns service object or null if none found + */ + public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService | null; + + /** + * retrieve a service for a specific mail address + * returns first service with specific primary address + * + * @since 30.0.0 + * + * @param string $userId user id + * @param string $address mail address (e.g. test@example.com) + * @param string $providerId provider id + * + * @return IService|null returns service object or null if none found + */ + public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService | null; + +} diff --git a/lib/public/Mail/Provider/IMessage.php b/lib/public/Mail/Provider/IMessage.php new file mode 100644 index 00000000000..ff2b52e5053 --- /dev/null +++ b/lib/public/Mail/Provider/IMessage.php @@ -0,0 +1,232 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Message Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail message object + * + * @since 30.0.0 + * + */ +interface IMessage { + + /** + * arbitrary unique text string identifying this message + * + * @since 30.0.0 + * + * @return string id of this message + */ + public function id(): string; + + /** + * sets the sender of this message + * + * @since 30.0.0 + * + * @param IAddress $value sender's mail address object + * + * @return self return this object for command chaining + */ + public function setFrom(IAddress $value): self; + + /** + * gets the sender of this message + * + * @since 30.0.0 + * + * @param IAddress|null sender's mail address object + */ + public function getFrom(): IAddress | null; + + /** + * sets the sender's reply to address of this message + * + * @since 30.0.0 + * + * @param IAddress $value senders's reply to mail address object + * + * @return self return this object for command chaining + */ + public function setReplyTo(IAddress $value): self; + + /** + * gets the sender's reply to address of this message + * + * @since 30.0.0 + * + * @param IAddress|null sender's reply to mail address object + */ + public function getReplyTo(): IAddress | null; + + /** + * sets the recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setTo(IAddress ...$value): self; + + /** + * gets the recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all recipient mail address objects + */ + public function getTo(): array | null; + + /** + * sets the copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setCc(IAddress ...$value): self; + + /** + * gets the copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all copied recipient mail address objects + */ + public function getCc(): array | null; + + /** + * sets the blind copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setBcc(IAddress ...$value): self; + + /** + * gets the blind copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all blind copied recipient mail address objects + */ + public function getBcc(): array | null; + + /** + * sets the subject of this message + * + * @since 30.0.0 + * + * @param string $value subject of mail message + * + * @return self return this object for command chaining + */ + public function setSubject(string $value): self; + + /** + * gets the subject of this message + * + * @since 30.0.0 + * + * @param string|null subject of message or null if one is not set + */ + public function getSubject(): string | null; + + /** + * sets the plain text or html body of this message + * + * @since 30.0.0 + * + * @param string $value text or html body of message + * @param bool $html html flag - true for html + * + * @return self return this object for command chaining + */ + public function setBody(string $value, bool $html): self; + + /** + * gets either the html or plain text body of this message + * + * html body will be returned over plain text if html body exists + * + * @since 30.0.0 + * + * @param string|null html/plain body of this message or null if one is not set + */ + public function getBody(): string | null; + + /** + * sets the html body of this message + * + * @since 30.0.0 + * + * @param string $value html body of message + * + * @return self return this object for command chaining + */ + public function setBodyHtml(string $value): self; + + /** + * gets the html body of this message + * + * @since 30.0.0 + * + * @param string|null html body of this message or null if one is not set + */ + public function getBodyHtml(): string | null; + + /** + * sets the plain text body of this message + * + * @since 30.0.0 + * + * @param string $value plain text body of message + * + * @return self return this object for command chaining + */ + public function setBodyPlain(string $value): self; + + /** + * gets the plain text body of this message + * + * @since 30.0.0 + * + * @param string|null plain text body of this message or null if one is not set + */ + public function getBodyPlain(): string | null; + + /** + * sets the attachments of this message + * + * @since 30.0.0 + * + * @param IAttachment ...$value collection of or one or more mail attachment objects + * + * @return self return this object for command chaining + */ + public function setAttachments(IAttachment ...$value): self; + + /** + * gets the attachments of this message + * + * @since 30.0.0 + * + * @return array<int,IAttachment>|null collection of all mail attachment objects + */ + public function getAttachments(): array | null; +} diff --git a/lib/public/Mail/Provider/IMessageSend.php b/lib/public/Mail/Provider/IMessageSend.php new file mode 100644 index 00000000000..e68f18cf95c --- /dev/null +++ b/lib/public/Mail/Provider/IMessageSend.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Message Send Interface + * + * This interface is the required set of methods and functionality used to extend IService with message sending functionality + * + * @since 30.0.0 + * + */ +interface IMessageSend { + + /** + * send an outbound message + * + * @since 30.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; + +} diff --git a/lib/public/Mail/Provider/IProvider.php b/lib/public/Mail/Provider/IProvider.php new file mode 100644 index 00000000000..f27f6264753 --- /dev/null +++ b/lib/public/Mail/Provider/IProvider.php @@ -0,0 +1,130 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Provider Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail provider object + * + * @since 30.0.0 + * + */ +interface IProvider { + + /** + * arbitrary unique text string identifying this provider + * + * @since 30.0.0 + * + * @return string id of this provider (e.g. UUID or 'IMAP/SMTP' or anything else) + */ + public function id(): string; + + /** + * localized human friendly name of this provider + * + * @since 30.0.0 + * + * @return string label/name of this provider (e.g. Plain Old IMAP/SMTP) + */ + public function label(): string; + + /** + * determine if any services are configured for a specific user + * + * @since 30.0.0 + * + * @param string $userId user id + * + * @return bool true if any services are configure for the user + */ + public function hasServices(string $userId): bool; + + /** + * retrieve collection of services for a specific user + * + * @param string $userId user id + * + * @since 30.0.0 + * + * @return array<string,IService> collection of service id and object ['1' => IServiceObject] + */ + public function listServices(string $userId): array; + + /** + * retrieve a service with a specific id + * + * @since 30.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; + + /** + * retrieve a service for a specific mail address + * + * @since 30.0.0 + * + * @param string $userId user id + * @param string $address mail address (e.g. test@example.com) + * + * @return IService|null returns service object or null if none found + */ + public function findServiceByAddress(string $userId, string $address): IService | null; + + /** + * construct a new empty service object + * + * @since 30.0.0 + * + * @return IService blank service object + */ + public function initiateService(): IService; + + /** + * Create a service configuration for a specific user + * + * @since 30.0.0 + * + * @param string $userId user id + * @param IService $service service object + * + * @return string id of created service + */ + public function createService(string $userId, IService $service): string; + + /** + * Modify a service configuration for a specific user + * + * @since 30.0.0 + * + * @param string $userId user id + * @param IService $service service object + * + * @return string id of modifided service + */ + public function modifyService(string $userId, IService $service): string; + + /** + * delete a service configuration for a specific user + * + * @since 30.0.0 + * + * @param string $userId user id + * @param IService $service service object + * + * @return bool status of delete action + */ + public function deleteService(string $userId, IService $service): bool; + +} diff --git a/lib/public/Mail/Provider/IService.php b/lib/public/Mail/Provider/IService.php new file mode 100644 index 00000000000..65056418c67 --- /dev/null +++ b/lib/public/Mail/Provider/IService.php @@ -0,0 +1,150 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Service Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail service object + * + * @since 30.0.0 + * + */ +interface IService { + + /** + * arbitrary unique text string identifying this service + * + * @since 30.0.0 + * + * @return string id of this service (e.g. 1 or service1 or anything else) + */ + public function id(): string; + + /** + * checks or retrieves what capabilites the service has + * + * @since 30.0.0 + * + * @param string $ability required ability e.g. 'MessageSend' + * + * @return bool|array true/false if ability is supplied, collection of abilities otherwise + */ + public function capable(?string $ability = null): bool | array; + + /** + * gets the localized human frendly name of this service + * + * @since 30.0.0 + * + * @return string label/name of service (e.g. ACME Company Mail Service) + */ + public function getLabel(): string; + + /** + * sets the localized human frendly name of this service + * + * @since 30.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; + + /** + * gets service itentity + * + * @since 30.0.0 + * + * @return IServiceIdentity service identity object + */ + public function getIdentity(): IServiceIdentity | null; + + /** + * sets service identity + * + * @since 30.0.0 + * + * @param IServiceIdentity $value service identity object + * + * @return self return this object for command chaining + */ + public function setIdentity(IServiceIdentity $value): self; + + /** + * gets service location + * + * @since 30.0.0 + * + * @return IServiceLocation service location object + */ + public function getLocation(): IServiceLocation | null; + + /** + * sets service location + * + * @since 30.0.0 + * + * @param IServiceLocation $value service location object + * + * @return self return this object for command chaining + */ + public function setLocation(IServiceLocation $value): self; + + /** + * gets the primary mailing address for this service + * + * @since 30.0.0 + * + * @return IAddress mail address object + */ + public function getPrimaryAddress(): IAddress; + + /** + * sets the primary mailing address for this service + * + * @since 30.0.0 + * + * @param IAddress $value mail address object + * + * @return self return this object for command chaining + */ + public function setPrimaryAddress(IAddress $value): self; + + /** + * gets the secondary mailing addresses (aliases) collection for this service + * + * @since 30.0.0 + * + * @return array<int, IAddress> collection of mail address objects + */ + public function getSecondaryAddresses(): array; + + /** + * sets the secondary mailing addresses (aliases) for this service + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setSecondaryAddresses(IAddress ...$value): self; + + /** + * construct a new empty message object + * + * @since 30.0.0 + * + * @return IMessage blank message object + */ + public function initiateMessage(): IMessage; + +} diff --git a/lib/public/Mail/Provider/IServiceIdentity.php b/lib/public/Mail/Provider/IServiceIdentity.php new file mode 100644 index 00000000000..7b52dec9dcd --- /dev/null +++ b/lib/public/Mail/Provider/IServiceIdentity.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Service Identity Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail service identity. + * + * @since 30.0.0 + * + */ +interface IServiceIdentity { + + /** + * arbitrary unique text string identifying this credential type + * + * @since 30.0.0 + * + * @return string + */ + public function type(): string; + +} diff --git a/lib/public/Mail/Provider/IServiceLocation.php b/lib/public/Mail/Provider/IServiceLocation.php new file mode 100644 index 00000000000..00696e55153 --- /dev/null +++ b/lib/public/Mail/Provider/IServiceLocation.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Service Location Interface + * + * This interface is a base requirement of methods and functionality used to construct a mail service location. + * + * @since 30.0.0 + * + */ +interface IServiceLocation { + + /** + * arbitrary unique text string identifying this location type + * + * @since 30.0.0 + * + * @return string + */ + public function type(): string; + +} diff --git a/lib/public/Mail/Provider/Message.php b/lib/public/Mail/Provider/Message.php new file mode 100644 index 00000000000..8bf07b34f70 --- /dev/null +++ b/lib/public/Mail/Provider/Message.php @@ -0,0 +1,338 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Mail\Provider; + +/** + * Mail Message Object + * + * This object is used to define a mail message that can be used to transfer data to a provider + * + * @since 30.0.0 + * + */ +class Message implements \OCP\Mail\Provider\IMessage { + + /** + * initialize the mail message object + * + * @since 30.0.0 + * + * @param array $data message data array + */ + public function __construct( + protected array $data = [], + ) { + } + + /** + * arbitrary unique text string identifying this message + * + * @since 30.0.0 + * + * @return string id of this message + */ + public function id(): string { + // return id of message + return (isset($this->data['id'])) ? $this->data['id'] : ''; + } + + /** + * sets the sender of this message + * + * @since 30.0.0 + * + * @param IAddress $value sender's mail address object + * + * @return self return this object for command chaining + */ + public function setFrom(IAddress $value): self { + // create or update field in data store with value + $this->data['from'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the sender of this message + * + * @since 30.0.0 + * + * @param IAddress|null sender's mail address object + */ + public function getFrom(): IAddress | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['from'])) ? $this->data['from'] : null; + } + + /** + * sets the sender's reply to address of this message + * + * @since 30.0.0 + * + * @param IAddress $value senders's reply to mail address object + * + * @return self return this object for command chaining + */ + public function setReplyTo(IAddress $value): self { + // create or update field in data store with value + $this->data['replyTo'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the sender's reply to address of this message + * + * @since 30.0.0 + * + * @param IAddress|null sender's reply to mail address object + */ + public function getReplyTo(): IAddress | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['replyTo'])) ? $this->data['replyTo'] : null; + } + + /** + * sets the recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setTo(IAddress ...$value): self { + // create or update field in data store with value + $this->data['to'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all recipient mail address objects + */ + public function getTo(): array | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['to'])) ? $this->data['to'] : null; + } + + /** + * sets the copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setCc(IAddress ...$value): self { + // create or update field in data store with value + $this->data['cc'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all copied recipient mail address objects + */ + public function getCc(): array | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['cc'])) ? $this->data['cc'] : null; + } + + /** + * sets the blind copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param IAddress ...$value collection of or one or more mail address objects + * + * @return self return this object for command chaining + */ + public function setBcc(IAddress ...$value): self { + // create or update field in data store with value + $this->data['bcc'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the blind copy to recipient(s) of this message + * + * @since 30.0.0 + * + * @param array<int,IAddress>|null collection of all blind copied recipient mail address objects + */ + public function getBcc(): array | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['bcc'])) ? $this->data['bcc'] : null; + } + + /** + * sets the subject of this message + * + * @since 30.0.0 + * + * @param string $value subject of mail message + * + * @return self return this object for command chaining + */ + public function setSubject(string $value): self { + // create or update field in data store with value + $this->data['subject'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the subject of this message + * + * @since 30.0.0 + * + * @param string|null subject of message or null if one is not set + */ + public function getSubject(): string | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['subject'])) ? $this->data['subject'] : null; + } + + /** + * sets the plain text or html body of this message + * + * @since 30.0.0 + * + * @param string $value text or html body of message + * @param bool $html html flag - true for html + * + * @return self return this object for command chaining + */ + public function setBody(string $value, bool $html = false): self { + // evaluate html flag and create or update appropriate field in data store with value + if ($html) { + $this->data['bodyHtml'] = $value; + } else { + $this->data['bodyPlain'] = $value; + } + // return this object for command chaining + return $this; + } + + /** + * gets either the html or plain text body of this message + * + * html body will be returned over plain text if html body exists + * + * @since 30.0.0 + * + * @param string|null html/plain body of this message or null if one is not set + */ + public function getBody(): string | null { + // evaluate if data store field(s) exists and return value + if (isset($this->data['bodyHtml'])) { + return $this->data['bodyHtml']; + } elseif (isset($this->data['bodyPlain'])) { + return $this->data['bodyPlain']; + } + // return null if data fields did not exist in data store + return null; + } + + /** + * sets the html body of this message + * + * @since 30.0.0 + * + * @param string $value html body of message + * + * @return self return this object for command chaining + */ + public function setBodyHtml(string $value): self { + // create or update field in data store with value + $this->data['bodyHtml'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the html body of this message + * + * @since 30.0.0 + * + * @param string|null html body of this message or null if one is not set + */ + public function getBodyHtml(): string | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['bodyHtml'])) ? $this->data['bodyHtml'] : null; + } + + /** + * sets the plain text body of this message + * + * @since 30.0.0 + * + * @param string $value plain text body of message + * + * @return self return this object for command chaining + */ + public function setBodyPlain(string $value): self { + // create or update field in data store with value + $this->data['bodyPlain'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the plain text body of this message + * + * @since 30.0.0 + * + * @param string|null plain text body of this message or null if one is not set + */ + public function getBodyPlain(): string | null { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['bodyPlain'])) ? $this->data['bodyPlain'] : null; + } + + /** + * sets the attachments of this message + * + * @since 30.0.0 + * + * @param IAttachment ...$value collection of or one or more mail attachment objects + * + * @return self return this object for command chaining + */ + public function setAttachments(IAttachment ...$value): self { + // create or update field in data store with value + $this->data['attachments'] = $value; + // return this object for command chaining + return $this; + } + + /** + * gets the attachments of this message + * + * @since 30.0.0 + * + * @return array<int,IAttachment> collection of all mail attachment objects + */ + public function getAttachments(): array { + // evaluate if data store field exists and return value(s) or null otherwise + return (isset($this->data['attachments'])) ? $this->data['attachments'] : []; + } + +} diff --git a/tests/lib/Mail/Provider/AddressTest.php b/tests/lib/Mail/Provider/AddressTest.php new file mode 100644 index 00000000000..5df22977bff --- /dev/null +++ b/tests/lib/Mail/Provider/AddressTest.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\Mail\Provider; + +use OCP\Mail\Provider\Address; +use Test\TestCase; + +class AddressTest extends TestCase { + + /** @var Address*/ + private $address; + + protected function setUp(): void { + parent::setUp(); + + $this->address = new Address('user1@testing.com', 'User One'); + + } + + public function testAddress(): void { + + // test set by constructor + $this->assertEquals('user1@testing.com', $this->address->getAddress()); + // test set by setter + $this->address->setAddress('user2@testing.com'); + $this->assertEquals('user2@testing.com', $this->address->getAddress()); + + } + + public function testLabel(): void { + + // test set by constructor + $this->assertEquals('User One', $this->address->getLabel()); + // test set by setter + $this->address->setLabel('User Two'); + $this->assertEquals('User Two', $this->address->getLabel()); + + } + +} diff --git a/tests/lib/Mail/Provider/AttachmentTest.php b/tests/lib/Mail/Provider/AttachmentTest.php new file mode 100644 index 00000000000..e5b254aacb9 --- /dev/null +++ b/tests/lib/Mail/Provider/AttachmentTest.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\Mail\Provider; + +use OCP\Mail\Provider\Attachment; +use Test\TestCase; + +class AttachmentTest extends TestCase { + + /** @var Attachment*/ + private $attachment; + + protected function setUp(): void { + parent::setUp(); + + $this->attachment = new Attachment( + 'This is the contents of a file', + 'example1.txt', + 'text/plain', + false + ); + + } + + public function testName(): void { + + // test set by constructor + $this->assertEquals('example1.txt', $this->attachment->getName()); + // test set by setter + $this->attachment->setName('example2.txt'); + $this->assertEquals('example2.txt', $this->attachment->getName()); + + } + + public function testType(): void { + + // test set by constructor + $this->assertEquals('text/plain', $this->attachment->getType()); + // test set by setter + $this->attachment->setType('text/html'); + $this->assertEquals('text/html', $this->attachment->getType()); + + } + + public function testContents(): void { + + // test set by constructor + $this->assertEquals('This is the contents of a file', $this->attachment->getContents()); + // test set by setter + $this->attachment->setContents('This is the modified contents of a file'); + $this->assertEquals('This is the modified contents of a file', $this->attachment->getContents()); + + } + + public function testEmbedded(): void { + + // test set by constructor + $this->assertEquals(false, $this->attachment->getEmbedded()); + // test set by setter + $this->attachment->setEmbedded(true); + $this->assertEquals(true, $this->attachment->getEmbedded()); + + } + +} diff --git a/tests/lib/Mail/Provider/ManagerTest.php b/tests/lib/Mail/Provider/ManagerTest.php new file mode 100644 index 00000000000..76ed953cfda --- /dev/null +++ b/tests/lib/Mail/Provider/ManagerTest.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\Mail\Provider; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Bootstrap\RegistrationContext; +use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\Mail\Provider\Manager; +use OCP\Mail\Provider\Address; +use OCP\Mail\Provider\IProvider; +use OCP\Mail\Provider\IService; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class ManagerTest extends TestCase { + + /** @var CoordinatorMockObject*/ + private $coordinator; + /** @var ContainerInterfaceMockObject*/ + private $container; + /** @var LoggerInterfaceMockObject*/ + private $logger; + /** @var IProviderMockObject*/ + private $provider; + /** @var IServiceMockObject*/ + private $service; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->createMock(LoggerInterface::class); + + // construct service registration + $registration = $this->createMock(ServiceRegistration::class); + $registration + ->method('getService') + ->willReturn('Mock\Provider\MailProvider'); + // construct registration context + $context = $this->createMock(RegistrationContext::class); + $context + ->method('getMailProviders') + ->willReturn([$registration]); + // construct coordinator + $this->coordinator = $this->createMock(Coordinator::class); + $this->coordinator + ->method('getRegistrationContext') + ->willReturn($context); + + // construct mail service + $this->service = $this->createMock(IService::class); + $this->service + ->method('id') + ->willReturn('100'); + $this->service + ->method('getLabel') + ->willReturn('Mock Mail Service'); + $this->service + ->method('getPrimaryAddress') + ->willReturn((new Address('user1@testing.com', 'User One'))); + // construct mail provider + $this->provider = $this->createMock(IProvider::class); + $this->provider + ->method('id') + ->willReturn('mock-provider'); + $this->provider + ->method('label') + ->willReturn('Mock Provider'); + $this->provider + ->method('listServices') + ->willReturnMap([ + ['user0', []], + ['user1', [$this->service->id() => $this->service]] + ]); + $this->provider + ->method('findServiceById') + ->willReturnMap([ + ['user0', '100', null], + ['user1', '100', $this->service] + ]); + $this->provider + ->method('findServiceByAddress') + ->willReturnMap([ + ['user0', 'user0@testing.com', null], + ['user1', 'user1@testing.com', $this->service] + ]); + // construct container interface + $this->container = $this->createMock(ContainerInterface::class); + $this->container + ->method('get') + ->willReturnMap([ + ['Mock\Provider\MailProvider', $this->provider] + ]); + + } + + public function testHas(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with providers found + $this->assertTrue($manager->has()); + + } + + public function testCount(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with providers found + $this->assertGreaterThan(0, $manager->count()); + + } + + public function testTypes(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with providers found + $this->assertEquals(['mock-provider' => 'Mock Provider'], $manager->types()); + + } + + public function testProviders(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with providers found + $this->assertEquals([$this->provider->id() => $this->provider], $manager->providers()); + + } + + public function testFindProviderById(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with providers found + $this->assertEquals($this->provider, $manager->findProviderById($this->provider->id())); + + } + + public function testServices(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with no services found + $this->assertEquals([], $manager->services('user0')); + // test result with services found + $this->assertEquals([$this->provider->id() => [$this->service->id() => $this->service]], $manager->services('user1')); + + } + + public function testFindServiceById(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with no services found and not provider specified + $this->assertEquals(null, $manager->findServiceById('user0', '100')); + // test result with no services found and provider specified + $this->assertEquals(null, $manager->findServiceById('user0', '100', $this->provider->id())); + // test result with services found and not provider specified + $this->assertEquals($this->service, $manager->findServiceById('user1', '100')); + // test result with services found and provider specified + $this->assertEquals($this->service, $manager->findServiceById('user1', '100', $this->provider->id())); + + } + + public function testFindServiceByAddress(): void { + + // construct mail manager + $manager = new Manager($this->coordinator, $this->container, $this->logger); + // test result with no services found and not provider specified + $this->assertEquals(null, $manager->findServiceByAddress('user0', 'user0@testing.com')); + // test result with no services found and provider specified + $this->assertEquals(null, $manager->findServiceByAddress('user0', 'user0@testing.com', $this->provider->id())); + // test result with services found and not provider specified + $this->assertEquals($this->service, $manager->findServiceByAddress('user1', 'user1@testing.com')); + // test result with services found and provider specified + $this->assertEquals($this->service, $manager->findServiceByAddress('user1', 'user1@testing.com', $this->provider->id())); + + } + +} diff --git a/tests/lib/Mail/Provider/MessageTest.php b/tests/lib/Mail/Provider/MessageTest.php new file mode 100644 index 00000000000..546d1f87e32 --- /dev/null +++ b/tests/lib/Mail/Provider/MessageTest.php @@ -0,0 +1,163 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\Mail\Provider; + +use OCP\Mail\Provider\Address; +use OCP\Mail\Provider\Attachment; +use OCP\Mail\Provider\Message; +use Test\TestCase; + +class MessageTest extends TestCase { + + /** @var Message*/ + private $message; + /** @var Address*/ + private $address1; + /** @var Address*/ + private $address2; + /** @var Attachment*/ + private $attachment1; + /** @var Attachment*/ + private $attachment2; + + protected function setUp(): void { + parent::setUp(); + + $this->message = new Message( + ['id' => 'cd02ea42-feac-4863-b9d8-484d16a587ea'] + ); + $this->address1 = new Address( + 'user1@testing.com', + 'User One' + ); + $this->address2 = new Address( + 'user2@testing.com', + 'User Two' + ); + $this->attachment1 = new Attachment( + 'This is the contents of the first attachment', + 'example1.txt', + 'text/plain', + false + ); + $this->attachment2 = new Attachment( + 'This is the contents of the second attachment', + 'example1.txt', + 'text/plain', + false + ); + + } + + public function testId(): void { + + // test set by constructor + $this->assertEquals('cd02ea42-feac-4863-b9d8-484d16a587ea', $this->message->id()); + + } + + public function testFrom(): void { + + // test not set + $this->assertNull($this->message->getFrom()); + // test set by setter + $this->message->setFrom($this->address1); + $this->assertEquals($this->address1, $this->message->getFrom()); + + } + + public function testReplyTo(): void { + + // test not set + $this->assertNull($this->message->getReplyTo()); + // test set by setter + $this->message->setReplyTo($this->address1); + $this->assertEquals($this->address1, $this->message->getReplyTo()); + + } + + public function testTo(): void { + + // test not set + $this->assertNull($this->message->getTo()); + // test set by setter single + $this->message->setTo($this->address1); + $this->assertEquals([$this->address1], $this->message->getTo()); + // test set by setter multiple + $this->message->setTo($this->address1, $this->address2); + $this->assertEquals([$this->address1, $this->address2], $this->message->getTo()); + + } + + public function testCc(): void { + + // test not set + $this->assertNull($this->message->getCc()); + // test set by setter single + $this->message->setCc($this->address1); + $this->assertEquals([$this->address1], $this->message->getCc()); + // test set by setter multiple + $this->message->setCc($this->address1, $this->address2); + $this->assertEquals([$this->address1, $this->address2], $this->message->getCc()); + + } + + public function testBcc(): void { + + // test not set + $this->assertNull($this->message->getBcc()); + // test set by setter single + $this->message->setBcc($this->address1); + $this->assertEquals([$this->address1], $this->message->getBcc()); + // test set by setter multiple + $this->message->setBcc($this->address1, $this->address2); + $this->assertEquals([$this->address1, $this->address2], $this->message->getBcc()); + + } + + public function testSubject(): void { + + // test not set + $this->assertNull($this->message->getSubject()); + // test set by setter + $this->message->setSubject('Testing Mail Subject'); + $this->assertEquals('Testing Mail Subject', $this->message->getSubject()); + + } + + public function testBody(): void { + + // test not set + $this->assertNull($this->message->getBody()); + // test set by setter - text body + $this->message->setBody('Testing Text Body', false); + $this->assertEquals('Testing Text Body', $this->message->getBody()); + $this->message->setBodyPlain('Testing Text Body Again', false); + $this->assertEquals('Testing Text Body Again', $this->message->getBodyPlain()); + // test set by setter - html body + $this->message->setBody('Testing HTML Body', true); + $this->assertEquals('Testing HTML Body', $this->message->getBody()); + $this->message->setBodyHtml('Testing HTML Body Again', false); + $this->assertEquals('Testing HTML Body Again', $this->message->getBodyHtml()); + + } + + public function testAttachments(): void { + + // test not set + $this->assertEquals([], $this->message->getAttachments()); + // test set by setter single + $this->message->setAttachments($this->attachment1); + $this->assertEquals([$this->attachment1], $this->message->getAttachments()); + // test set by setter multiple + $this->message->setAttachments($this->attachment1, $this->attachment2); + $this->assertEquals([$this->attachment1, $this->attachment2], $this->message->getAttachments()); + + } +} -- GitLab