Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnna Larch <anna@nextcloud.com>2022-11-03 17:17:51 +0300
committerAnna Larch <anna@nextcloud.com>2022-11-10 23:46:33 +0300
commit7738e90c2ea18dff0a57e1ea47b195c8b544bfdb (patch)
treeca2eb238cd31bf216c75e7d88b91c6c82ebb170a
parentcecf046ba532b6bfd452b57f2b126941448ab0fe (diff)
Local Draft Handling Backend
Signed-off-by: Anna Larch <anna@nextcloud.com>
-rw-r--r--appinfo/info.xml2
-rw-r--r--lib/AppInfo/Application.php2
-rw-r--r--lib/BackgroundJob/DraftsJob.php57
-rw-r--r--lib/Contracts/IMailTransmission.php2
-rw-r--r--lib/Controller/DraftsController.php198
-rw-r--r--lib/Db/LocalMessage.php11
-rw-r--r--lib/Db/LocalMessageMapper.php58
-rw-r--r--lib/Events/DraftMessageCreatedEvent.php56
-rw-r--r--lib/Listener/DeleteDraftListener.php3
-rw-r--r--lib/Migration/Version2020Date20221103140538.php59
-rw-r--r--lib/Service/DraftsService.php240
-rw-r--r--lib/Service/MailTransmission.php14
-rw-r--r--tests/Unit/Service/DraftsServiceTest.php499
13 files changed, 1188 insertions, 13 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/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/Unit/Service/DraftsServiceTest.php b/tests/Unit/Service/DraftsServiceTest.php
new file mode 100644
index 000000000..f5262b157
--- /dev/null
+++ b/tests/Unit/Service/DraftsServiceTest.php
@@ -0,0 +1,499 @@
+<?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\Recipient;
+use OCA\Mail\Exception\ClientException;
+use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\Attachment\AttachmentService;
+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 OutboxServiceTest extends TestCase {
+ /** @var MailTransmission|MockObject */
+ private $transmission;
+
+ /** @var LocalMessageMapper|MockObject */
+ private $mapper;
+
+ /** @var OutboxService */
+ private $outboxService;
+
+ /** @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->accountService = $this->createMock(AccountService::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->outboxService = new OutboxService(
+ $this->transmission,
+ $this->mapper,
+ $this->attachmentService,
+ $this->createMock(EventDispatcher::class),
+ $this->clientFactory,
+ $this->mailManager,
+ $this->accountService,
+ $this->timeFactory,
+ $this->logger,
+ );
+ $this->userId = 'linus';
+ $this->time = $this->createMock(ITimeFactory::class);
+ }
+
+ public function testGetMessages(): void {
+ $this->mapper->expects(self::once())
+ ->method('getAllForUser')
+ ->with($this->userId)
+ ->willReturn([
+ [
+ 'id' => 1,
+ 'type' => 0,
+ 'account_id' => 1,
+ 'alias_id' => 2,
+ 'send_at' => $this->time->getTime(),
+ 'subject' => 'Test',
+ 'body' => 'Test',
+ 'html' => false,
+ 'reply_to_id' => null,
+ 'draft_id' => 99
+
+ ],
+ [
+ 'id' => 2,
+ 'type' => 0,
+ 'account_id' => 1,
+ 'alias_id' => 2,
+ 'send_at' => $this->time->getTime(),
+ 'subject' => 'Second Test',
+ 'body' => 'Second Test',
+ 'html' => true,
+ 'reply_to_id' => null,
+ 'draft_id' => null
+ ]
+ ]);
+
+ $this->outboxService->getMessages($this->userId);
+ }
+
+ public function testGetMessagesNoneFound(): void {
+ $this->mapper->expects(self::once())
+ ->method('getAllForUser')
+ ->with($this->userId)
+ ->willThrowException(new Exception());
+
+ $this->expectException(Exception::class);
+ $this->outboxService->getMessages($this->userId);
+ }
+
+ public function testGetMessage(): 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('abcd');
+
+ $this->mapper->expects(self::once())
+ ->method('findById')
+ ->with(1, $this->userId)
+ ->willReturn($message);
+
+ $this->outboxService->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->outboxService->getMessage(1, $this->userId);
+ }
+
+ public function testDeleteMessage(): 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');
+
+ $this->attachmentService->expects(self::once())
+ ->method('deleteLocalMessageAttachments')
+ ->with($this->userId, $message->getId());
+ $this->mapper->expects(self::once())
+ ->method('deleteWithRecipients')
+ ->with($message);
+
+ $this->outboxService->deleteMessage($this->userId, $message);
+ }
+
+ public function testSaveMessage(): 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('abcd');
+ $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->outboxService->saveMessage($account, $message, $to, $cc, $bcc, $attachments);
+ }
+
+ public function testSaveMessageNoAttachments(): 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('abcd');
+ $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->outboxService->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($this->time->getTime());
+ $message->setSubject('Test');
+ $message->setBody('Test Test Test');
+ $message->setHtml(true);
+ $message->setInReplyToMessageId('abcd');
+ $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->outboxService->updateMessage($account, $message, $to, $cc, $bcc, $attachments);
+ }
+
+ public function testUpdateMessageNoAttachments(): 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');
+ $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->outboxService->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');
+ $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->outboxService->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->outboxService->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->outboxService->sendMessage($message, $account);
+ }
+}