From e7d0dfc55bb8874abe3b6c03b6f5e3e3917c43bf Mon Sep 17 00:00:00 2001
From: SebastianKrupinski <krupinskis05@gmail.com>
Date: Thu, 16 May 2024 19:06:34 -0400
Subject: [PATCH] feat: mail provider backend

Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
---
 apps/dav/lib/CalDAV/Schedule/IMipPlugin.php   |  82 +++--
 apps/dav/lib/Server.php                       |   3 +-
 .../unit/CalDAV/Schedule/IMipPluginTest.php   |   9 +-
 lib/composer/composer/autoload_classmap.php   |  13 +
 lib/composer/composer/autoload_static.php     |  13 +
 .../Bootstrap/RegistrationContext.php         |  24 ++
 lib/private/Mail/Provider/Manager.php         | 255 +++++++++++++
 lib/private/Server.php                        |   3 +
 .../Bootstrap/IRegistrationContext.php        |  11 +
 lib/public/Mail/Provider/Address.php          |  85 +++++
 lib/public/Mail/Provider/Attachment.php       | 139 +++++++
 lib/public/Mail/Provider/IAddress.php         |  61 ++++
 lib/public/Mail/Provider/IAttachment.php      | 101 ++++++
 lib/public/Mail/Provider/IManager.php         | 106 ++++++
 lib/public/Mail/Provider/IMessage.php         | 232 ++++++++++++
 lib/public/Mail/Provider/IMessageSend.php     |  31 ++
 lib/public/Mail/Provider/IProvider.php        | 130 +++++++
 lib/public/Mail/Provider/IService.php         | 150 ++++++++
 lib/public/Mail/Provider/IServiceIdentity.php |  30 ++
 lib/public/Mail/Provider/IServiceLocation.php |  30 ++
 lib/public/Mail/Provider/Message.php          | 338 ++++++++++++++++++
 tests/lib/Mail/Provider/AddressTest.php       |  46 +++
 tests/lib/Mail/Provider/AttachmentTest.php    |  71 ++++
 tests/lib/Mail/Provider/ManagerTest.php       | 189 ++++++++++
 tests/lib/Mail/Provider/MessageTest.php       | 163 +++++++++
 25 files changed, 2288 insertions(+), 27 deletions(-)
 create mode 100644 lib/private/Mail/Provider/Manager.php
 create mode 100644 lib/public/Mail/Provider/Address.php
 create mode 100644 lib/public/Mail/Provider/Attachment.php
 create mode 100644 lib/public/Mail/Provider/IAddress.php
 create mode 100644 lib/public/Mail/Provider/IAttachment.php
 create mode 100644 lib/public/Mail/Provider/IManager.php
 create mode 100644 lib/public/Mail/Provider/IMessage.php
 create mode 100644 lib/public/Mail/Provider/IMessageSend.php
 create mode 100644 lib/public/Mail/Provider/IProvider.php
 create mode 100644 lib/public/Mail/Provider/IService.php
 create mode 100644 lib/public/Mail/Provider/IServiceIdentity.php
 create mode 100644 lib/public/Mail/Provider/IServiceLocation.php
 create mode 100644 lib/public/Mail/Provider/Message.php
 create mode 100644 tests/lib/Mail/Provider/AddressTest.php
 create mode 100644 tests/lib/Mail/Provider/AttachmentTest.php
 create mode 100644 tests/lib/Mail/Provider/ManagerTest.php
 create mode 100644 tests/lib/Mail/Provider/MessageTest.php

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