Page MenuHomePhorge

D5862.1775155782.diff
No OneTemporary

Authored By
Unknown
Size
7 KB
Referenced Files
None
Subscribers
None

D5862.1775155782.diff

diff --git a/doc/Policies/RATELIMIT.md b/doc/Policies/RATELIMIT.md
new file mode 100644
--- /dev/null
+++ b/doc/Policies/RATELIMIT.md
@@ -0,0 +1,40 @@
+# Rate Limit Policy
+
+Mail submission requests are rate limited. For this we configure mail exchanger (Postfix)
+to check the sender's address in the policy service, which is part of the Kolab Cockpit and
+uses HTTP API.
+
+## Rules
+
+Here's the rules applied to a submission (in order):
+
+1. Mail from soft-deleted or suspended senders is put on HOLD.
+2. Whitelisted senders are NOT rate limited (see the Whitelists section below).
+3. Accounts with 100% discount are NOT rate limited.
+4. Accounts with positive balance and any payments are NOT rate limited.
+5. If a sender or all users in an account, in last 60 minutes:
+ a) sent at least `RATELIMIT_MAX_MESSAGES` (default: 10) messages or
+ b) sent messages to at least `RATELIMIT_MAX_RECIPIENTS` (default: 250) recipients,
+ sumbission if DEFER-ed.
+
+## Automatic suspending
+
+A sender (or the whole account) created in last two months gets suspended if the submission rate
+is exceeded too much. Limits are:
+
+- count of messages in last 60 minutes: (`RATELIMIT_MAX_MESSAGES * RATELIMIT_SUSPEND_FACTOR`)
+- count of recipients in last 60 minutes: (`RATELIMIT_MAX_RECIPIENTS * RATELIMIT_SUSPEND_FACTOR`)
+
+## Whitelists
+
+There is an exceptions list for users and domains. Mail from such senders is NOT rate limited.
+The whitelist can be managed in command line using these three commands:
+
+```
+$ php artisan policy:ratelimit:whitelist:create {object}
+$ php artisan policy:ratelimit:whitelist:delete {object}
+$ php artisan policy:ratelimit:whitelist:read
+```
+
+There is an additional static whitelist available in configuration (`RATELIMIT_WHITELIST`).
+This list accepts full email addresses, no domains.
diff --git a/src/app/Policy/RateLimit.php b/src/app/Policy/RateLimit.php
--- a/src/app/Policy/RateLimit.php
+++ b/src/app/Policy/RateLimit.php
@@ -99,11 +99,6 @@
return new Response(Response::ACTION_DUNNO);
}
- // user nor domain whitelisted, continue scrutinizing the request
- sort($recipients);
- $recipientCount = count($recipients);
- $recipientHash = hash('sha256', implode(',', $recipients));
-
// Retrieve the wallet to get to the owner
$wallet = $user->wallet();
@@ -114,6 +109,11 @@
$owner = $wallet->owner;
+ // user nor domain whitelisted, continue scrutinizing the request
+ sort($recipients);
+ $recipientCount = count($recipients);
+ $recipientHash = hash('sha256', implode(',', $recipients));
+
// find or create the request
$request = self::where('recipient_hash', $recipientHash)
->where('user_id', $user->id)
@@ -149,40 +149,42 @@
->exists();
if ($isPayer) {
- return new Response();
+ return new Response(Response::ACTION_DUNNO);
}
}
+ $max_messages = config('app.ratelimit_max_messages');
+ $max_recipients = config('app.ratelimit_max_recipients');
+ $suspend_factor = config('app.ratelimit_suspend_factor');
+
+ $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
+
// Examine the rates at which the owner (or its users) is sending
$ownerRates = self::where('owner_id', $owner->id)
->where('updated_at', '>=', Carbon::now()->subHour());
- if (($count = $ownerRates->count()) >= 10) {
- // automatically suspend (recursively) if 2.5 times over the original limit and younger than two months
- $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($count >= 25 && $owner->created_at > $ageThreshold) {
+ if (($count = $ownerRates->count()) >= $max_messages) {
+ // automatically suspend (recursively) if X times over the original limit and younger than two months
+ if ($count >= $max_messages * $suspend_factor && $owner->created_at > $ageThreshold) {
$owner->suspendAccount();
}
return new Response(
Response::ACTION_DEFER_IF_PERMIT,
- 'The account is at 10 messages per hour, cool down.',
+ "The account is at {$max_messages} messages per hour, cool down.",
403
);
}
- if (($recipientCount = $ownerRates->sum('recipient_count')) >= 100) {
- // automatically suspend if 2.5 times over the original limit and younger than two months
- $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($recipientCount >= 250 && $owner->created_at > $ageThreshold) {
+ if (($recipientCount = $ownerRates->sum('recipient_count')) >= $max_recipients) {
+ // automatically suspend if X times over the original limit and younger than two months
+ if ($recipientCount >= $max_recipients * $suspend_factor && $owner->created_at > $ageThreshold) {
$owner->suspendAccount();
}
return new Response(
Response::ACTION_DEFER_IF_PERMIT,
- 'The account is at 100 recipients per hour, cool down.',
+ "The account is at {$max_recipients} recipients per hour, cool down.",
403
);
}
@@ -192,32 +194,28 @@
$userRates = self::where('user_id', $user->id)
->where('updated_at', '>=', Carbon::now()->subHour());
- if (($count = $userRates->count()) >= 10) {
- // automatically suspend if 2.5 times over the original limit and younger than two months
- $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($count >= 25 && $user->created_at > $ageThreshold) {
+ if (($count = $userRates->count()) >= $max_messages) {
+ // automatically suspend if X times over the original limit and younger than two months
+ if ($count >= $max_messages * $suspend_factor && $user->created_at > $ageThreshold) {
$user->suspend();
}
return new Response(
Response::ACTION_DEFER_IF_PERMIT,
- 'User is at 10 messages per hour, cool down.',
+ "User is at {$max_messages} messages per hour, cool down.",
403
);
}
- if (($recipientCount = $userRates->sum('recipient_count')) >= 100) {
- // automatically suspend if 2.5 times over the original limit
- $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($recipientCount >= 250 && $user->created_at > $ageThreshold) {
+ if (($recipientCount = $userRates->sum('recipient_count')) >= $max_recipients) {
+ // automatically suspend if X times over the original limit
+ if ($recipientCount >= $max_recipients * $suspend_factor && $user->created_at > $ageThreshold) {
$user->suspend();
}
return new Response(
Response::ACTION_DEFER_IF_PERMIT,
- 'The account is at 100 recipients per hour, cool down.',
+ "The account is at {$max_recipients} recipients per hour, cool down.",
403
);
}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -275,6 +275,9 @@
'woat_ns2' => env('WOAT_NS2', 'ns02.' . env('APP_DOMAIN')),
'ratelimit_whitelist' => explode(',', env('RATELIMIT_WHITELIST', '')),
+ 'ratelimit_max_messages' => (int) env('RATELIMIT_MAX_MESSAGES', 10),
+ 'ratelimit_max_recipients' => (int) env('RATELIMIT_MAX_RECIPIENTS', 100),
+ 'ratelimit_suspend_factor' => (float) env('RATELIMIT_SUSPEND_FACTOR', 2.5),
'companion_download_link' => env(
'COMPANION_DOWNLOAD_LINK',
"https://mirror.apheleia-it.ch/pub/companion-app-beta.apk"

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 6:49 PM (19 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820348
Default Alt Text
D5862.1775155782.diff (7 KB)

Event Timeline