]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat: mail provider backend 45383/head
authorSebastianKrupinski <krupinskis05@gmail.com>
Thu, 16 May 2024 23:06:34 +0000 (19:06 -0400)
committerSebastianKrupinski <krupinskis05@gmail.com>
Tue, 23 Jul 2024 20:20:36 +0000 (16:20 -0400)
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
25 files changed:
apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
apps/dav/lib/Server.php
apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/AppFramework/Bootstrap/RegistrationContext.php
lib/private/Mail/Provider/Manager.php [new file with mode: 0644]
lib/private/Server.php
lib/public/AppFramework/Bootstrap/IRegistrationContext.php
lib/public/Mail/Provider/Address.php [new file with mode: 0644]
lib/public/Mail/Provider/Attachment.php [new file with mode: 0644]
lib/public/Mail/Provider/Exception/Exception.php [new file with mode: 0644]
lib/public/Mail/Provider/Exception/SendException.php [new file with mode: 0644]
lib/public/Mail/Provider/IAddress.php [new file with mode: 0644]
lib/public/Mail/Provider/IAttachment.php [new file with mode: 0644]
lib/public/Mail/Provider/IManager.php [new file with mode: 0644]
lib/public/Mail/Provider/IMessage.php [new file with mode: 0644]
lib/public/Mail/Provider/IMessageSend.php [new file with mode: 0644]
lib/public/Mail/Provider/IProvider.php [new file with mode: 0644]
lib/public/Mail/Provider/IService.php [new file with mode: 0644]
lib/public/Mail/Provider/Message.php [new file with mode: 0644]
tests/lib/Mail/Provider/AddressTest.php [new file with mode: 0644]
tests/lib/Mail/Provider/AttachmentTest.php [new file with mode: 0644]
tests/lib/Mail/Provider/ManagerTest.php [new file with mode: 0644]
tests/lib/Mail/Provider/MessageTest.php [new file with mode: 0644]

index ef506d1593c285c21da6a79b54c5917d68c20f1e..1958531630ade8f71b56b04e5c54720e93013f93 100644 (file)
@@ -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)]);
index 9e6d601649f200fa1016f1d6cbf3a9ea0f172ea1..4fdd70d05c738109f47802df8ea84fcf3fb00451 100644 (file)
@@ -292,7 +292,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());
index eb6bd204bdd62eb2ad8110e3626ae4dc9c2b7427..104f7cd7f5176b9e068053d489c39add11b413ce 100644 (file)
@@ -18,6 +18,10 @@ use OCP\Mail\IAttachment;
 use OCP\Mail\IEMailTemplate;
 use OCP\Mail\IMailer;
 use OCP\Mail\IMessage;
+use OCP\Mail\Provider\IManager as IMailManager;
+use OCP\Mail\Provider\IMessage as IMailMessageNew;
+use OCP\Mail\Provider\IMessageSend as IMailMessageSend;
+use OCP\Mail\Provider\IService as IMailService;
 use PHPUnit\Framework\MockObject\MockObject;
 use Psr\Log\LoggerInterface;
 use Sabre\VObject\Component\VCalendar;
@@ -26,6 +30,11 @@ use Sabre\VObject\ITip\Message;
 use Test\TestCase;
 use function array_merge;
 
