Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: mail provider backend #9651

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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. test@example.com)
*
* @return MailAccount[]
*/
public function findByUserIdAndAddress(string $userId, string $address): array {
SebastianKrupinski marked this conversation as resolved.
Show resolved Hide resolved
$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
SebastianKrupinski marked this conversation as resolved.
Show resolved Hide resolved
*
* @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(
SebastianKrupinski marked this conversation as resolved.
Show resolved Hide resolved
$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);
kesselb marked this conversation as resolved.
Show resolved Hide resolved
} 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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserId and getId only work when you pass an LocalAttachment object. You are calling jsonSerialize in Line 95, and therefore we have a plain array here.

I prefer working with real objects rather than plain arrays and therefore recommend removing jsonSerialize in Line 95, add the phpdoc back for purgeSavedAttachmends, and use array_map(static fn(LocalAttachment $attachment) => $attachment->jsonSerialize(), $attachments) in Line 109.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I fixed the oversight with the LocalAttachment. But i left he phpdoc as paslm is still not happy and I don't want miss another deadline because of paslm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@param array<int, LocalAttachment> $attachments collection of local attachment objects

}
}

}
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. test@example.com)
*
* @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
Loading