138 lines
3.7 KiB
PHP
138 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2021 F7cloud GmbH and F7cloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Mail\Service;
|
|
|
|
use OCA\Mail\AppInfo\Application;
|
|
use OCA\Mail\Db\LocalMessage;
|
|
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, 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;
|
|
}
|
|
}
|