diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2022-11-11 11:17:41 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-11 11:17:41 +0300 |
commit | 6a3e7b0cbc812f54e32ac9de1c37e3d4f8148dea (patch) | |
tree | d690941cb2f8cc707e63018ac4180faf70b72c26 | |
parent | 990b3f0dd59afbca274e22663fa361bad34fc1ce (diff) | |
parent | 110b17afcd0ae9d205027c102968fb7ebd318a60 (diff) |
Merge pull request #7517 from nextcloud/enh/draft-handling-new
Rework local draft handling (Backend)
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 2 | ||||
-rw-r--r-- | lib/BackgroundJob/DraftsJob.php | 57 | ||||
-rw-r--r-- | lib/BackgroundJob/IMipMessageJob.php | 4 | ||||
-rw-r--r-- | lib/Contracts/IMailTransmission.php | 2 | ||||
-rw-r--r-- | lib/Controller/DraftsController.php | 198 | ||||
-rw-r--r-- | lib/Db/LocalMessage.php | 11 | ||||
-rw-r--r-- | lib/Db/LocalMessageMapper.php | 58 | ||||
-rw-r--r-- | lib/Events/DraftMessageCreatedEvent.php | 56 | ||||
-rw-r--r-- | lib/Listener/DeleteDraftListener.php | 3 | ||||
-rw-r--r-- | lib/Migration/Version2020Date20221103140538.php | 59 | ||||
-rw-r--r-- | lib/Service/DraftsService.php | 240 | ||||
-rw-r--r-- | lib/Service/MailTransmission.php | 14 | ||||
-rw-r--r-- | tests/Integration/Service/DraftServiceIntegrationTest.php | 272 | ||||
-rw-r--r-- | tests/Unit/Controller/DraftsControllerTest.php | 597 | ||||
-rw-r--r-- | tests/Unit/Service/DraftsServiceTest.php | 641 | ||||
-rw-r--r-- | tests/Unit/Service/MailTransmissionTest.php | 83 |
17 files changed, 2284 insertions, 15 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index 051395a95..93e91dc9c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ - **🙈 We’re not reinventing the wheel!** Based on the great [Horde](https://horde.org) libraries. - **📬 Want to host your own mail server?** We do not have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)! ]]></description> - <version>2.2.0-alpha.0</version> + <version>2.2.0-alpha.1</version> <licence>agpl</licence> <author>Greta Doçi</author> <author homepage="https://github.com/nextcloud/groupware">Nextcloud Groupware Team</author> diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 0cae71c6b..106e7b65b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -35,6 +35,7 @@ use OCA\Mail\Contracts\IUserPreferences; use OCA\Mail\Dashboard\ImportantMailWidget; use OCA\Mail\Dashboard\UnreadMailWidget; use OCA\Mail\Events\BeforeMessageSentEvent; +use OCA\Mail\Events\DraftMessageCreatedEvent; use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MailboxesSynchronizedEvent; use OCA\Mail\Events\OutboxMessageCreatedEvent; @@ -105,6 +106,7 @@ class Application extends App implements IBootstrap { $context->registerEventListener(BeforeMessageSentEvent::class, AntiAbuseListener::class); $context->registerEventListener(DraftSavedEvent::class, DeleteDraftListener::class); + $context->registerEventListener(DraftMessageCreatedEvent::class, DeleteDraftListener::class); $context->registerEventListener(OutboxMessageCreatedEvent::class, DeleteDraftListener::class); $context->registerEventListener(MailboxesSynchronizedEvent::class, MailboxesSynchronizedSpecialMailboxesUpdater::class); $context->registerEventListener(MessageFlaggedEvent::class, MessageCacheUpdaterListener::class); diff --git a/lib/BackgroundJob/DraftsJob.php b/lib/BackgroundJob/DraftsJob.php new file mode 100644 index 000000000..1bd5f0627 --- /dev/null +++ b/lib/BackgroundJob/DraftsJob.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * @author 2022 Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\BackgroundJob; + +use OCA\Mail\Db\LocalMessageMapper; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DraftsService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +class DraftsJob extends TimedJob { + private DraftsService $draftsService; + private LocalMessageMapper $messageMapper; + private AccountService $accountService; + private LoggerInterface $logger; + + public function __construct(ITimeFactory $time, + DraftsService $draftsService, + AccountService $accountService, + LocalMessageMapper $messageMapper, + LoggerInterface $logger) { + parent::__construct($time); + + // Run once per five minutes + $this->setInterval(5 * 60); + $this->setTimeSensitivity(self::TIME_SENSITIVE); + $this->draftsService = $draftsService; + } + + protected function run($argument): void { + $this->draftsService->flush(); + } +} diff --git a/lib/BackgroundJob/IMipMessageJob.php b/lib/BackgroundJob/IMipMessageJob.php index 371645c91..c2038ac92 100644 --- a/lib/BackgroundJob/IMipMessageJob.php +++ b/lib/BackgroundJob/IMipMessageJob.php @@ -33,12 +33,12 @@ class IMipMessageJob extends TimedJob { private IMipService $iMipService; public function __construct(ITimeFactory $time, - IMipService $iMipService) { + IMipService $draftsService) { parent::__construct($time); // Run once per hour $this->setInterval(60 * 60); - $this->iMipService = $iMipService; + $this->iMipService = $draftsService; } protected function run($argument): void { diff --git a/lib/Contracts/IMailTransmission.php b/lib/Contracts/IMailTransmission.php index f8e29bbd9..812c921fc 100644 --- a/lib/Contracts/IMailTransmission.php +++ b/lib/Contracts/IMailTransmission.php @@ -57,7 +57,7 @@ interface IMailTransmission { * @throws ServiceException * @return void */ - public function sendLocalMessage(Account $account, LocalMessage $message): void; + public function sendLocalMessage(Account $account, LocalMessage $message, bool $isDraft = false): void; /** * Save a message draft diff --git a/lib/Controller/DraftsController.php b/lib/Controller/DraftsController.php new file mode 100644 index 000000000..b008ea117 --- /dev/null +++ b/lib/Controller/DraftsController.php @@ -0,0 +1,198 @@ +<?php + +declare(strict_types=1); + +/** + * Mail App + * + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Mail\Controller; + +use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Http\JsonResponse; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DraftsService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IRequest; + +class DraftsController extends Controller { + private DraftsService $service; + private string $userId; + private AccountService $accountService; + private ITimeFactory $timeFactory; + + public function __construct(string $appName, + $UserId, + IRequest $request, + DraftsService $service, + AccountService $accountService, + ITimeFactory $timeFactory) { + parent::__construct($appName, $request); + $this->userId = $UserId; + $this->service = $service; + $this->accountService = $accountService; + $this->timeFactory = $timeFactory; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $accountId + * @param string $subject + * @param string $body + * @param string $editorBody + * @param bool $isHtml + * @param array $to i. e. [['label' => 'Linus', 'email' => 'tent@stardewvalley.com'], ['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']] + * @param array $cc + * @param array $bcc + * @param array $attachments + * @param int|null $aliasId + * @param string|null $inReplyToMessageId + * @param int|null $sendAt + * @param int|null $draftId + * @return JsonResponse + */ + public function create( + int $accountId, + string $subject, + string $body, + string $editorBody, + bool $isHtml, + array $to = [], + array $cc = [], + array $bcc = [], + array $attachments = [], + ?int $aliasId = null, + ?string $inReplyToMessageId = null, + ?int $sendAt = null, + ?int $draftId = null) : JsonResponse { + $account = $this->accountService->find($this->userId, $accountId); + if ($draftId !== null) { + $this->service->handleDraft($account, $draftId); + } + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($accountId); + $message->setAliasId($aliasId); + $message->setSubject($subject); + $message->setBody($body); + $message->setEditorBody($editorBody); + $message->setHtml($isHtml); + $message->setInReplyToMessageId($inReplyToMessageId); + $message->setUpdatedAt($this->timeFactory->getTime()); + $message->setSendAt($sendAt); + if ($sendAt !== null) { + $message->setType(LocalMessage::TYPE_OUTGOING); + } + $this->service->saveMessage($account, $message, $to, $cc, $bcc, $attachments); + + return JsonResponse::success($message, Http::STATUS_CREATED); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id + * @param int $accountId + * @param string $subject + * @param string $body + * @param string $editorBody + * @param bool $isHtml + * @param bool $failed + * @param array $to i. e. [['label' => 'Linus', 'email' => 'tent@stardewvalley.com'], ['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']] + * @param array $cc + * @param array $bcc + * @param array $attachments + * @param int|null $aliasId + * @param string|null $inReplyToMessageId + * @param int|null $sendAt + * @return JsonResponse + */ + public function update(int $id, + int $accountId, + string $subject, + string $body, + string $editorBody, + bool $isHtml, + bool $failed = false, + array $to = [], + array $cc = [], + array $bcc = [], + array $attachments = [], + ?int $aliasId = null, + ?string $inReplyToMessageId = null, + ?int $sendAt = null): JsonResponse { + $message = $this->service->getMessage($id, $this->userId); + $account = $this->accountService->find($this->userId, $accountId); + + ($sendAt !== null) + ? $message->setType(LocalMessage::TYPE_OUTGOING) + : $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($accountId); + $message->setAliasId($aliasId); + $message->setSubject($subject); + $message->setBody($body); + $message->setEditorBody($editorBody); + $message->setHtml($isHtml); + $message->setFailed($failed); + $message->setInReplyToMessageId($inReplyToMessageId); + $message->setSendAt($sendAt); + $message->setUpdatedAt($this->timeFactory->getTime()); + + $message = $this->service->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + return JsonResponse::success($message, Http::STATUS_ACCEPTED); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id + * @return JsonResponse + */ + public function destroy(int $id): JsonResponse { + $message = $this->service->getMessage($id, $this->userId); + $account = $this->accountService->find($this->userId, $message->getAccountId()); + + $this->service->deleteMessage($this->userId, $message); + return JsonResponse::success('Message deleted', Http::STATUS_ACCEPTED); + } + + /** + * @NoAdminRequired + * @TrapError + * @param int $id + * @return JsonResponse + */ + public function move(int $id): JsonResponse { + $message = $this->service->getMessage($id, $this->userId); + $account = $this->accountService->find($this->userId, $message->getAccountId()); + + $this->service->sendMessage($message, $account); + return JsonResponse::success( + 'Message moved to IMAP', Http::STATUS_ACCEPTED + ); + } +} diff --git a/lib/Db/LocalMessage.php b/lib/Db/LocalMessage.php index 944b403f9..4e1b72ad0 100644 --- a/lib/Db/LocalMessage.php +++ b/lib/Db/LocalMessage.php @@ -37,8 +37,8 @@ use function array_filter; * @method void setAccountId(int $accountId) * @method int|null getAliasId() * @method void setAliasId(?int $aliasId) - * @method int getSendAt() - * @method void setSendAt(int $sendAt) + * @method int|null getSendAt() + * @method void setSendAt(?int $sendAt) * @method string getSubject() * @method void setSubject(string $subject) * @method string getBody() @@ -51,6 +51,8 @@ use function array_filter; * @method void setFailed(bool $failed) * @method string|null getInReplyToMessageId() * @method void setInReplyToMessageId(?string $inReplyToId) + * @method int|null getUpdatedAt() + * @method setUpdatedAt(?int $updatedAt) */ class LocalMessage extends Entity implements JsonSerializable { public const TYPE_OUTGOING = 0; @@ -95,6 +97,9 @@ class LocalMessage extends Entity implements JsonSerializable { /** @var bool|null */ protected $failed; + /** @var int|null */ + protected $updatedAt; + public function __construct() { $this->addType('type', 'integer'); $this->addType('accountId', 'integer'); @@ -102,6 +107,7 @@ class LocalMessage extends Entity implements JsonSerializable { $this->addType('sendAt', 'integer'); $this->addType('html', 'boolean'); $this->addType('failed', 'boolean'); + $this->addType('updatedAt', 'integer'); } #[ReturnTypeWillChange] @@ -112,6 +118,7 @@ class LocalMessage extends Entity implements JsonSerializable { 'accountId' => $this->getAccountId(), 'aliasId' => $this->getAliasId(), 'sendAt' => $this->getSendAt(), + 'updatedAt' => $this->getUpdatedAt(), 'subject' => $this->getSubject(), 'body' => $this->getBody(), 'editorBody' => $this->getEditorBody(), diff --git a/lib/Db/LocalMessageMapper.php b/lib/Db/LocalMessageMapper.php index b15b917fe..7a2e2801d 100644 --- a/lib/Db/LocalMessageMapper.php +++ b/lib/Db/LocalMessageMapper.php @@ -57,14 +57,14 @@ class LocalMessageMapper extends QBMapper { * @return LocalMessage[] * @throws DBException */ - public function getAllForUser(string $userId): array { + public function getAllForUser(string $userId, int $type = LocalMessage::TYPE_OUTGOING): array { $qb = $this->db->getQueryBuilder(); $qb->select('m.*') ->from('mail_accounts', 'a') ->join('a', $this->getTableName(), 'm', $qb->expr()->eq('m.account_id', 'a.id')) ->where( $qb->expr()->eq('a.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR), - $qb->expr()->eq('m.type', $qb->createNamedParameter(LocalMessage::TYPE_OUTGOING, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT) + $qb->expr()->eq('m.type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT) ); $rows = $qb->execute(); @@ -124,12 +124,13 @@ class LocalMessageMapper extends QBMapper { * * @return LocalMessage[] */ - public function findDue(int $time): array { + public function findDue(int $time, int $type = LocalMessage::TYPE_OUTGOING): array { $qb = $this->db->getQueryBuilder(); $select = $qb->select('*') ->from($this->getTableName()) ->where( $qb->expr()->isNotNull('send_at'), + $qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), $qb->expr()->lte('send_at', $qb->createNamedParameter($time, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), $qb->expr()->orX( $qb->expr()->isNull('failed'), @@ -167,6 +168,57 @@ class LocalMessageMapper extends QBMapper { } /** + * Find all messages that should be sent + * + * @param int $time upper bound send time stamp + * + * @return LocalMessage[] + */ + public function findDueDrafts(int $time): array { + $qb = $this->db->getQueryBuilder(); + $select = $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->isNotNull('send_at'), + $qb->expr()->eq('type', $qb->createNamedParameter(LocalMessage::TYPE_DRAFT, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->lte('updated_at', $qb->createNamedParameter($time - 300, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->orX( + $qb->expr()->isNull('failed'), + $qb->expr()->eq('failed', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL), + ) + ) + ->orderBy('updated_at', 'asc') + ->orderBy('account_id', 'asc'); + $messages = $this->findEntities($select); + + if (empty($messages)) { + return []; + } + + $ids = array_map(function (LocalMessage $message) { + return $message->getId(); + }, $messages); + + $attachments = $this->attachmentMapper->findByLocalMessageIds($ids); + $recipients = $this->recipientMapper->findByLocalMessageIds($ids); + + $recipientMap = []; + foreach ($recipients as $r) { + $recipientMap[$r->getLocalMessageId()][] = $r; + } + $attachmentMap = []; + foreach ($attachments as $a) { + $attachmentMap[$a->getLocalMessageId()][] = $a; + } + + return array_map(static function ($localMessage) use ($attachmentMap, $recipientMap) { + $localMessage->setAttachments($attachmentMap[$localMessage->getId()] ?? []); + $localMessage->setRecipients($recipientMap[$localMessage->getId()] ?? []); + return $localMessage; + }, $messages); + } + + /** * @param Recipient[] $to * @param Recipient[] $cc * @param Recipient[] $bcc diff --git a/lib/Events/DraftMessageCreatedEvent.php b/lib/Events/DraftMessageCreatedEvent.php new file mode 100644 index 000000000..efd92290b --- /dev/null +++ b/lib/Events/DraftMessageCreatedEvent.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * @author 2022 Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Events; + +use OCA\Mail\Account; +use OCA\Mail\Db\Message; +use OCP\EventDispatcher\Event; + +/** + * @psalm-immutable + */ +class DraftMessageCreatedEvent extends Event { + /** @var Account */ + private $account; + + /** @var Message */ + private $draft; + + public function __construct(Account $account, + Message $draft) { + parent::__construct(); + $this->account = $account; + $this->draft = $draft; + } + + public function getAccount(): Account { + return $this->account; + } + + public function getDraft(): ?Message { + return $this->draft; + } +} diff --git a/lib/Listener/DeleteDraftListener.php b/lib/Listener/DeleteDraftListener.php index c615122d8..dc53778d1 100644 --- a/lib/Listener/DeleteDraftListener.php +++ b/lib/Listener/DeleteDraftListener.php @@ -31,6 +31,7 @@ use OCA\Mail\Account; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message; +use OCA\Mail\Events\DraftMessageCreatedEvent; use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\OutboxMessageCreatedEvent; @@ -71,7 +72,7 @@ class DeleteDraftListener implements IEventListener { } public function handle(Event $event): void { - if (($event instanceof DraftSavedEvent || $event instanceof OutboxMessageCreatedEvent) && $event->getDraft() !== null) { + if (($event instanceof DraftSavedEvent || $event instanceof OutboxMessageCreatedEvent || $event instanceof DraftMessageCreatedEvent) && $event->getDraft() !== null) { $this->deleteDraft($event->getAccount(), $event->getDraft()); } } diff --git a/lib/Migration/Version2020Date20221103140538.php b/lib/Migration/Version2020Date20221103140538.php new file mode 100644 index 000000000..e380ca88e --- /dev/null +++ b/lib/Migration/Version2020Date20221103140538.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version2020Date20221103140538 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @psalm-param Closure $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $localMessagesTable = $schema->getTable('mail_local_messages'); + if (!$localMessagesTable->hasColumn('updated_at')) { + $localMessagesTable->addColumn('updated_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + ]); + } + $localMessagesTable->changeColumn('send_at', [ + 'notnull' => false + ]); + return $schema; + } +} diff --git a/lib/Service/DraftsService.php b/lib/Service/DraftsService.php new file mode 100644 index 000000000..bee465378 --- /dev/null +++ b/lib/Service/DraftsService.php @@ -0,0 +1,240 @@ +<?php + +declare(strict_types=1); + +/** + * Mail App + * + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Mail\Service; + +use OCA\Mail\Account; +use OCA\Mail\Contracts\ILocalMailboxService; +use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Contracts\IMailTransmission; +use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\LocalMessageMapper; +use OCA\Mail\Db\Recipient; +use OCA\Mail\Events\DraftMessageCreatedEvent; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\NotImplemented; +use OCA\Mail\Exception\ServiceException; +use OCA\Mail\IMAP\IMAPClientFactory; +use OCA\Mail\Service\Attachment\AttachmentService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use Psr\Log\LoggerInterface; +use Throwable; + +class DraftsService implements ILocalMailboxService { + private IMailTransmission $transmission; + private LocalMessageMapper $mapper; + private AttachmentService $attachmentService; + private IEventDispatcher $eventDispatcher; + private IMAPClientFactory $clientFactory; + private IMailManager $mailManager; + private LoggerInterface $logger; + private AccountService $accountService; + private ITimeFactory $time; + + public function __construct(IMailTransmission $transmission, + LocalMessageMapper $mapper, + AttachmentService $attachmentService, + IEventDispatcher $eventDispatcher, + IMAPClientFactory $clientFactory, + IMailManager $mailManager, + LoggerInterface $logger, + AccountService $accountService, + ITimeFactory $time) { + $this->transmission = $transmission; + $this->mapper = $mapper; + $this->attachmentService = $attachmentService; + $this->eventDispatcher = $eventDispatcher; + $this->clientFactory = $clientFactory; + $this->mailManager = $mailManager; + $this->logger = $logger; + $this->accountService = $accountService; + $this->time = $time; + } + + /** + * @param array $recipients + * @param int $type + * @return Recipient[] + */ + private static function convertToRecipient(array $recipients, int $type): array { + return array_map(function ($recipient) use ($type) { + $r = new Recipient(); + $r->setType($type); + $r->setLabel($recipient['label'] ?? $recipient['email']); + $r->setEmail($recipient['email']); + return $r; + }, $recipients); + } + + + public function deleteMessage(string $userId, LocalMessage $message): void { + $this->attachmentService->deleteLocalMessageAttachments($userId, $message->getId()); + $this->mapper->deleteWithRecipients($message); + } + + public function saveMessage(Account $account, LocalMessage $message, array $to, array $cc, array $bcc, array $attachments = []): LocalMessage { + $toRecipients = self::convertToRecipient($to, Recipient::TYPE_TO); + $ccRecipients = self::convertToRecipient($cc, Recipient::TYPE_CC); + $bccRecipients = self::convertToRecipient($bcc, Recipient::TYPE_BCC); + + // if this message has a "sent at" datestamp and the type is set as Type Outbox + // check for a valid recipient + if ($message->getType() === LocalMessage::TYPE_OUTGOING && empty($toRecipients)) { + throw new ClientException('Cannot convert message to outbox message without at least one recipient'); + } + + $message = $this->mapper->saveWithRecipients($message, $toRecipients, $ccRecipients, $bccRecipients); + + if (empty($attachments)) { + $message->setAttachments($attachments); + return $message; + } + + $client = $this->clientFactory->getClient($account); + try { + $attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client); + } finally { + $client->logout(); + } + + $message->setAttachments($this->attachmentService->saveLocalMessageAttachments($account->getUserId(), $message->getId(), $attachmentIds)); + return $message; + } + + public function updateMessage(Account $account, LocalMessage $message, array $to, array $cc, array $bcc, array $attachments = []): LocalMessage { + $toRecipients = self::convertToRecipient($to, Recipient::TYPE_TO); + $ccRecipients = self::convertToRecipient($cc, Recipient::TYPE_CC); + $bccRecipients = self::convertToRecipient($bcc, Recipient::TYPE_BCC); + + // if this message has a "sent at" datestamp and the type is set as Type Outbox + // check for a valid recipient + if ($message->getType() === LocalMessage::TYPE_OUTGOING && empty($toRecipients)) { + throw new ClientException('Cannot convert message to outbox message without at least one recipient'); + } + + $message = $this->mapper->updateWithRecipients($message, $toRecipients, $ccRecipients, $bccRecipients); + + if (empty($attachments)) { + $message->setAttachments($this->attachmentService->updateLocalMessageAttachments($account->getUserId(), $message, [])); + return $message; + } + + $client = $this->clientFactory->getClient($account); + try { + $attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client); + } finally { + $client->logout(); + } + $message->setAttachments($this->attachmentService->updateLocalMessageAttachments($account->getUserId(), $message, $attachmentIds)); + return $message; + } + + public function handleDraft(Account $account, int $draftId): void { + $message = $this->mailManager->getMessage($account->getUserId(), $draftId); + $this->eventDispatcher->dispatchTyped(new DraftMessageCreatedEvent($account, $message)); + } + + /** + * "Send" the message + * + * @param LocalMessage $message + * @param Account $account + * @return void + */ + public function sendMessage(LocalMessage $message, Account $account): void { + try { + $this->transmission->sendLocalMessage($account, $message, true); + } catch (ClientException|ServiceException $e) { + $this->logger->error('Could not move draft to IMAP', ['exception' => $e]); + // Mark as failed so the message is not moved repeatedly in background + $message->setFailed(true); + $this->mapper->update($message); + throw $e; + } + $this->attachmentService->deleteLocalMessageAttachments($account->getUserId(), $message->getId()); + $this->mapper->deleteWithRecipients($message); + } + + /** + * @throws DoesNotExistException + */ + public function getMessage(int $id, string $userId): LocalMessage { + return $this->mapper->findById($id, $userId); + } + + /** + * @throws NotImplemented + */ + public function getMessages(string $userId): array { + throw new NotImplemented('Not implemented'); + } + + public function flush() { + $messages = $this->mapper->findDueDrafts($this->time->getTime()); + if (empty($messages)) { + return; + } + + $accountIds = array_unique(array_map(function ($message) { + return $message->getAccountId(); + }, $messages)); + + $accounts = array_combine($accountIds, array_map(function ($accountId) { + try { + return $this->accountService->findById($accountId); + } catch (DoesNotExistException $e) { + // The message belongs to a deleted account + return null; + } + }, $accountIds)); + + foreach ($messages as $message) { + try { + $account = $accounts[$message->getAccountId()]; + if ($account === null) { + // Ignore message of non-existent account + continue; + } + $this->sendMessage( + $message, + $account, + ); + $this->logger->debug('Draft {id} moved to IMAP', [ + 'id' => $message->getId(), + ]); + } catch (Throwable $e) { + // Failure of one message should not stop sending other messages + // Log and continue + $this->logger->warning('Could not move draft {id} to IMAP: ' . $e->getMessage(), [ + 'id' => $message->getId(), + 'exception' => $e, + ]); + } + } + } +} diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index 210bef244..4472809ad 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -218,7 +218,7 @@ class MailTransmission implements IMailTransmission { ); } - public function sendLocalMessage(Account $account, LocalMessage $message): void { + public function sendLocalMessage(Account $account, LocalMessage $message, bool $isDraft = false): void { $to = new AddressList( array_map( static function ($recipient) { @@ -271,10 +271,14 @@ class MailTransmission implements IMailTransmission { $alias = $this->aliasesService->find($message->getAliasId(), $account->getUserId()); } - try { - $this->sendMessage($messageData, $message->getInReplyToMessageId(), $alias ?? null); - } catch (SentMailboxNotSetException $e) { - throw new ClientException('Could not send message' . $e->getMessage(), (int)$e->getCode(), $e); + if ($isDraft) { + $this->saveDraft($messageData); + } else { + try { + $this->sendMessage($messageData, $message->getInReplyToMessageId(), $alias ?? null); + } catch (SentMailboxNotSetException $e) { + throw new ClientException('Could not send message' . $e->getMessage(), (int)$e->getCode(), $e); + } } } diff --git a/tests/Integration/Service/DraftServiceIntegrationTest.php b/tests/Integration/Service/DraftServiceIntegrationTest.php new file mode 100644 index 000000000..b31ec7cab --- /dev/null +++ b/tests/Integration/Service/DraftServiceIntegrationTest.php @@ -0,0 +1,272 @@ +<?php + +declare(strict_types=1); + +/** + * @author Anna Larch <anna.larch@gmx.net> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Integration\Service; + +use ChristophWurst\Nextcloud\Testing\TestUser; +use OC; +use OCA\Mail\Account; +use OCA\Mail\Contracts\IAttachmentService; +use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Contracts\IMailTransmission; +use OCA\Mail\Db\LocalAttachmentMapper; +use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\LocalMessageMapper; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\IMAP\IMAPClientFactory; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\Attachment\AttachmentService; +use OCA\Mail\Service\Attachment\AttachmentStorage; +use OCA\Mail\Service\DraftsService; +use OCA\Mail\Service\OutboxService; +use OCA\Mail\Tests\Integration\Framework\ImapTest; +use OCA\Mail\Tests\Integration\Framework\ImapTestAccount; +use OCA\Mail\Tests\Integration\TestCase; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Folder; +use OCP\IServerContainer; +use OCP\IUser; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +class DraftServiceIntegrationTest extends TestCase { + use ImapTest, + ImapTestAccount, + TestUser; + + /** @var MailAccount */ + private $account; + + /** @var IUser */ + private $user; + + /** @var IAttachmentService */ + private $attachmentService; + + /** @var IMailTransmission */ + private $transmission; + + /** @var OutboxService */ + private $service; + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IMAPClientFactory */ + private $clientFactory; + + /** @var LocalMessageMapper */ + private $mapper; + + /** @var Folder */ + private $userFolder; + + /** @var AccountService|\PHPUnit\Framework\MockObject\MockObject */ + private $accountService; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + protected function setUp(): void { + parent::setUp(); + + $this->resetImapAccount(); + $this->disconnectImapAccount(); + + $this->user = $this->createTestUser(); + $this->account = $this->createTestAccount($this->user->getUID()); + $c = OC::$server->get(ContainerInterface::class); + $userContainer = $c->get(IServerContainer::class); + $this->userFolder = $userContainer->getUserFolder($this->account->getUserId()); + $mailManager = OC::$server->get(IMailManager::class); + $this->attachmentService = new AttachmentService( + $this->userFolder, + OC::$server->get(LocalAttachmentMapper::class), + OC::$server->get(AttachmentStorage::class), + $mailManager, + OC::$server->get(\OCA\Mail\IMAP\MessageMapper::class), + new NullLogger() + ); + $this->client = $this->getClient($this->account); + $this->mapper = OC::$server->get(LocalMessageMapper::class); + $this->transmission = OC::$server->get(IMailTransmission::class); + $this->eventDispatcher = OC::$server->get(IEventDispatcher::class); + $this->clientFactory = OC::$server->get(IMAPClientFactory::class); + $this->accountService = $this->createMock(AccountService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->db = \OC::$server->getDatabaseConnection(); + $qb = $this->db->getQueryBuilder(); + $delete = $qb->delete($this->mapper->getTableName()); + $delete->execute(); + + $this->service = new DraftsService( + $this->transmission, + $this->mapper, + $this->attachmentService, + $this->eventDispatcher, + $this->clientFactory, + $mailManager, + $this->createMock(LoggerInterface::class), + $this->accountService, + $this->timeFactory + ); + } + + public function testSaveAndGetMessage(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->service->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($message->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + $retrieved = $this->service->getMessage($message->getId(), $this->user->getUID()); + $this->assertNotEmpty($message->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + self::assertCount(1, $retrieved->getRecipients()); + } + + public function testSaveAndDeleteMessage(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->service->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($message->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + $this->service->deleteMessage($this->user->getUID(), $saved); + + $this->expectException(DoesNotExistException::class); + $this->service->getMessage($message->getId(), $this->user->getUID()); + } + + public function testSaveAndUpdateMessage(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->service->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($message->getRecipients()); + $this->assertCount(1, $saved->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + $saved->setSubject('Your Trailer will be put up for sale'); + $cc = [[ + 'label' => 'Pam', + 'email' => 'buyMeABeer@stardewvalley.com' + ]]; + $updated = $this->service->updateMessage(new Account($this->account), $saved, $to, $cc, []); + + $this->assertNotEmpty($updated->getRecipients()); + $this->assertEquals('Your Trailer will be put up for sale', $updated->getSubject()); + $this->assertCount(2, $updated->getRecipients()); + } + public function testSaveAndConvertToOutboxMessage(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->service->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($message->getRecipients()); + $this->assertCount(1, $saved->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + $saved->setSubject('Your Trailer will be put up for sale'); + $cc = [[ + 'label' => 'Pam', + 'email' => 'buyMeABeer@stardewvalley.com' + ]]; + $saved->setType(LocalMessage::TYPE_OUTGOING); + $saved->setSendAt(123456); + $updated = $this->service->updateMessage(new Account($this->account), $saved, $to, $cc, []); + + $this->assertNotEmpty($updated->getRecipients()); + $this->assertEquals('Your Trailer will be put up for sale', $updated->getSubject()); + $this->assertCount(2, $updated->getRecipients()); + $this->assertEquals(LocalMessage::TYPE_OUTGOING, $saved->getType()); + } + + + public function testSaveAndSendMessage(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->service->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($message->getRecipients()); + $this->assertCount(1, $saved->getRecipients()); + $this->assertEmpty($message->getAttachments()); + + $this->service->sendMessage($saved, new Account($this->account)); + + $this->expectException(DoesNotExistException::class); + $this->service->getMessage($message->getId(), $this->user->getUID()); + } +} diff --git a/tests/Unit/Controller/DraftsControllerTest.php b/tests/Unit/Controller/DraftsControllerTest.php new file mode 100644 index 000000000..503431106 --- /dev/null +++ b/tests/Unit/Controller/DraftsControllerTest.php @@ -0,0 +1,597 @@ +<?php + +declare(strict_types=1); + +/** + * @author Anna Larch <anna.larch@gmx.net> + * + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Unit\Controller; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OC\AppFramework\Http; +use OCA\Mail\Account; +use OCA\Mail\Controller\DraftsController; +use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\ServiceException; +use OCA\Mail\Http\JsonResponse; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DraftsService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\IRequest; + +class DraftsControllerTest extends TestCase { + protected function setUp(): void { + parent::setUp(); + + $this->appName = 'mail'; + $this->service = $this->createMock(DraftsService::class); + $this->userId = 'john'; + $this->request = $this->createMock(IRequest::class); + $this->accountService = $this->createMock(AccountService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->controller = new DraftsController( + $this->appName, + $this->userId, + $this->request, + $this->service, + $this->accountService, + $this->timeFactory + ); + } + + public function testMove(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $account = new Account(new MailAccount()); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('sendMessage') + ->with($message, $account); + + $expected = JsonResponse::success('Message moved to IMAP', Http::STATUS_ACCEPTED); + $actual = $this->controller->move($message->getId()); + + $this->assertEquals($expected, $actual); + } + + public function testMoveNoMessage(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willThrowException(new DoesNotExistException('')); + $this->accountService->expects(self::never()) + ->method('find'); + $this->service->expects(self::never()) + ->method('sendMessage'); + + $this->expectException(DoesNotExistException::class); + $expected = JsonResponse::fail('', Http::STATUS_NOT_FOUND); + $actual = $this->controller->move($message->getId()); + + $this->assertEquals($expected, $actual); + } + + public function testSendClientException(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willThrowException(new ClientException()); + $this->service->expects(self::never()) + ->method('sendMessage'); + + $this->expectException(ClientException::class); + $this->controller->move($message->getId()); + } + + public function testSendServiceException(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $account = new Account(new MailAccount()); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('sendMessage') + ->willThrowException(new ServiceException()); + + $this->expectException(ServiceException::class); + $this->controller->move($message->getId()); + } + + public function testDestroy(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $account = new Account(new MailAccount()); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $this->accountService->expects(self::once()) + ->method('find') + ->willReturn($account); + $this->service->expects(self::once()) + ->method('deleteMessage') + ->with($this->userId, $message); + + $expected = JsonResponse::success('Message deleted', Http::STATUS_ACCEPTED); + $actual = $this->controller->destroy($message->getId()); + + $this->assertEquals($expected, $actual); + } + + public function testDestroyNoMessage(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willThrowException(new DoesNotExistException('')); + $this->accountService->expects(self::never()) + ->method('find'); + $this->service->expects(self::never()) + ->method('deleteMessage'); + + $this->expectException(DoesNotExistException::class); + $expected = JsonResponse::fail('', Http::STATUS_NOT_FOUND); + $actual = $this->controller->destroy($message->getId()); + + $this->assertEquals($expected, $actual); + } + + public function testCreate(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setSendAt(null); + $message->setUpdatedAt(123456); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn(123456); + $this->service->expects(self::once()) + ->method('saveMessage') + ->with($account, $message, $to, $cc, [], []); + + $expected = JsonResponse::success($message, Http::STATUS_CREATED); + $actual = $this->controller->create( + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + + $this->assertEquals($expected, $actual); + } + + public function testCreateFromDraft(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setSendAt(null); + $message->setUpdatedAt(123456); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('handleDraft'); + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn(123456); + $this->service->expects(self::once()) + ->method('saveMessage') + ->with($account, $message, $to, $cc, [], []); + + $expected = JsonResponse::success($message, Http::STATUS_CREATED); + $actual = $this->controller->create( + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId(), + null, + 1 + ); + + $this->assertEquals($expected, $actual); + } + + public function testCreateWithEmptyRecipients(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setSendAt(null); + $message->setUpdatedAt(123456); + + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn(123456); + $this->service->expects(self::once()) + ->method('saveMessage') + ->with($account, $message, [], [], [], []); + + $expected = JsonResponse::success($message, Http::STATUS_CREATED); + $actual = $this->controller->create( + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + [], + [], + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + + $this->assertEquals($expected, $actual); + } + + public function testCreateAccountNotFound(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $message->setSendAt(null); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willThrowException(new ClientException()); + $this->service->expects(self::never()) + ->method('saveMessage'); + + $this->expectException(ClientException::class); + $actual = $this->controller->create( + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + } + + public function testCreateDbException(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()); + $this->service->expects(self::once()) + ->method('saveMessage') + ->willThrowException(new Exception()); + + $this->expectException(Exception::class); + $this->controller->create( + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + } + + public function testUpdate(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setFailed(false); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('updateMessage') + ->with($account, $message, $to, $cc, [], []) + ->willReturn($message); + + $expected = JsonResponse::success($message, Http::STATUS_ACCEPTED); + $actual = $this->controller->update( + $message->getId(), + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + false, + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + + $this->assertEquals($expected, $actual); + } + + public function testUpdateMoveToOutbox(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $message->setFailed(false); + $message->setSendAt(123456); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('updateMessage') + ->with($account, $message, $to, $cc, [], []) + ->willReturn($message); + + $expected = JsonResponse::success($message, Http::STATUS_ACCEPTED); + $actual = $this->controller->update( + $message->getId(), + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + false, + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId(), + $message->getSendAt() + ); + + $this->assertEquals($expected, $actual); + } + + + public function testUpdateMessageNotFound(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setFailed(false); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willThrowException(new DoesNotExistException('')); + $this->service->expects(self::never()) + ->method('updateMessage'); + + + $this->expectException(DoesNotExistException::class); + $expected = JsonResponse::fail('', Http::STATUS_NOT_FOUND); + $actual = $this->controller->update( + $message->getId(), + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + false, + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + + $this->assertEquals($expected, $actual); + } + + public function testUpdateDbException(): void { + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $message->setAliasId(2); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setEditorBody('<p>message</p>'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setFailed(false); + $to = [['label' => 'Lewis', 'email' => 'tent@stardewvalley.com']]; + $cc = [['label' => 'Pierre', 'email' => 'generalstore@stardewvalley.com']]; + + $this->service->expects(self::once()) + ->method('getMessage') + ->with($message->getId(), $this->userId) + ->willReturn($message); + $account = new Account(new MailAccount()); + $this->accountService->expects(self::once()) + ->method('find') + ->with($this->userId, $message->getAccountId()) + ->willReturn($account); + $this->service->expects(self::once()) + ->method('updateMessage') + ->with($account, $message, $to, $cc, [], []) + ->willThrowException(new Exception()); + + $this->expectException(Exception::class); + $this->controller->update( + $message->getId(), + $message->getAccountId(), + $message->getSubject(), + $message->getBody(), + '<p>message</p>', + $message->isHtml(), + false, + $to, + $cc, + [], + [], + $message->getAliasId(), + $message->getInReplyToMessageId() + ); + } +} diff --git a/tests/Unit/Service/DraftsServiceTest.php b/tests/Unit/Service/DraftsServiceTest.php new file mode 100644 index 000000000..af144f950 --- /dev/null +++ b/tests/Unit/Service/DraftsServiceTest.php @@ -0,0 +1,641 @@ +<?php + +declare(strict_types=1); + +/** + * Mail App + * + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Mail\Tests\Unit\Service; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OC\EventDispatcher\EventDispatcher; +use OCA\Mail\Account; +use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Db\LocalAttachment; +use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\LocalMessageMapper; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\Db\Message; +use OCA\Mail\Db\Recipient; +use OCA\Mail\Events\DraftMessageCreatedEvent; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\NotImplemented; +use OCA\Mail\IMAP\IMAPClientFactory; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\Attachment\AttachmentService; +use OCA\Mail\Service\DraftsService; +use OCA\Mail\Service\MailTransmission; +use OCA\Mail\Service\OutboxService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class DraftsServiceTest extends TestCase { + /** @var MailTransmission|MockObject */ + private $transmission; + + /** @var LocalMessageMapper|MockObject */ + private $mapper; + + /** @var OutboxService */ + private $draftsService; + + /** @var string */ + private $userId; + + /** @var ITimeFactory|MockObject */ + private $time; + + /** @var AttachmentService|MockObject */ + private $attachmentService; + + /** @var IMAPClientFactory|MockObject */ + private $clientFactory; + + /** @var IMailManager|MockObject */ + private $mailManager; + + /** @var AccountService|MockObject */ + private $accountService; + + /** @var ITimeFactory|MockObject */ + private $timeFactory; + + /** @var MockObject|LoggerInterface */ + private $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->transmission = $this->createMock(MailTransmission::class); + $this->mapper = $this->createMock(LocalMessageMapper::class); + $this->attachmentService = $this->createMock(AttachmentService::class); + $this->clientFactory = $this->createMock(IMAPClientFactory::class); + $this->mailManager = $this->createMock(IMailManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcher::class); + $this->accountService = $this->createMock(AccountService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->draftsService = new DraftsService( + $this->transmission, + $this->mapper, + $this->attachmentService, + $this->eventDispatcher, + $this->clientFactory, + $this->mailManager, + $this->logger, + $this->accountService, + $this->timeFactory + ); + $this->userId = 'linus'; + $this->time = $this->createMock(ITimeFactory::class); + } + + public function testGetMessages(): void { + $this->expectException(NotImplemented::class); + $this->draftsService->getMessages($this->userId); + } + + public function testGetMessage(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setUpdatedAt(123456); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + + $this->mapper->expects(self::once()) + ->method('findById') + ->with(1, $this->userId) + ->willReturn($message); + + $this->draftsService->getMessage(1, $this->userId); + } + + public function testNoMessage(): void { + $this->mapper->expects(self::once()) + ->method('findById') + ->with(1, $this->userId) + ->willThrowException(new DoesNotExistException('Could not fetch any messages')); + + $this->expectException(DoesNotExistException::class); + $this->draftsService->getMessage(1, $this->userId); + } + + public function testDeleteMessage(): void { + $message = new LocalMessage(); + $message->setId(10); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + + $this->attachmentService->expects(self::once()) + ->method('deleteLocalMessageAttachments') + ->with($this->userId, $message->getId()); + $this->mapper->expects(self::once()) + ->method('deleteWithRecipients') + ->with($message); + + $this->draftsService->deleteMessage($this->userId, $message); + } + + public function testSaveMessage(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_DRAFT); + $to = [ + [ + 'label' => 'Lewis', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $cc = []; + $bcc = []; + $attachments = [[]]; + $attachmentIds = [1]; + $rTo = Recipient::fromParams([ + 'label' => 'Lewis', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message2 = $message; + $message2->setId(10); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + $client = $this->createMock(\Horde_Imap_Client_Socket::class); + + $this->mapper->expects(self::once()) + ->method('saveWithRecipients') + ->with($message, [$rTo], $cc, $bcc) + ->willReturn($message2); + $this->clientFactory->expects(self::once()) + ->method('getClient') + ->with($account) + ->willReturn($client); + $this->attachmentService->expects(self::once()) + ->method('handleAttachments') + ->with($account, $attachments, $client) + ->willReturn($attachmentIds); + $this->attachmentService->expects(self::once()) + ->method('saveLocalMessageAttachments') + ->with($this->userId, 10, $attachmentIds); + + $this->draftsService->saveMessage($account, $message, $to, $cc, $bcc, $attachments); + } + + public function testSaveMessageNoAttachments(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_DRAFT); + $to = [ + [ + 'label' => 'Lewis', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $cc = []; + $bcc = []; + $attachments = []; + $rTo = Recipient::fromParams([ + 'label' => 'Lewis', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message2 = $message; + $message2->setId(10); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + + $this->mapper->expects(self::once()) + ->method('saveWithRecipients') + ->with($message, [$rTo], $cc, $bcc) + ->willReturn($message2); + $this->clientFactory->expects(self::never()) + ->method('getClient'); + $this->attachmentService->expects(self::never()) + ->method('handleAttachments'); + $this->attachmentService->expects(self::never()) + ->method('saveLocalMessageAttachments'); + + $result = $this->draftsService->saveMessage($account, $message, $to, $cc, $bcc, $attachments); + $this->assertEquals($message2->getId(), $result->getId()); + $this->assertEmpty($result->getAttachments()); + } + + public function testUpdateMessage(): void { + $message = new LocalMessage(); + $message->setId(10); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_DRAFT); + $old = Recipient::fromParams([ + 'label' => 'Pam', + 'email' => 'BuyMeAnAle@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message->setRecipients([$old]); + $to = [ + [ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $cc = []; + $bcc = []; + $attachments = [['type' => '']]; + $attachmentIds = [3]; + $rTo = Recipient::fromParams([ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message2 = $message; + $message2->setRecipients([$rTo]); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + $client = $this->createMock(\Horde_Imap_Client_Socket::class); + + $this->mapper->expects(self::once()) + ->method('updateWithRecipients') + ->with($message, [$rTo], $cc, $bcc) + ->willReturn($message2); + $this->clientFactory->expects(self::once()) + ->method('getClient') + ->with($account) + ->willReturn($client); + $this->attachmentService->expects(self::once()) + ->method('handleAttachments') + ->with($account, $attachments, $client) + ->willReturn($attachmentIds); + $this->attachmentService->expects(self::once()) + ->method('updateLocalMessageAttachments') + ->with($this->userId, $message2, $attachmentIds); + + $this->draftsService->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + } + + public function testConvertToOutboxMessage(): void { + $message = new LocalMessage(); + $message->setId(10); + $message->setAccountId(1); + $message->setSendAt($this->time->getTime()); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $old = Recipient::fromParams([ + 'label' => 'Pam', + 'email' => 'BuyMeAnAle@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message->setRecipients([$old]); + $to = [ + [ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $cc = []; + $bcc = []; + $attachments = [['type' => '']]; + $attachmentIds = [3]; + $rTo = Recipient::fromParams([ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message2 = $message; + $message2->setRecipients([$rTo]); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + $client = $this->createMock(\Horde_Imap_Client_Socket::class); + + $this->mapper->expects(self::once()) + ->method('updateWithRecipients') + ->with($message, [$rTo], $cc, $bcc) + ->willReturn($message2); + $this->clientFactory->expects(self::once()) + ->method('getClient') + ->with($account) + ->willReturn($client); + $this->attachmentService->expects(self::once()) + ->method('handleAttachments') + ->with($account, $attachments, $client) + ->willReturn($attachmentIds); + $this->attachmentService->expects(self::once()) + ->method('updateLocalMessageAttachments') + ->with($this->userId, $message2, $attachmentIds); + + $this->draftsService->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + } + + public function testConvertToOutboxMessageNoRecipients(): void { + $message = new LocalMessage(); + $message->setId(10); + $message->setAccountId(1); + $message->setSendAt($this->time->getTime()); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $old = Recipient::fromParams([ + 'label' => 'Pam', + 'email' => 'BuyMeAnAle@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message->setRecipients([$old]); + $to = []; + $cc = []; + $bcc = []; + $attachments = [['type' => '']]; + $attachmentIds = [3]; + $message2 = $message; + $message2->setRecipients([]); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + $client = $this->createMock(\Horde_Imap_Client_Socket::class); + + $this->mapper->expects(self::never()) + ->method('updateWithRecipients') + ->with($message, [], $cc, $bcc) + ->willReturn($message2); + $this->clientFactory->expects(self::never()) + ->method('getClient') + ->with($account) + ->willReturn($client); + $this->attachmentService->expects(self::never()) + ->method('handleAttachments') + ->with($account, $attachments, $client) + ->willReturn($attachmentIds); + $this->attachmentService->expects(self::never()) + ->method('updateLocalMessageAttachments') + ->with($this->userId, $message2, $attachmentIds); + + $this->expectException(ClientException::class); + $this->draftsService->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + } + + public function testUpdateMessageNoAttachments(): void { + $message = new LocalMessage(); + $message->setId(10); + $message->setAccountId(1); + $message->setSendAt(null); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('abcd'); + $message->setType(LocalMessage::TYPE_DRAFT); + $old = Recipient::fromParams([ + 'label' => 'Pam', + 'email' => 'BuyMeAnAle@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message->setRecipients([$old]); + $to = [ + [ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $cc = []; + $bcc = []; + $attachments = []; + $rTo = Recipient::fromParams([ + 'label' => 'Linus', + 'email' => 'tent-living@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $message2 = $message; + $message2->setRecipients([$rTo]); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + $this->mapper->expects(self::once()) + ->method('updateWithRecipients') + ->with($message, [$rTo], $cc, $bcc) + ->willReturn($message2); + $this->attachmentService->expects(self::once()) + ->method('updateLocalMessageAttachments') + ->with($this->userId, $message2, $attachments); + $this->clientFactory->expects(self::never()) + ->method('getClient'); + $this->attachmentService->expects(self::never()) + ->method('handleAttachments'); + $result = $this->draftsService->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + $this->assertEmpty($result->getAttachments()); + } + + public function testSaveMessageError(): void { + $message = new LocalMessage(); + $message->setAccountId(1); + $message->setSendAt($this->time->getTime()); + $message->setSubject('Test'); + $message->setBody('Test Test Test'); + $message->setHtml(true); + $message->setInReplyToMessageId('laskdjhsakjh33233928@startdewvalley.com'); + $message->setType(LocalMessage::TYPE_OUTGOING); + $to = [ + [ + 'label' => 'Gunther', + 'email' => 'museum@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ] + ]; + $rTo = Recipient::fromParams([ + 'label' => 'Gunther', + 'email' => 'museum@startdewvalley.com', + 'type' => Recipient::TYPE_TO, + ]); + $account = $this->createMock(Account::class); + + $this->mapper->expects(self::once()) + ->method('saveWithRecipients') + ->with($message, [$rTo], [], []) + ->willThrowException(new Exception()); + $this->attachmentService->expects(self::never()) + ->method('saveLocalMessageAttachments'); + $this->expectException(Exception::class); + + $this->draftsService->saveMessage($account, $message, $to, [], []); + } + + public function testSendMessage(): void { + $message = new LocalMessage(); + $message->setId(1); + $recipient = new Recipient(); + $recipient->setEmail('museum@startdewvalley.com'); + $recipient->setLabel('Gunther'); + $recipient->setType(Recipient::TYPE_TO); + $recipients = [$recipient]; + $attachment = new LocalAttachment(); + $attachment->setMimeType('image/png'); + $attachment->setFileName('SlimesInTheMines.png'); + $attachment->setCreatedAt($this->time->getTime()); + $attachments = [$attachment]; + $message->setRecipients($recipients); + $message->setAttachments($attachments); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + + $this->transmission->expects(self::once()) + ->method('sendLocalMessage') + ->with($account, $message); + $this->attachmentService->expects(self::once()) + ->method('deleteLocalMessageAttachments') + ->with($account->getUserId(), $message->getId()); + $this->mapper->expects(self::once()) + ->method('deleteWithRecipients') + ->with($message); + + $this->draftsService->sendMessage($message, $account); + } + + public function testSendMessageTransmissionError(): void { + $message = new LocalMessage(); + $message->setId(1); + $recipient = new Recipient(); + $recipient->setEmail('museum@startdewvalley.com'); + $recipient->setLabel('Gunther'); + $recipient->setType(Recipient::TYPE_TO); + $recipients = [$recipient]; + $attachment = new LocalAttachment(); + $attachment->setMimeType('image/png'); + $attachment->setFileName('SlimesInTheMines.png'); + $attachment->setCreatedAt($this->time->getTime()); + $attachments = [$attachment]; + $message->setRecipients($recipients); + $message->setAttachments($attachments); + $account = $this->createConfiguredMock(Account::class, [ + 'getUserId' => $this->userId + ]); + + $this->transmission->expects(self::once()) + ->method('sendLocalMessage') + ->with($account, $message) + ->willThrowException(new ClientException()); + $this->attachmentService->expects(self::never()) + ->method('deleteLocalMessageAttachments'); + $this->mapper->expects(self::never()) + ->method('deleteWithRecipients'); + + $this->expectException(ClientException::class); + $this->draftsService->sendMessage($message, $account); + } + + public function testHandleDraft(): void { + $mailAccount = new MailAccount(); + $mailAccount->setUserId('admin'); + $account = new Account($mailAccount); + $draftId = 1; + $message = new Message(); + + $this->mailManager->expects(self::once()) + ->method('getMessage') + ->with($account->getUserId(), $draftId) + ->willReturn($message); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(new DraftMessageCreatedEvent($account, $message)); + + $this->draftsService->handleDraft($account, $draftId); + } + + public function testFlush(): void { + $time = 123456; + $message = new LocalMessage(); + $message->setId(1); + $message->setAccountId(1); + $messages = [$message]; + $mailAccount = new MailAccount(); + $mailAccount->setUserId('linus'); + $account = new Account($mailAccount); + + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn($time); + $this->mapper->expects(self::once()) + ->method('findDueDrafts') + ->with($time) + ->willReturn($messages); + $this->accountService->expects(self::once()) + ->method('findById') + ->willReturn($account); + $this->logger->expects(self::once()) + ->method('debug'); + + $this->draftsService->flush(); + } + + public function testFlushNoMessages(): void { + $time = 123456; + + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn($time); + $this->mapper->expects(self::once()) + ->method('findDueDrafts') + ->with($time) + ->willReturn([]); + $this->accountService->expects(self::never()) + ->method('findById'); + $this->logger->expects(self::never()) + ->method('debug'); + $this->logger->expects(self::never()) + ->method('warning'); + + $this->draftsService->flush(); + } +} diff --git a/tests/Unit/Service/MailTransmissionTest.php b/tests/Unit/Service/MailTransmissionTest.php index cd7295e50..f3a1aaa1d 100644 --- a/tests/Unit/Service/MailTransmissionTest.php +++ b/tests/Unit/Service/MailTransmissionTest.php @@ -38,6 +38,7 @@ use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message as DbMessage; use OCA\Mail\Db\Recipient; use OCA\Mail\Events\MessageSentEvent; +use OCA\Mail\Exception\ClientException; use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\IMAP\MessageMapper; use OCA\Mail\Model\Message; @@ -518,4 +519,86 @@ class MailTransmissionTest extends TestCase { $this->assertStringContainsString('Content-Type: image/png', $rawMessage); $this->assertStringContainsString('Content-Disposition: inline', $rawMessage); } + + public function testSendLocalDraft(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(10); + $mailAccount->setUserId('gunther'); + $mailAccount->setName('Gunther'); + $mailAccount->setEmail('gunther@stardewvalley-museum.com'); + $mailAccount->setDraftsMailboxId(123); + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($mailAccount->getId()); + $message->setAliasId(2); + $message->setSendAt(123); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setAttachments([]); + $to = Recipient::fromParams([ + 'email' => 'emily@stardewvalleypub.com', + 'label' => 'Emily', + 'type' => Recipient::TYPE_TO + ]); + $message->setRecipients([$to]); + + $alias = Alias::fromParams([ + 'id' => 1, + 'accountId' => 10, + 'name' => 'Emily', + 'alias' => 'Emmerlie' + ]); + $this->aliasService->expects(self::once()) + ->method('find') + ->with($message->getAliasId(), $mailAccount->getUserId()) + ->willReturn($alias); + + $replyMessage = new DbMessage(); + $replyMessage->setMessageId('abc'); + + $this->transmission->sendLocalMessage(new Account($mailAccount), $message, true); + } + + public function testSendLocalDraftNoDraftsMailbox(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(10); + $mailAccount->setUserId('gunther'); + $mailAccount->setName('Gunther'); + $mailAccount->setEmail('gunther@stardewvalley-museum.com'); + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_DRAFT); + $message->setAccountId($mailAccount->getId()); + $message->setAliasId(2); + $message->setSendAt(123); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + $message->setInReplyToMessageId('abc'); + $message->setAttachments([]); + $to = Recipient::fromParams([ + 'email' => 'emily@stardewvalleypub.com', + 'label' => 'Emily', + 'type' => Recipient::TYPE_TO + ]); + $message->setRecipients([$to]); + + $alias = Alias::fromParams([ + 'id' => 1, + 'accountId' => 10, + 'name' => 'Emily', + 'alias' => 'Emmerlie' + ]); + $this->aliasService->expects(self::once()) + ->method('find') + ->with($message->getAliasId(), $mailAccount->getUserId()) + ->willReturn($alias); + + $replyMessage = new DbMessage(); + $replyMessage->setMessageId('abc'); + + $this->expectException(ClientException::class); + $this->transmission->sendLocalMessage(new Account($mailAccount), $message, true); + } } |