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:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2021-12-03 15:00:18 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2022-03-21 19:11:54 +0300
commit9096248744e858975b30a7279323a793390afd6e (patch)
tree4ceffca46e6b0fa59c607cd6f4c5fd0244ac65ef
parente181724033cd525fc9840594e545f1a2e3aa0f4b (diff)
Add anti abuse detection
* Trigger alert for number of recipients of a single message * Trigger alert for number of messages per time period (15m, 1h, 1d) Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
-rw-r--r--doc/admin.md24
-rw-r--r--lib/AppInfo/Application.php3
-rw-r--r--lib/Events/BeforeMessageSentEvent.php97
-rw-r--r--lib/Listener/AntiAbuseListener.php72
-rw-r--r--lib/Service/AntiAbuseService.php151
-rw-r--r--lib/Service/MailTransmission.php5
-rw-r--r--tests/Unit/Service/AntiAbuseServiceTest.php199
-rw-r--r--tests/psalm-baseline.xml17
8 files changed, 566 insertions, 2 deletions
diff --git a/doc/admin.md b/doc/admin.md
index 41a5622d8..90716eb41 100644
--- a/doc/admin.md
+++ b/doc/admin.md
@@ -44,6 +44,30 @@ Turn off TLS verfication for IMAP/SMTP. This happens globally for all accounts a
'app.mail.verify-tls-peer' => false
```
+### Anti-abuse alerts
+
+The app can write alerts to the logs when users send messages to a high number of recipients or sends a high number of messages for a short period of time. These events might indicate that the account is abused for sending spam messages.
+
+To enable anti-abuse alerts, you'll have to set a few configuration options [via occ](https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/occ_command.html).
+
+```bash
+# Turn alerts on
+occ config:app:set mail abuse_detection --value=on
+# Turn alerts off
+occ config:app:set mail abuse_detection --value=off
+
+# Alert when 50 or more recipients are used for one single message
+occ config:app:set mail abuse_number_of_recipients_per_message_threshold --value=50
+
+# Alerts can be configured for three intervals: 15m, 1h and 1d
+# Alert when more than 10 messages are sent in 15 minutes
+occ config:app:set mail abuse_number_of_messages_per_15m --value=10
+# Alert when more than 30 messages are sent in one hour
+occ config:app:set mail abuse_number_of_messages_per_1h --value=30
+# Alert when more than 100 messages are sent in one day
+occ config:app:set mail abuse_number_of_messages_per_1d --value=100
+```
+
## Troubleshooting
### Logging
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 3228834e5..8a1adbab7 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -34,6 +34,7 @@ use OCA\Mail\Contracts\ITrustedSenderService;
use OCA\Mail\Contracts\IUserPreferences;
use OCA\Mail\Dashboard\ImportantMailWidget;
use OCA\Mail\Dashboard\UnreadMailWidget;
+use OCA\Mail\Events\BeforeMessageSentEvent;
use OCA\Mail\Events\DraftSavedEvent;
use OCA\Mail\Events\MailboxesSynchronizedEvent;
use OCA\Mail\Events\SynchronizationEvent;
@@ -45,6 +46,7 @@ use OCA\Mail\HordeTranslationHandler;
use OCA\Mail\Http\Middleware\ErrorMiddleware;
use OCA\Mail\Http\Middleware\ProvisioningMiddleware;
use OCA\Mail\Listener\AddressCollectionListener;
+use OCA\Mail\Listener\AntiAbuseListener;
use OCA\Mail\Listener\HamReportListener;
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\DeleteDraftListener;
@@ -100,6 +102,7 @@ class Application extends App implements IBootstrap {
$context->registerServiceAlias(ITrustedSenderService::class, TrustedSenderService::class);
$context->registerServiceAlias(IUserPreferences::class, UserPreferenceService::class);
+ $context->registerEventListener(BeforeMessageSentEvent::class, AntiAbuseListener::class);
$context->registerEventListener(DraftSavedEvent::class, DeleteDraftListener::class);
$context->registerEventListener(MailboxesSynchronizedEvent::class, MailboxesSynchronizedSpecialMailboxesUpdater::class);
$context->registerEventListener(MessageFlaggedEvent::class, MessageCacheUpdaterListener::class);
diff --git a/lib/Events/BeforeMessageSentEvent.php b/lib/Events/BeforeMessageSentEvent.php
new file mode 100644
index 000000000..b7164e14f
--- /dev/null
+++ b/lib/Events/BeforeMessageSentEvent.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 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\Events;
+
+use Horde_Mime_Mail;
+use OCA\Mail\Account;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Model\IMessage;
+use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Model\RepliedMessageData;
+use OCP\EventDispatcher\Event;
+
+/**
+ * @psalm-immutable
+ */
+class BeforeMessageSentEvent extends Event {
+
+ /** @var Account */
+ private $account;
+
+ /** @var NewMessageData */
+ private $newMessageData;
+
+ /** @var null|RepliedMessageData */
+ private $repliedMessageData;
+
+ /** @var Message|null */
+ private $draft;
+
+ /** @var IMessage */
+ private $message;
+
+ /** @var Horde_Mime_Mail */
+ private $mail;
+
+ public function __construct(Account $account,
+ NewMessageData $newMessageData,
+ ?RepliedMessageData $repliedMessageData,
+ ?Message $draft,
+ IMessage $message,
+ Horde_Mime_Mail $mail) {
+ parent::__construct();
+ $this->account = $account;
+ $this->newMessageData = $newMessageData;
+ $this->repliedMessageData = $repliedMessageData;
+ $this->draft = $draft;
+ $this->message = $message;
+ $this->mail = $mail;
+ }
+
+ public function getAccount(): Account {
+ return $this->account;
+ }
+
+ public function getNewMessageData(): NewMessageData {
+ return $this->newMessageData;
+ }
+
+ public function getRepliedMessageData(): ?RepliedMessageData {
+ return $this->repliedMessageData;
+ }
+
+ public function getDraft(): ?Message {
+ return $this->draft;
+ }
+
+ public function getMessage(): IMessage {
+ return $this->message;
+ }
+
+ public function getMail(): Horde_Mime_Mail {
+ return $this->mail;
+ }
+}
diff --git a/lib/Listener/AntiAbuseListener.php b/lib/Listener/AntiAbuseListener.php
new file mode 100644
index 000000000..0f9aac5cf
--- /dev/null
+++ b/lib/Listener/AntiAbuseListener.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 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\Listener;
+
+use OCA\Mail\Events\BeforeMessageSentEvent;
+use OCA\Mail\Service\AntiAbuseService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class AntiAbuseListener implements IEventListener {
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var AntiAbuseService */
+ private $service;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ public function __construct(IUserManager $userManager,
+ AntiAbuseService $service,
+ LoggerInterface $logger) {
+ $this->service = $service;
+ $this->userManager = $userManager;
+ $this->logger = $logger;
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof BeforeMessageSentEvent)) {
+ return;
+ }
+
+ $user = $this->userManager->get($event->getAccount()->getUserId());
+ if ($user === null) {
+ $this->logger->error('User {user} for mail account {id} does not exist', [
+ 'user' => $event->getAccount()->getUserId(),
+ 'id' => $event->getAccount()->getId(),
+ ]);
+ }
+
+ $this->service->onBeforeMessageSent(
+ $user,
+ $event->getNewMessageData(),
+ );
+ }
+}
diff --git a/lib/Service/AntiAbuseService.php b/lib/Service/AntiAbuseService.php
new file mode 100644
index 000000000..ae4c09e19
--- /dev/null
+++ b/lib/Service/AntiAbuseService.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 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\Service;
+
+use OCA\Mail\AppInfo\Application;
+use OCA\Mail\Model\NewMessageData;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IMemcache;
+use OCP\IUser;
+use Psr\Log\LoggerInterface;
+use function implode;
+
+class AntiAbuseService {
+
+ /** @var IConfig */
+ private $config;
+
+ /** @var ICacheFactory */
+ private $cacheFactory;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ public function __construct(IConfig $config,
+ ICacheFactory $cacheFactory,
+ ITimeFactory $timeFactory,
+ LoggerInterface $logger) {
+ $this->config = $config;
+ $this->cacheFactory = $cacheFactory;
+ $this->timeFactory = $timeFactory;
+ $this->logger = $logger;
+ }
+
+ public function onBeforeMessageSent(IUser $user,
+ NewMessageData $messageData): void {
+ $abuseDetection = $this->config->getAppValue(
+ Application::APP_ID,
+ 'abuse_detection',
+ 'off'
+ );
+ if ($abuseDetection !== 'on') {
+ $this->logger->debug('Anti abuse detection is off');
+ return;
+ }
+
+ $this->checkNumberOfRecipients($user, $messageData);
+ $this->checkRateLimits($user, $messageData);
+ }
+
+ private function checkNumberOfRecipients(IUser $user,
+ NewMessageData $messageData): void {
+ $numberOfRecipientsThreshold = (int)$this->config->getAppValue(
+ Application::APP_ID,
+ 'abuse_number_of_recipients_per_message_threshold',
+ '0',
+ );
+ if ($numberOfRecipientsThreshold <= 1) {
+ return;
+ }
+
+ $actualNumberOfRecipients = count($messageData->getTo())
+ + count($messageData->getCc())
+ + count($messageData->getBcc());
+
+ if ($actualNumberOfRecipients >= $numberOfRecipientsThreshold) {
+ $this->logger->alert('User {user} sends to a suspicious number of recipients. {expected} are allowed. {actual} are used', [
+ 'user' => $user->getUID(),
+ 'expected' => $numberOfRecipientsThreshold,
+ 'actual' => $actualNumberOfRecipients,
+ ]);
+ }
+ }
+
+ private function checkRateLimits(IUser $user,
+ NewMessageData $messageData): void {
+ if (!$this->cacheFactory->isAvailable()) {
+ // No cache, no rate limits
+ return;
+ }
+ $cache = $this->cacheFactory->createDistributed('mail_anti_abuse');
+ if (!($cache instanceof IMemcache)) {
+ // This integration only works with caches that support inc and dec
+ return;
+ }
+
+ $this->checkRateLimitsForPeriod($user, $messageData, $cache, '15m', 15 * 60);
+ $this->checkRateLimitsForPeriod($user, $messageData, $cache, '1h', 60 * 60);
+ $this->checkRateLimitsForPeriod($user, $messageData, $cache, '1d', 24 * 60 * 60);
+ }
+
+ private function checkRateLimitsForPeriod(IUser $user,
+ NewMessageData $messageData,
+ IMemcache $cache,
+ string $id,
+ int $period): void {
+ $maxNumberOfMessages = (int)$this->config->getAppValue(
+ Application::APP_ID,
+ 'abuse_number_of_messages_per_' . $id,
+ '0',
+ );
+ if ($maxNumberOfMessages === 0) {
+ // No limit set
+ return;
+ }
+
+ $now = $this->timeFactory->getTime();
+
+ // Build blocks of periods per period size
+ $periodStart = ((int)($now / $period)) * $period;
+ $cacheKey = implode('_', ['counter', $id, $periodStart]);
+ $cache->add($cacheKey, 0);
+ $counter = $cache->inc($cacheKey, count($messageData->getTo()) + count($messageData->getCc()) + count($messageData->getBcc()));
+
+ if ($counter >= $maxNumberOfMessages) {
+ $this->logger->alert('User {user} sends a supcious number of messages within {period}. {expected} are allowed. {actual} have been sent', [
+ 'user' => $user->getUID(),
+ 'period' => $id,
+ 'expected' => $maxNumberOfMessages,
+ 'actual' => $counter,
+ ]);
+ }
+ }
+}
diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php
index e30531ab1..23fe8bdda 100644
--- a/lib/Service/MailTransmission.php
+++ b/lib/Service/MailTransmission.php
@@ -49,6 +49,7 @@ use OCA\Mail\Db\Alias;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
+use OCA\Mail\Events\BeforeMessageSentEvent;
use OCA\Mail\Events\DraftSavedEvent;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Events\SaveDraftEvent;
@@ -190,6 +191,10 @@ class MailTransmission implements IMailTransmission {
$mail->addMimePart($attachment);
}
+ $this->eventDispatcher->dispatchTyped(
+ new BeforeMessageSentEvent($account, $messageData, $replyData, $draft, $message, $mail)
+ );
+
// Send the message
try {
$mail->send($transport, false, false);
diff --git a/tests/Unit/Service/AntiAbuseServiceTest.php b/tests/Unit/Service/AntiAbuseServiceTest.php
new file mode 100644
index 000000000..ffbd22f4f
--- /dev/null
+++ b/tests/Unit/Service/AntiAbuseServiceTest.php
@@ -0,0 +1,199 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 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\Tests\Unit\Service;
+
+use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Account;
+use OCA\Mail\Address;
+use OCA\Mail\AddressList;
+use OCA\Mail\Model\NewMessageData;
+use OCA\Mail\Service\AntiAbuseService;
+use OCP\IMemcache;
+use OCP\IUser;
+use function array_map;
+use function range;
+
+class AntiAbuseServiceTest extends TestCase {
+
+ /** @var AntiAbuseService */
+ private $service;
+
+ /** @var ServiceMockObject */
+ private $serviceMock;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->serviceMock = $this->createServiceMock(AntiAbuseService::class);
+ $this->service = $this->serviceMock->getService();
+ }
+
+ public function testThresholdDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $account = $this->createMock(Account::class);
+ $messageData = new NewMessageData(
+ $account,
+ new AddressList([]),
+ new AddressList([]),
+ new AddressList([]),
+ 'subject',
+ 'henlo',
+ );
+ $this->serviceMock->getParameter('config')
+ ->expects(self::once())
+ ->method('getAppValue')
+ ->withConsecutive(
+ ['mail', 'abuse_detection', 'off'],
+ )->willReturnOnConsecutiveCalls(
+ 'off',
+ );
+ $this->serviceMock->getParameter('logger')
+ ->expects(self::never())
+ ->method('alert');
+
+ $this->service->onBeforeMessageSent(
+ $user,
+ $messageData,
+ );
+ }
+
+ public function testThresholdReached(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $account = $this->createMock(Account::class);
+ $messageData = new NewMessageData(
+ $account,
+ new AddressList(array_map(static function (int $i) {
+ return Address::fromRaw(
+ "user$i@domain.tld",
+ "user$i@domain.tld",
+ );
+ }, range(1, 50))),
+ new AddressList(array_map(static function (int $i) {
+ return Address::fromRaw(
+ "user$i@domain.tld",
+ "user$i@domain.tld",
+ );
+ }, range(51, 60))),
+ new AddressList(array_map(static function (int $i) {
+ return Address::fromRaw(
+ "user$i@domain.tld",
+ "user$i@domain.tld",
+ );
+ }, range(51, 70))),
+ 'subject',
+ 'henlo',
+ );
+ $this->serviceMock->getParameter('config')
+ ->method('getAppValue')
+ ->withConsecutive(
+ ['mail', 'abuse_detection', 'off'],
+ ['mail', 'abuse_number_of_recipients_per_message_threshold', '0'],
+ )->willReturnOnConsecutiveCalls(
+ 'on',
+ '50',
+ );
+ $this->serviceMock->getParameter('logger')
+ ->expects(self::once())
+ ->method('alert')
+ ->with(self::anything(), [
+ 'user' => 'user123',
+ 'expected' => 50,
+ 'actual' => 80,
+ ]);
+
+ $this->service->onBeforeMessageSent(
+ $user,
+ $messageData,
+ );
+ }
+
+ public function test15mThreshold(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $account = $this->createMock(Account::class);
+ $messageData = new NewMessageData(
+ $account,
+ new AddressList([
+ Address::fromRaw(
+ "user@domain.tld",
+ "user@domain.tld",
+ )
+ ]),
+ new AddressList([]),
+ new AddressList([]),
+ 'subject',
+ 'henlo',
+ );
+ $this->serviceMock->getParameter('config')
+ ->method('getAppValue')
+ ->withConsecutive(
+ ['mail', 'abuse_detection', 'off'],
+ ['mail', 'abuse_number_of_recipients_per_message_threshold', '0'],
+ ['mail', 'abuse_number_of_messages_per_15m', '0']
+ )->willReturnOnConsecutiveCalls(
+ 'on',
+ '0',
+ '5',
+ );
+ $this->serviceMock->getParameter('cacheFactory')
+ ->expects(self::once())
+ ->method('isAvailable')
+ ->willReturn(true);
+ $cache = $this->createMock(IMemcache::class);
+ $this->serviceMock->getParameter('cacheFactory')
+ ->expects(self::once())
+ ->method('createDistributed')
+ ->willReturn($cache);
+ $this->serviceMock->getParameter('timeFactory')
+ ->expects(self::once())
+ ->method('getTime')
+ ->willReturn(123456);
+ $cache->expects(self::once())
+ ->method('add')
+ ->with('counter_15m_123300', 0);
+ $cache->expects(self::once())
+ ->method('inc')
+ ->with('counter_15m_123300')
+ ->willReturn(5);
+ $this->serviceMock->getParameter('logger')
+ ->expects(self::once())
+ ->method('alert')
+ ->with(self::anything(), [
+ 'user' => 'user123',
+ 'period' => '15m',
+ 'expected' => 5,
+ 'actual' => 5,
+ ]);
+
+ $this->service->onBeforeMessageSent(
+ $user,
+ $messageData,
+ );
+ }
+}
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index 16f0951d4..e2909dab4 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.x-dev@">
<file src="lib/AppInfo/Application.php">
- <MissingDependency occurrences="14">
+ <MissingDependency occurrences="15">
+ <code>BeforeMessageSentEvent</code>
<code>DraftSavedEvent</code>
<code>MailboxesSynchronizedEvent</code>
<code>MessageDeletedEvent</code>
@@ -161,6 +162,11 @@
<code>Event</code>
</MissingDependency>
</file>
+ <file src="lib/Events/BeforeMessageSentEvent.php">
+ <MissingDependency occurrences="1">
+ <code>Event</code>
+ </MissingDependency>
+ </file>
<file src="lib/Events/DraftSavedEvent.php">
<MissingDependency occurrences="1">
<code>Event</code>
@@ -218,6 +224,12 @@
<code>MessageSentEvent</code>
</MissingDependency>
</file>
+ <file src="lib/Listener/AntiAbuseListener.php">
+ <MissingDependency occurrences="2">
+ <code>AntiAbuseListener</code>
+ <code>BeforeMessageSentEvent</code>
+ </MissingDependency>
+ </file>
<file src="lib/Listener/DashboardPanelListener.php">
<MissingDependency occurrences="2">
<code>DashboardPanelListener</code>
@@ -313,7 +325,8 @@
</MissingDependency>
</file>
<file src="lib/Service/MailTransmission.php">
- <MissingDependency occurrences="6">
+ <MissingDependency occurrences="7">
+ <code>BeforeMessageSentEvent</code>
<code>DraftSavedEvent</code>
<code>DraftSavedEvent</code>
<code>MessageSentEvent</code>