diff options
-rw-r--r-- | appinfo/info.xml | 3 | ||||
-rw-r--r-- | lib/BackgroundJob/OutboxWorkerJob.php | 57 | ||||
-rw-r--r-- | lib/Db/LocalAttachmentMapper.php | 3 | ||||
-rw-r--r-- | lib/Db/LocalMessageMapper.php | 33 | ||||
-rw-r--r-- | lib/Db/RecipientMapper.php | 4 | ||||
-rw-r--r-- | lib/Service/OutboxService.php | 54 | ||||
-rw-r--r-- | tests/Integration/Service/OutboxServiceIntegrationTest.php | 39 | ||||
-rw-r--r-- | tests/Unit/Service/OutboxServiceTest.php | 19 |
8 files changed, 207 insertions, 5 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index c26a173d6..fce914275 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 don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)! ]]></description> - <version>1.12.0-alpha.3</version> + <version>1.12.0-alpha.4</version> <licence>agpl</licence> <author>Greta Doçi</author> <author homepage="https://github.com/nextcloud/groupware">Nextcloud Groupware Team</author> @@ -34,6 +34,7 @@ </dependencies> <background-jobs> <job>OCA\Mail\BackgroundJob\CleanupJob</job> + <job>OCA\Mail\BackgroundJob\OutboxWorkerJob</job> </background-jobs> <repair-steps> <post-migration> diff --git a/lib/BackgroundJob/OutboxWorkerJob.php b/lib/BackgroundJob/OutboxWorkerJob.php new file mode 100644 index 000000000..305adf8f7 --- /dev/null +++ b/lib/BackgroundJob/OutboxWorkerJob.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @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\Service\OutboxService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use function defined; +use function method_exists; + +class OutboxWorkerJob extends TimedJob { + + /** @var OutboxService */ + private $outboxService; + + public function __construct(ITimeFactory $time, + OutboxService $outboxService) { + parent::__construct($time); + + // Run once per five minutes + $this->setInterval(5 * 60); + /** + * @todo remove checks with 24+ + */ + if (defined('\OCP\BackgroundJob\IJob::TIME_SENSITIVE') && method_exists($this, 'setTimeSensitivity')) { + $this->setTimeSensitivity(self::TIME_SENSITIVE); + } + $this->outboxService = $outboxService; + } + + protected function run($argument): void { + $this->outboxService->flush(); + } +} diff --git a/lib/Db/LocalAttachmentMapper.php b/lib/Db/LocalAttachmentMapper.php index 3bb38a2f4..50bb25158 100644 --- a/lib/Db/LocalAttachmentMapper.php +++ b/lib/Db/LocalAttachmentMapper.php @@ -60,6 +60,9 @@ class LocalAttachmentMapper extends QBMapper { * @return LocalAttachment[] */ public function findByLocalMessageIds(array $localMessageIds): array { + if (empty($localMessageIds)) { + return []; + } $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) diff --git a/lib/Db/LocalMessageMapper.php b/lib/Db/LocalMessageMapper.php index 3b36381b6..71029e672 100644 --- a/lib/Db/LocalMessageMapper.php +++ b/lib/Db/LocalMessageMapper.php @@ -28,6 +28,7 @@ namespace OCA\Mail\Db; use OCP\AppFramework\Db\DoesNotExistException; use OCP\DB\Exception as DBException; use Throwable; +use function array_filter; use function array_map; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -117,6 +118,38 @@ class LocalMessageMapper extends QBMapper { } /** + * Find all messages that should be sent + * + * @param int $time upper bound send time stamp + * + * @return LocalMessage[] + */ + public function findDue(int $time): array { + $qb = $this->db->getQueryBuilder(); + $select = $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->isNotNull('send_at'), + $qb->expr()->lte('send_at', $qb->createNamedParameter($time, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT) + ); + $messages = $this->findEntities($select); + $ids = array_map(function (LocalMessage $message) { + return $message->getId(); + }, $messages); + $attachments = $this->attachmentMapper->findByLocalMessageIds($ids); + $recipients = $this->recipientMapper->findByLocalMessageIds($ids); + return array_map(static function ($message) use ($attachments, $recipients) { + $message->setAttachments(array_filter($attachments, function (LocalAttachment $attachment) use ($message) { + return $attachment->getLocalMessageId() === $message->getId(); + })); + $message->setRecipients(array_filter($recipients, function (Recipient $recipient) use ($message) { + return $recipient->getLocalMessageId() === $message->getId(); + })); + return $message; + }, $messages); + } + + /** * @param Recipient[] $to * @param Recipient[] $cc * @param Recipient[] $bcc diff --git a/lib/Db/RecipientMapper.php b/lib/Db/RecipientMapper.php index 8de6728ff..4f5b8bcc0 100644 --- a/lib/Db/RecipientMapper.php +++ b/lib/Db/RecipientMapper.php @@ -57,8 +57,10 @@ class RecipientMapper extends QBMapper { * @return Recipient[] */ public function findByLocalMessageIds(array $localMessageIds): array { + if (empty($localMessageIds)) { + return []; + } $qb = $this->db->getQueryBuilder(); - $query = $qb->select('*') ->from($this->getTableName()) ->where( diff --git a/lib/Service/OutboxService.php b/lib/Service/OutboxService.php index 90e778c8c..6c7d0470f 100644 --- a/lib/Service/OutboxService.php +++ b/lib/Service/OutboxService.php @@ -38,6 +38,9 @@ use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\Attachment\AttachmentService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\IEventDispatcher; +use OCP\AppFramework\Utility\ITimeFactory; +use Psr\Log\LoggerInterface; +use Throwable; class OutboxService implements ILocalMailboxService { @@ -59,18 +62,33 @@ class OutboxService implements ILocalMailboxService { /** @var IMailManager */ private $mailManager; + /** @var AccountService */ + private $accountService; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var LoggerInterface */ + private $logger; + public function __construct(IMailTransmission $transmission, LocalMessageMapper $mapper, AttachmentService $attachmentService, IEventDispatcher $eventDispatcher, IMAPClientFactory $clientFactory, - IMailManager $mailManager) { + IMailManager $mailManager, + AccountService $accountService, + ITimeFactory $timeFactory, + LoggerInterface $logger) { $this->transmission = $transmission; $this->mapper = $mapper; $this->attachmentService = $attachmentService; $this->eventDispatcher = $eventDispatcher; $this->clientFactory = $clientFactory; $this->mailManager = $mailManager; + $this->timeFactory = $timeFactory; + $this->logger = $logger; + $this->accountService = $accountService; } /** @@ -153,4 +171,38 @@ class OutboxService implements ILocalMailboxService { new OutboxMessageCreatedEvent($account, $message) ); } + + public function flush(): void { + $messages = $this->mapper->findDue( + $this->timeFactory->getTime() + ); + + $accountIds = array_unique(array_map(function ($message) { + return $message->getAccountId(); + }, $messages)); + + $accounts = array_combine($accountIds, array_map(function ($accountId) { + return $this->accountService->findById($accountId); + }, $accountIds)); + + foreach ($messages as $message) { + try { + // TODO: memoize accounts + $this->sendMessage( + $message, + $accounts[$message->getAccountId()], + ); + $this->logger->debug('Outbox message {id} sent', [ + 'id' => $message->getId(), + ]); + } catch (Throwable $e) { + // Failure of one message should not stop sending other messages + // Log and continue + $this->logger->warning('Could not send outbox message {id}: ' . $e->getMessage(), [ + 'id' => $message->getId(), + 'exception' => $e, + ]); + } + } + } } diff --git a/tests/Integration/Service/OutboxServiceIntegrationTest.php b/tests/Integration/Service/OutboxServiceIntegrationTest.php index 501433d16..cbfa17c13 100644 --- a/tests/Integration/Service/OutboxServiceIntegrationTest.php +++ b/tests/Integration/Service/OutboxServiceIntegrationTest.php @@ -37,6 +37,7 @@ use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\MessageMapper; 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\OutboxService; @@ -45,11 +46,13 @@ 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 OutboxServiceIntegrationTest extends TestCase { @@ -84,6 +87,12 @@ class OutboxServiceIntegrationTest extends TestCase { /** @var Folder */ private $userFolder; + /** @var */ + private $accountService; + + /** @var ITimeFactory */ + private $timeFactory; + protected function setUp(): void { parent::setUp(); @@ -109,6 +118,8 @@ class OutboxServiceIntegrationTest extends TestCase { $this->transmission = OC::$server->get(IMailTransmission::class); $this->eventDispatcher = OC::$server->get(IEventDispatcher::class); $this->clientFactory = OC::$server->get(IMAPClientFactory::class); + $this->accountService = OC::$server->get(AccountService::class); + $this->timeFactory = OC::$server->get(ITimeFactory::class); $this->db = \OC::$server->getDatabaseConnection(); $qb = $this->db->getQueryBuilder(); @@ -121,7 +132,10 @@ class OutboxServiceIntegrationTest extends TestCase { $this->attachmentService, $this->eventDispatcher, $this->clientFactory, - $mailManager + $mailManager, + $this->accountService, + $this->timeFactory, + $this->createMock(LoggerInterface::class) ); } @@ -348,4 +362,27 @@ class OutboxServiceIntegrationTest extends TestCase { $this->expectException(DoesNotExistException::class); $this->outbox->getMessage($message->getId(), $this->user->getUID()); } + + public function testSaveAndFlush(): void { + $message = new LocalMessage(); + $message->setType(LocalMessage::TYPE_OUTGOING); + $message->setAccountId($this->account->getId()); + $message->setSubject('subject'); + $message->setBody('message'); + $message->setHtml(true); + $message->setSendAt(100); + + $to = [[ + 'label' => 'Penny', + 'email' => 'library@stardewvalley.com' + ]]; + + $saved = $this->outbox->saveMessage(new Account($this->account), $message, $to, [], []); + $this->assertNotEmpty($saved->getRecipients()); + $this->assertEmpty($saved->getAttachments()); + + $this->outbox->flush(); + $this->expectException(DoesNotExistException::class); + $this->outbox->getMessage($saved->getId(), $this->user->getUID()); + } } diff --git a/tests/Unit/Service/OutboxServiceTest.php b/tests/Unit/Service/OutboxServiceTest.php index c743f635b..78b857bc5 100644 --- a/tests/Unit/Service/OutboxServiceTest.php +++ b/tests/Unit/Service/OutboxServiceTest.php @@ -36,6 +36,7 @@ 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; @@ -43,6 +44,7 @@ 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 { @@ -71,6 +73,15 @@ class OutboxServiceTest extends TestCase { /** @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(); @@ -79,13 +90,19 @@ class OutboxServiceTest extends TestCase { $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->mailManager, + $this->accountService, + $this->timeFactory, + $this->logger, ); $this->userId = 'linus'; $this->time = $this->createMock(ITimeFactory::class); |