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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 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,10 @@ public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(Provider::class);
}

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

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

// bypass Horde Translation system
Expand Down
23 changes: 23 additions & 0 deletions lib/Db/MailAccountMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ 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[]
*
* @throws DoesNotExistException
*/
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
120 changes: 120 additions & 0 deletions lib/Provider/Command/MessageSend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?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\LocalMessage;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\OutboxService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Mail\Provider\Exception\SendException;
use OCP\Mail\Provider\IAddress;
use OCP\Mail\Provider\IMessage;

class MessageSend {

public function __construct(
protected IConfig $config,
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
*/
public function perform(string $userId, string $serviceId, IMessage $message, array $option = []): LocalMessage {
// find user mail account details
$account = $this->accountService->find($userId, (int) $serviceId);
// convert mail provider message to mail app message
$localMessage = new LocalMessage();
$localMessage->setType($localMessage::TYPE_OUTGOING);
$localMessage->setAccountId($account->getId());
$localMessage->setSubject($message->getSubject());
$localMessage->setBody($message->getBody());
Copy link
Contributor

Choose a reason for hiding this comment

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

The local message send fails with:

{"reqId":"go23DSZ0ui2eI6J3jaPr","level":3,"time":"2024-08-20T15:04:22+00:00","remoteAddr":"127.0.0.1","user":"admin","app":"mail","method":"PUT","url":"/index.php/apps/mail/api/outbox/602","message":"OCA\\Mail\\Controller\\OutboxController::update(): Argument #5 ($editorBody) must be of type string, null given, called in /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php on line 208 in file '/var/www/nextcloud/apps/mail/lib/Controller/OutboxController.php' line 176","userAgent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0","version":"31.0.0.1","exception":{"Exception":"Exception","Message":"OCA\\Mail\\Controller\\OutboxController::update(): Argument #5 ($editorBody) must be of type string, null given, called in /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php on line 208 in file '/var/www/nextcloud/apps/mail/lib/Controller/OutboxController.php' line 176","Code":0,"Trace":[{"file":"/var/www/nextcloud/lib/private/AppFramework/App.php","line":161,"function":"dispatch","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"/var/www/nextcloud/lib/private/Route/Router.php","line":309,"function":"main","class":"OC\\AppFramework\\App","type":"::"},{"file":"/var/www/nextcloud/lib/base.php","line":1003,"function":"match","class":"OC\\Route\\Router","type":"->"},{"file":"/var/www/nextcloud/index.php","line":24,"function":"handleRequest","class":"OC","type":"::"}],"File":"/var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php","Line":146,"Previous":{"Exception":"TypeError","Message":"OCA\\Mail\\Controller\\OutboxController::update(): Argument #5 ($editorBody) must be of type string, null given, called in /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php on line 208","Code":0,"Trace":[{"file":"/var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php","line":208,"function":"update","class":"OCA\\Mail\\Controller\\OutboxController","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php","line":114,"function":"executeController","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"/var/www/nextcloud/lib/private/AppFramework/App.php","line":161,"function":"dispatch","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"/var/www/nextcloud/lib/private/Route/Router.php","line":309,"function":"main","class":"OC\\AppFramework\\App","type":"::"},{"file":"/var/www/nextcloud/lib/base.php","line":1003,"function":"match","class":"OC\\Route\\Router","type":"->"},{"file":"/var/www/nextcloud/index.php","line":24,"function":"handleRequest","class":"OC","type":"::"}],"File":"/var/www/nextcloud/apps/mail/lib/Controller/OutboxController.php","Line":176},"message":"OCA\\Mail\\Controller\\OutboxController::update(): Argument #5 ($editorBody) must be of type string, null given, called in /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php on line 208 in file '/var/www/nextcloud/apps/mail/lib/Controller/OutboxController.php' line 176","exception":{},"CustomMessage":"OCA\\Mail\\Controller\\OutboxController::update(): Argument #5 ($editorBody) must be of type string, null given, called in /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php on line 208 in file '/var/www/nextcloud/apps/mail/lib/Controller/OutboxController.php' line 176"}}

you probably need to set the editorBody as well here.

// disabled due to issues caused by opening these messages in gui
//$localMessage->setEditorBody($message->getBody());
$localMessage->setHtml(true);
$localMessage->setSendAt($this->time->getTime());

// convert all mail provider attachments to local attachments
$attachments = [];
if (count($message->getAttachments()) > 0) {
// iterate attachments and save them
foreach ($message->getAttachments() as $entry) {
// determine if required parameters are set
if (empty($entry->getName()) || empty($entry->getType()) || empty($entry->getContents())) {
throw new SendException("Invalid Attachment Parameter: MUST contain values for Name, Type and Contents");
}
// convert mail provider attachment to mail app attachment
$attachments[] = $this->attachmentService->addFileFromString(
$userId,
$entry->getName(),
$entry->getType(),
$entry->getContents()
)->jsonSerialize();
}
}
// determine if required To address is set
if (empty($message->getTo()) || empty($message->getTo()[0]->getAddress())) {
throw new SendException("Invalid Message Parameter: MUST contain at least one TO address with a valid address");
}
// convert recipient addresses
$to = $this->convertAddressArray($message->getTo());
$cc = $this->convertAddressArray($message->getCc());
$bcc = $this->convertAddressArray($message->getBcc());
// save message for sending
$localMessage = $this->outboxService->saveMessage(
$account,
$localMessage,
$to,
$cc,
$bcc,
$attachments
);

// evaluate if job scheduler is NOT cron, send message right away otherwise let cron job handle it
if ($this->config->getAppValue('core', 'backgroundjobs_mode', 'ajax') !== 'cron') {
$localMessage = $this->outboxService->sendMessage($localMessage, $account);
}

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}>
*/
protected function convertAddressArray(array $addresses): array {
return array_map(static function (IAddress $address) {
return !empty($address->getLabel())
? ['email' => $address->getAddress(), 'label' => $address->getLabel()]
: ['email' => $address->getAddress()];
}, $addresses);
}

}
175 changes: 175 additions & 0 deletions lib/Provider/MailProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?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\Service\AccountService;
use OCP\Mail\Provider\Address as MailAddress;
use OCP\Mail\Provider\IProvider;
use OCP\Mail\Provider\IService;
use Psr\Container\ContainerInterface;

class MailProvider implements IProvider {

private ?array $ServiceCollection = [];

public function __construct(
protected ContainerInterface $container,
protected AccountService $accountService
) {
}

/**
* arbitrary unique text string identifying this provider
SebastianKrupinski marked this conversation as resolved.
Show resolved Hide resolved
*
* @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 '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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* retrieve collection of services for a specific user
* 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 {

try {
// retrieve service(s) details from data store
$accounts = $this->accountService->findByUserId($userId);
} catch (\Throwable $th) {
return [];
}
// construct temporary collection
$services = [];
// add services to collection
foreach ($accounts as $entry) {
// extract values
$serviceId = (string) $entry->getId();
$label = $entry->getName();
$address = new MailAddress($entry->getEmail(), $entry->getName());
// add service to collection
$services[$serviceId] = new MailService($this->container, $userId, $serviceId, $label, $address);
}
// return list of services for user
return $services;

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

}

/**
* retrieve a service with a specific id
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* retrieve a service with a specific id

Method name is pretty self explanatory here

*
* @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;
}

try {
// retrieve service details from data store
$account = $this->accountService->find($userId, (int) $serviceId);
} catch(\Throwable $th) {
return null;
}

// extract values
$serviceId = (string) $account->getId();
$label = $account->getName();
$address = new MailAddress($account->getEmail(), $account->getName());
// return mail service object
return new MailService($this->container, $userId, $serviceId, $label, $address);

}

/**
* retrieve a service for a specific mail address
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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 returns service object or null if none found
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @return IService returns service object or null if none found
* @return IService

Self documenting code really

*/
public function findServiceByAddress(string $userId, string $address): IService | null {

try {
// retrieve service details from data store
$accounts = $this->accountService->findByUserIdAndAddress($userId, $address);
} catch(\Throwable $th) {
return null;
}
// evaluate if service details where found
if (isset($accounts[0])) {
// extract values
$serviceId = (string) $accounts[0]->getId();
$label = $accounts[0]->getName();
$address = new MailAddress($accounts[0]->getEmail(), $accounts[0]->getName());
// return mail service object
return new MailService($this->container, $userId, $serviceId, $label, $address);
}

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);

}

}
Loading
Loading