Skip to content

Commit

Permalink
Merge pull request #9651 from nextcloud/feature/request-803
Browse files Browse the repository at this point in the history
feature: mail provider backend
  • Loading branch information
ChristophWurst authored Sep 11, 2024
2 parents bee022e + ce653d0 commit 18f93fd
Show file tree
Hide file tree
Showing 9 changed files with 1,183 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\UserDeletedListener;
use OCA\Mail\Notification\Notifier;
use OCA\Mail\Provider\MailProvider;
use OCA\Mail\Search\FilteringProvider;
use OCA\Mail\Search\Provider;
use OCA\Mail\Service\Attachment\AttachmentService;
Expand Down Expand Up @@ -156,6 +157,9 @@ public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(Provider::class);
}

// Added in version 4.0.0
$context->registerMailProvider(MailProvider::class);

$context->registerNotifierService(Notifier::class);

// bypass Horde Translation system
Expand Down
21 changes: 21 additions & 0 deletions lib/Db/MailAccountMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ public function findByUserId(string $userId): array {
return $this->findEntities($query);
}

/**
* Finds a mail account(s) by user id and mail address
*
* @since 4.0.0
*
* @param string $userId system user id
* @param string $address mail address (e.g. [email protected])
*
* @return MailAccount[]
*/
public function findByUserIdAndAddress(string $userId, string $address): array {
$qb = $this->db->getQueryBuilder();
$query = $qb
->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
->andWhere($qb->expr()->eq('email', $qb->createNamedParameter($address)));

return $this->findEntities($query);
}

/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
Expand Down
151 changes: 151 additions & 0 deletions lib/Provider/Command/MessageSend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Provider\Command;

use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\UploadException;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\OutboxService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Mail\Provider\Exception\SendException;
use OCP\Mail\Provider\IAddress;
use OCP\Mail\Provider\IMessage;

class MessageSend {

public function __construct(
protected ITimeFactory $time,
protected AccountService $accountService,
protected OutboxService $outboxService,
protected AttachmentService $attachmentService
) {
}

/**
* Performs send operation
*
* @since 4.0.0
*
* @param string $userId system user id
* @param string $serviceId mail account id
* @param IMessage $message mail message object with all required parameters to send a message
* @param array $options array of options reserved for future use
*
* @return LocalMessage
*
* @throws SendException on failure, check message for reason
*
*/
public function perform(string $userId, string $serviceId, IMessage $message, array $options = []): LocalMessage {
// validate that at least one To address is present
if (count($message->getTo()) === 0) {
throw new SendException('Invalid Message Parameter: MUST contain at least one TO address with a valid address');
}
// validate that all To, CC and BCC have email address
$entries = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
array_walk($entries, function ($entry) {
if (empty($entry->getAddress())) {
throw new SendException('Invalid Message Parameter: All TO, CC and BCC addresses MUST contain at least an email address');
}
});
// validate that all attachments have a name, type, and contents
$entries = $message->getAttachments();
array_walk($entries, function ($entry) {
if (empty($entry->getName()) || empty($entry->getType()) || empty($entry->getContents())) {
throw new SendException('Invalid Attachment Parameter: MUST contain values for Name, Type and Contents');
}
});
// retrieve user mail account details
try {
$account = $this->accountService->find($userId, (int)$serviceId);
} catch (ClientException $e) {
throw new SendException('Error: occurred while retrieving mail account details', 0, $e);
}
// convert mail provider message to mail app message
$localMessage = new LocalMessage();
$localMessage->setType($localMessage::TYPE_OUTGOING);
$localMessage->setAccountId($account->getId());
$localMessage->setSubject((string)$message->getSubject());
$localMessage->setBody((string)$message->getBody());
// disabled due to issues caused by opening these messages in gui
//$localMessage->setEditorBody($message->getBody());
$localMessage->setHtml(true);
$localMessage->setSendAt($this->time->getTime());
// convert mail provider addresses to recipient addresses
$to = $this->convertAddressArray($message->getTo());
$cc = $this->convertAddressArray($message->getCc());
$bcc = $this->convertAddressArray($message->getBcc());
// save attachments
$attachments = [];
try {
foreach ($message->getAttachments() as $entry) {
$attachments[] = $this->attachmentService->addFileFromString(
$userId,
(string)$entry->getName(),
(string)$entry->getType(),
(string)$entry->getContents()
);
}
} catch (UploadException $e) {
$this->purgeSavedAttachments($attachments);
throw new SendException('Error: occurred while saving mail message attachment', 0, $e);
}
// save message
$localMessage = $this->outboxService->saveMessage(
$account,
$localMessage,
$to,
$cc,
$bcc,
array_map(static fn (LocalAttachment $attachment) => $attachment->jsonSerialize(), $attachments)
);
// send message
try {
$localMessage = $this->outboxService->sendMessage($localMessage, $account);
} catch (\Throwable $e) {
throw new SendException('Error: occurred while sending mail message', 0, $e);
}

return $localMessage;
}

/**
* Converts IAddress objects collection to plain array
*
* @since 4.0.0
*
* @param array<int,IAddress> $addresses collection of IAddress objects
*
* @return array<int, array{email: string, label?: string}> collection of addresses and labels
*/
protected function convertAddressArray(array $addresses): array {
return array_map(static function (IAddress $address) {
return !empty($address->getLabel())
? ['email' => (string)$address->getAddress(), 'label' => (string)$address->getLabel()]
: ['email' => (string)$address->getAddress()];
}, $addresses);
}

/**
* Removes attachments from data store
*
* @since 4.0.0
*
* @param array<int, LocalAttachment> $attachments collection of local attachment objects
*/
protected function purgeSavedAttachments(array $attachments): void {
foreach ($attachments as $attachment) {
$this->attachmentService->deleteAttachment($attachment->getUserId(), $attachment->getId());
}
}

}
167 changes: 167 additions & 0 deletions lib/Provider/MailProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Provider;