+interface IMailServiceMock extends IMailService, IMailMessageSend {
+       // workaround for creating mock class with multiple interfaces
+       // TODO: remove after phpUnit 10 is supported.
+}
+
 class IMipPluginTest extends TestCase {
 
        /** @var IMessage|MockObject */
@@ -67,6 +76,15 @@ class IMipPluginTest extends TestCase {
        /** @var EventComparisonService|MockObject */
        private $eventComparisonService;
 
+       /** @var IMailManager|MockObject */
+       private $mailManager;
+
+       /** @var IMailService|IMailMessageSend|MockObject */
+       private $mailService;
+
+       /** @var IMailMessageNew|MockObject */
+       private $mailMessageNew;
+
        protected function setUp(): void {
                $this->mailMessage = $this->createMock(IMessage::class);
                $this->mailMessage->method('setFrom')->willReturn($this->mailMessage);
@@ -90,10 +108,6 @@ class IMipPluginTest extends TestCase {
                $this->config = $this->createMock(IConfig::class);
 
                $this->user = $this->createMock(IUser::class);
-               /*
-               $this->user->method('getUID');
-               $this->user->method('getDisplayName');
-               */
 
                $this->userSession = $this->createMock(IUserSession::class);
                $this->userSession->method('getUser')
@@ -107,6 +121,12 @@ class IMipPluginTest extends TestCase {
 
                $this->eventComparisonService = $this->createMock(EventComparisonService::class);
 
+               $this->mailManager = $this->createMock(IMailManager::class);
+
+               $this->mailService = $this->createMock(IMailServiceMock::class);
+
+               $this->mailMessageNew = $this->createMock(IMailMessageNew::class);
+
                $this->plugin = new IMipPlugin(
                        $this->config,
                        $this->mailer,
@@ -115,7 +135,8 @@ class IMipPluginTest extends TestCase {
                        $this->defaults,
                        $this->userSession,
                        $this->service,
-                       $this->eventComparisonService
+                       $this->eventComparisonService,
+                       $this->mailManager,
                );
        }
 
@@ -582,6 +603,111 @@ class IMipPluginTest extends TestCase {
                $this->assertEquals('5.0', $message->getScheduleStatus());
        }
 
+       public function testMailProviderSend(): void {
+               // construct iTip message with event and attendees
+               $message = new Message();
+               $message->method = 'REQUEST';
+               $calendar = new VCalendar();
+               $event = new VEvent($calendar, 'one', array_merge([
+                       'UID' => 'uid-1234',
+                       'SEQUENCE' => 1,
+                       'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+                       'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+               ], []));
+               $event->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+               $event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE',  'CN' => 'Frodo']);
+               $message->message = $calendar;
+               $message->sender = 'mailto:gandalf@wiz.ard';
+               $message->senderName = 'Mr. Wizard';
+               $message->recipient = 'mailto:' . 'frodo@hobb.it';
+               // construct
+               foreach ($event->select('ATTENDEE') as $entry) {
+                       if (strcasecmp($entry->getValue(), $message->recipient) === 0) {
+                               $attendee = $entry;
+                       }
+               }
+               // construct body data return
+               $data = ['invitee_name' => 'Mr. Wizard',
+                       'meeting_title' => 'Fellowship meeting without (!) Boromir',
+                       'attendee_name' => 'frodo@hobb.it'
+               ];
+               // construct system config mock returns
+               $this->config->expects(self::once())
+                       ->method('getAppValue')
+                       ->with('dav', 'invitation_link_recipients', 'yes')
+                       ->willReturn('yes');
+               // construct user mock returns
+               $this->user->expects(self::any())
+                       ->method('getUID')
+                       ->willReturn('user1');
+               $this->user->expects(self::any())
+                       ->method('getDisplayName')
+                       ->willReturn('Mr. Wizard');
+               // construct user session mock returns
+               $this->userSession->expects(self::any())
+                       ->method('getUser')
+                       ->willReturn($this->user);
+               // construct service mock returns
+               $this->service->expects(self::once())
+                       ->method('getLastOccurrence')
+                       ->willReturn('1496912700');
+               $this->service->expects(self::once())
+                       ->method('getCurrentAttendee')
+                       ->with($message)
+                       ->willReturn($attendee);
+               $this->service->expects(self::once())
+                       ->method('isRoomOrResource')
+                       ->with($attendee)
+                       ->willReturn(false);
+               $this->service->expects(self::once())
+                       ->method('buildBodyData')
+                       ->with($event, null)
+                       ->willReturn($data);
+               $this->service->expects(self::once())
+                       ->method('getFrom');
+               $this->service->expects(self::once())
+                       ->method('addSubjectAndHeading')
+                       ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false);
+               $this->service->expects(self::once())
+                       ->method('addBulletList')
+                       ->with($this->emailTemplate, $event, $data);
+               $this->service->expects(self::once())
+                       ->method('getAttendeeRsvpOrReqForParticipant')
+                       ->willReturn(true);
+               $this->service->expects(self::once())
+                       ->method('createInvitationToken')
+                       ->with($message, $event, '1496912700')
+                       ->willReturn('token');
+               $this->service->expects(self::once())
+                       ->method('addResponseButtons')
+                       ->with($this->emailTemplate, 'token');
+               $this->service->expects(self::once())
+                       ->method('addMoreOptionsButton')
+                       ->with($this->emailTemplate, 'token');
+               $this->eventComparisonService->expects(self::once())
+                       ->method('findModified')
+                       ->willReturn(['old' => [] ,'new' => [$event]]);
+               // construct mail mock returns
+               $this->mailer->expects(self::once())
+                       ->method('validateMailAddress')
+                       ->with('frodo@hobb.it')
+                       ->willReturn(true);
+               // construct mail provider mock returns
+               $this->mailService
+                       ->method('initiateMessage')
+                       ->willReturn($this->mailMessageNew);
+               $this->mailService
+                       ->method('sendMessage')
+                       ->with($this->mailMessageNew);
+               $this->mailManager
+                       ->method('findServiceByAddress')
+                       ->with('user1', 'gandalf@wiz.ard')
+                       ->willReturn($this->mailService);
+               
+               $this->plugin->schedule($message);
+               $this->assertEquals('1.1', $message->getScheduleStatus());
+       }
+
        public function testNoOldEvent(): void {
                $message = new Message();
                $message->method = 'REQUEST';
index 7a2c2c6d227f3d7b2d23f7783325aaf63467c9b5..379a1edbe6338bd175c6089b9843c6cbc38b99fc 100644 (file)
@@ -568,6 +568,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\\Exception\\Exception' => $baseDir . '/lib/public/Mail/Provider/Exception/Exception.php',
+    'OCP\\Mail\\Provider\\Exception\\SendException' => $baseDir . '/lib/public/Mail/Provider/Exception/SendException.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\\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',
@@ -1618,6 +1630,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',
index 2f733673312fd3abbc6823d71075c6b5f4ad4d00..41109fd5f3d2bcc58e5806c3f1dfa3973ac730ed 100644 (file)
@@ -601,6 +601,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\\Exception\\Exception' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Exception/Exception.php',
+        'OCP\\Mail\\Provider\\Exception\\SendException' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Exception/SendException.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\\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',
@@ -1651,6 +1663,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',
index df03d59ebfafec99f9be65e7bf8dea656ad4dab2..f59d5b557064b176c066e9d65864105182be12b5 100644 (file)
@@ -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 (file)
index 0000000..244aa86
--- /dev/null
@@ -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;
+
+       }
+}
index cfbb6dc317fa5d5f402cc582747ac764a95ce02c..3d15645492036bbea56be047530b938f7e803e44 100644 (file)
@@ -1026,6 +1026,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);
 
index b86f7bcd76d71e10d0816b172c2061240a1d2e9e..57e76f268d986544b4ef051744e2801623d429a0 100644 (file)
@@ -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 (file)
index 0000000..9cd2859
--- /dev/null
@@ -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 (file)
index 0000000..d7790a3
--- /dev/null
@@ -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/Exception/Exception.php b/lib/public/Mail/Provider/Exception/Exception.php
new file mode 100644 (file)
index 0000000..7514c72
--- /dev/null
@@ -0,0 +1,18 @@
+<?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\Exception;
+
+/**
+ * Mail Provider Base Exception
+ *
+ * @since 30.0.0
+ */
+class Exception extends \Exception {
+
+}
diff --git a/lib/public/Mail/Provider/Exception/SendException.php b/lib/public/Mail/Provider/Exception/SendException.php
new file mode 100644 (file)
index 0000000..fba0903
--- /dev/null
@@ -0,0 +1,18 @@
+<?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\Exception;
+
+/**
+ * Mail Provider Send Exception
+ *
+ * @since 30.0.0
+ */
+class SendException extends Exception {
+
+}
diff --git a/lib/public/Mail/Provider/IAddress.php b/lib/public/Mail/Provider/IAddress.php
new file mode 100644 (file)
index 0000000..b980f31
--- /dev/null
@@ -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 (file)
index 0000000..b5bdffc
--- /dev/null
@@ -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 (file)
index 0000000..4c06cff
--- /dev/null
@@ -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 (file)
index 0000000..7102198
--- /dev/null
@@ -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>                           collection of all recipient mail address objects
+        */
+       public function getTo(): array;
+
+       /**
+        * 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>                           collection of all copied recipient mail address objects
+        */
+       public function getCc(): array;
+
+       /**
+        * 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>                           collection of all blind copied recipient mail address objects
+        */
+       public function getBcc(): array;
+
+       /**
+        * 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>                       collection of all mail attachment objects
+        */
+       public function getAttachments(): array;
+}
diff --git a/lib/public/Mail/Provider/IMessageSend.php b/lib/public/Mail/Provider/IMessageSend.php
new file mode 100644 (file)
index 0000000..e68f18c
--- /dev/null
@@ -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 (file)
index 0000000..47d6002
--- /dev/null
@@ -0,0 +1,94 @@
+<?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;
+
+}
diff --git a/lib/public/Mail/Provider/IService.php b/lib/public/Mail/Provider/IService.php
new file mode 100644 (file)
index 0000000..a8fd737
--- /dev/null
@@ -0,0 +1,119 @@
+<?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 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;
+
+       /**
+        * 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;
+
+       /**
+        * 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 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/Message.php b/lib/public/Mail/Provider/Message.php
new file mode 100644 (file)
index 0000000..5863896
--- /dev/null
@@ -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>                           collection of all recipient mail address objects
+        */
+       public function getTo(): array {
+               // evaluate if data store field exists and return value(s) or empty collection
+               return (isset($this->data['to'])) ? $this->data['to'] : [];
+       }
+
+       /**
+        * 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>             collection of all copied recipient mail address objects
+        */
+       public function getCc(): array {
+               // evaluate if data store field exists and return value(s) or empty collection
+               return (isset($this->data['cc'])) ? $this->data['cc'] : [];
+       }
+
+       /**
+        * 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>              collection of all blind copied recipient mail address objects
+        */
+       public function getBcc(): array {
+               // evaluate if data store field exists and return value(s) or empty collection
+               return (isset($this->data['bcc'])) ? $this->data['bcc'] : [];
+       }
+
+       /**
+        * 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 (file)
index 0000000..ee03f6f
--- /dev/null
@@ -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&MockObject */
+       private Address $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 (file)
index 0000000..2833916
--- /dev/null
@@ -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&MockObject */
+       private Attachment $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 (file)
index 0000000..2658c32
--- /dev/null
@@ -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 Coordinator&MockObject */
+       private Coordinator $coordinator;
+       /** @var ContainerInterface&MockObject */
+       private ContainerInterface $container;
+       /** @var LoggerInterface&MockObject */
+       private LoggerInterface $logger;
+       /** @var IProvider&MockObject */
+       private IProvider $provider;
+       /** @var IService&MockObject */
+       private IService $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 (file)
index 0000000..1791a03
--- /dev/null
@@ -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&MockObject */
+       private Message $message;
+       /** @var Address&MockObject */
+       private Address $address1;
+       /** @var Address&MockObject */
+       private Address $address2;
+       /** @var Attachment&MockObject */
+       private Attachment $attachment1;
+       /** @var Attachment&MockObject */
+       private Attachment $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->assertEquals([], $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->assertEquals([], $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->assertEquals([], $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());
+
+       }
+}