config = $config; $this->cacheFactory = $cacheFactory; $this->timeFactory = $timeFactory; $this->logger = $logger; } public function onBeforeMessageSent(IUser $user, LocalMessage $localMessage): 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, $localMessage); $this->checkRateLimits($user, $localMessage); } private function checkNumberOfRecipients(IUser $user, LocalMessage $message): void { $numberOfRecipientsThreshold = (int)$this->config->getAppValue( Application::APP_ID, 'abuse_number_of_recipients_per_message_threshold', '0', ); if ($numberOfRecipientsThreshold <= 1) { return; } $actualNumberOfRecipients = count($message->getRecipients()); if ($actualNumberOfRecipients >= $numberOfRecipientsThreshold) { $message->setStatus(LocalMessage::STATUS_TOO_MANY_RECIPIENTS); $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, LocalMessage $message): 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; } $ratelimited = ( $this->checkRateLimitsForPeriod($user, $cache, '15m', 15 * 60, $message) || $this->checkRateLimitsForPeriod($user, $cache, '1h', 60 * 60, $message) || $this->checkRateLimitsForPeriod($user, $cache, '1d', 24 * 60 * 60, $message) ); if ($ratelimited) { $message->setStatus(LocalMessage::STATUS_RATELIMIT); } } private function checkRateLimitsForPeriod(IUser $user, IMemcache $cache, string $id, int $period, LocalMessage $message): bool { $maxNumberOfMessages = (int)$this->config->getAppValue( Application::APP_ID, 'abuse_number_of_messages_per_' . $id, '0', ); if ($maxNumberOfMessages === 0) { // No limit set return false; } $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($message->getRecipients())); 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, ]); return true; } return false; } }