use OCA\Mail\Account;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Service\AccountService;
use OCP\IL10N;
use OCP\Mail\Provider\Address as MailAddress;
use OCP\Mail\Provider\IProvider;
use OCP\Mail\Provider\IService;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

class MailProvider implements IProvider {

public function __construct(
protected ContainerInterface $container,
protected AccountService $accountService,
protected LoggerInterface $logger,
protected IL10N $l10n
) {
}

/**
* Arbitrary unique text string identifying this provider
*
* @since 4.0.0
*
* @return string id of this provider (e.g. UUID or 'IMAP/SMTP' or anything else)
*/
public function id(): string {
return 'mail-application';
}

/**
* Localized human friendly name of this provider
*
* @since 4.0.0
*
* @return string label/name of this provider (e.g. Plain Old IMAP/SMTP)
*/
public function label(): string {
return $this->l10n->t('Mail Application');
}

/**
* Determine if any services are configured for a specific user
*
* @since 4.0.0
*
* @param string $userId system user id
*
* @return bool true if any services are configure for the user
*/
public function hasServices(string $userId): bool {
return (count($this->listServices($userId)) > 0);
}

/**
* Retrieve collection of services for a specific user
*
* @since 4.0.0
*
* @param string $userId system user id
*
* @return array<string,IService> collection of service id and object ['1' => IServiceObject]
*/
public function listServices(string $userId): array {
// retrieve service(s) details from data store
$accounts = $this->accountService->findByUserId($userId);
// construct temporary collection
$services = [];
// add services to collection
foreach ($accounts as $entry) {
$services[(string)$entry->getId()] = $this->serviceFromAccount($userId, $entry);
}
// return list of services for user
return $services;
}

/**
* Retrieve a service with a specific id
*
* @since 4.0.0
*
* @param string $userId system user id
* @param string $serviceId mail account id
*
* @return IService|null returns service object or null if none found
*
*/
public function findServiceById(string $userId, string $serviceId): IService|null {
// determine if a valid user and service id was submitted
if (empty($userId) && !ctype_digit($serviceId)) {
return null;
}
// retrieve service details from data store
try {
$account = $this->accountService->find($userId, (int)$serviceId);
} catch(ClientException $e) {
$this->logger->error('Error occurred while retrieving mail account details', [ 'exception' => $e ]);
return null;
}
// return mail service object
return $this->serviceFromAccount($userId, $account);
}

/**
* Retrieve a service for a specific mail address
*
* @since 4.0.0
*
* @param string $userId system user id
* @param string $address mail address (e.g. [email protected])
*
* @return IService|null returns service object or null if none found
*/
public function findServiceByAddress(string $userId, string $address): IService|null {
// retrieve service details from data store
$accounts = $this->accountService->findByUserIdAndAddress($userId, $address);
// evaluate if service details where found
if (count($accounts) > 0) {
// return mail service object
return $this->serviceFromAccount($userId, $accounts[0]);
}

return null;
}

/**
* Construct a new fresh service object
*
* @since 4.0.0
*
* @return IService fresh service object
*/
public function initiateService(): IService {
return new MailService($this->container);
}

/**
* Construct a service object from a mail account
*
* @since 4.0.0
*
* @param string $userId system user id
* @param Account $account mail account
*
* @return IService service object
*/
protected function serviceFromAccount(string $userId, Account $account): IService {
// extract values
$serviceId = (string)$account->getId();
$serviceLabel = $account->getName();
$serviceAddress = new MailAddress($account->getEmail(), $account->getName());
// return mail service object
return new MailService($this->container, $userId, $serviceId, $serviceLabel, $serviceAddress);
}

}
Loading

0 comments on commit 18f93fd

Please sign in to comment.