Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117766397
D5865.1775232543.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
42 KB
Referenced Files
None
Subscribers
None
D5865.1775232543.diff
View Options
diff --git a/doc/Policies/RATELIMIT.md b/doc/Policies/RATELIMIT.md
--- a/doc/Policies/RATELIMIT.md
+++ b/doc/Policies/RATELIMIT.md
@@ -10,20 +10,25 @@
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.
+3. If a sender or all users in an account, in last 60 minutes sent messages to
+ at least `RATELIMIT_MAX_RECIPIENTS` (default: 100) recipients, sumbission is DEFER-ed.
+ The limit for restricted (new) accounts is different (`RATELIMIT_MAX_RECIPIENTS_RESTRICTED`),
+ and defaults to 1/4th of the limit for non-restricted accounts.
+4. If a sender or all users in an account, in last 24 hours sent messages to
+ at least `RATELIMIT_MAX_RECIPIENTS_DAILY` (default: 1000) recipients, sumbission is DEFER-ed.
+ The limit for restricted accounts is different (`RATELIMIT_MAX_RECIPIENTS_RESTRICTED_DAILY`),
+ and defaults to 1/4th of the limit for non-restricted accounts.
## 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:
+A sender (or the whole account) gets suspended if the submission rate is exceeded too much.
-- 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`)
+1. Limit to number of recipients in last 60 minutes is:
+ - for all accounts: `RATELIMIT_SUSPEND_MAX_RECIPIENTS`
+ - for restricted accounts: `RATELIMIT_SUSPEND_MAX_RECIPIENTS_RESTRICTED`
+2. Limit to number of recipients in last 24 hours is:
+ - for all accounts: `RATELIMIT_SUSPEND_MAX_RECIPIENTS_DAILY`
+ - for restricted accounts: `RATELIMIT_SUSPEND_MAX_RECIPIENTS_RESTRICTED_DAILY`
## Whitelists
diff --git a/src/app/Console/Commands/Policy/RateLimit/ExpungeCommand.php b/src/app/Console/Commands/Policy/RateLimit/ExpungeCommand.php
--- a/src/app/Console/Commands/Policy/RateLimit/ExpungeCommand.php
+++ b/src/app/Console/Commands/Policy/RateLimit/ExpungeCommand.php
@@ -29,6 +29,7 @@
*/
public function handle()
{
- RateLimit::where('updated_at', '<', Carbon::now()->subMonthsWithoutOverflow(6))->delete();
+ $months = config('policy.ratelimit.retention_months');
+ RateLimit::where('updated_at', '<', Carbon::now()->subMonthsWithoutOverflow($months))->delete();
}
}
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
@@ -2,8 +2,8 @@
namespace App\Policy;
+use App\EventLog;
use App\Traits\BelongsToUserTrait;
-use App\Transaction;
use App\User;
use App\UserAlias;
use App\Utils;
@@ -42,7 +42,7 @@
$sender = $local . '@' . $domain;
- if (in_array($sender, \config('app.ratelimit_whitelist', []), true)) {
+ if (in_array($sender, \config('policy.ratelimit.whitelist', []), true)) {
return new Response(Response::ACTION_DUNNO);
}
@@ -110,6 +110,7 @@
$owner = $wallet->owner;
// user nor domain whitelisted, continue scrutinizing the request
+ // TODO: Exclude local users from the count? Could be expensive, but at least exclude the same domain as the sender?
sort($recipients);
$recipientCount = count($recipients);
$recipientHash = hash('sha256', implode(',', $recipients));
@@ -133,94 +134,80 @@
$request->save();
}
- // exempt owners that have 100% discount.
- if ($wallet->discount && $wallet->discount->discount == 100) {
- return new Response(Response::ACTION_DUNNO);
+ // Examine the hourly rates at which the account is sending
+ if ($error = self::checkLimits($user, $owner, false)) {
+ return new Response(Response::ACTION_DEFER_IF_PERMIT, $error, 403);
}
- // exempt owners that currently maintain a positive balance and made any payments.
- // Because there might be users that pay via external methods (and don't have Payment records)
- // we can't check only the Payments table. Instead we assume that a credit/award transaction
- // is enough to consider the user a "paying user" for purpose of the rate limit.
- if ($wallet->balance > 0) {
- $isPayer = $wallet->transactions()
- ->whereIn('type', [Transaction::WALLET_AWARD, Transaction::WALLET_CREDIT])
- ->where('amount', '>', 0)
- ->exists();
-
- if ($isPayer) {
- return new Response(Response::ACTION_DUNNO);
- }
+ // Examine the daily rates at which the account is sending
+ if ($error = self::checkLimits($user, $owner, true)) {
+ return new Response(Response::ACTION_DEFER_IF_PERMIT, $error, 403);
}
- $max_messages = config('app.ratelimit_max_messages');
- $max_recipients = config('app.ratelimit_max_recipients');
- $suspend_factor = config('app.ratelimit_suspend_factor');
+ return new Response(Response::ACTION_DUNNO);
+ }
- $ageThreshold = Carbon::now()->subMonthsWithoutOverflow(2);
+ /**
+ * Check number of recipients limit (per hour or per day)
+ */
+ private static function checkLimits($user, $owner, bool $daily = false): ?string
+ {
+ $suffix = $daily ? '_daily' : '';
- // 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());
+ $max_recipients = config('policy.ratelimit.max_recipients' . $suffix);
+ $max_recipients_restricted = config('policy.ratelimit.max_recipients_restricted' . $suffix);
+ $suspend_max_recipients = config('policy.ratelimit.suspend_max_recipients' . $suffix);
+ $suspend_max_recipients_restricted = config('policy.ratelimit.suspend_max_recipients_restricted' . $suffix);
- 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();
+ // New users should get a lower limit
+ if ($owner->isRestricted()) {
+ if (!$max_recipients_restricted) {
+ $max_recipients_restricted = (int) ($max_recipients / 4);
}
- return new Response(
- Response::ACTION_DEFER_IF_PERMIT,
- "The account is at {$max_messages} messages per hour, cool down.",
- 403
- );
+ $max_recipients = $max_recipients_restricted;
}
- 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 {$max_recipients} recipients per hour, cool down.",
- 403
- );
+ if (!$max_recipients) {
+ return null;
}
- // Examine the rates at which the user is sending (if not also the owner)
- if ($user->id != $owner->id) {
- $userRates = self::where('user_id', $user->id)
- ->where('updated_at', '>=', Carbon::now()->subHour());
+ $start = Carbon::now()->subHours($daily ? 24 : 1);
+
+ $count = self::where('owner_id', $owner->id)->where('updated_at', '>=', $start)->sum('recipient_count');
- 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();
+ if ($count >= $max_recipients) {
+ $type = $daily ? 'daily' : 'hourly';
+ \Log::info("[Rate-Limit] {$owner->email} {$type} recipients count: {$count}"
+ . ($owner->id != $user->id ? ". Sender: {$user->email}" : ''));
+
+ // New users should get a lower limit for suspension
+ if ($owner->isRestricted()) {
+ if (!$suspend_max_recipients_restricted) {
+ $suspend_max_recipients_restricted = (int) ($suspend_max_recipients / 4);
}
- return new Response(
- Response::ACTION_DEFER_IF_PERMIT,
- "User is at {$max_messages} messages per hour, cool down.",
- 403
- );
+ $suspend_max_recipients = $suspend_max_recipients_restricted;
}
- 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();
- }
+ // automatically suspend if too much over the original limit
+ if ($suspend_max_recipients && $count >= $suspend_max_recipients) {
+ $owner->suspendAccount();
+
+ // TODO: We could include in the message who sent how many messages
+ $msg = "Exceeded {$type} rate limit ({$suspend_max_recipients})";
+ EventLog::createFor($owner, EventLog::TYPE_SUSPENDED, $msg);
+
+ \Log::warning("[Rate-Limit] Suspended spammer {$owner->email}"
+ . ($owner->id != $user->id ? ". Sender: {$user->email}" : ''));
- return new Response(
- Response::ACTION_DEFER_IF_PERMIT,
- "The account is at {$max_recipients} recipients per hour, cool down.",
- 403
- );
+ // TODO: Send a notification email to the account owner?
}
+
+ $type = $daily ? 'per day' : 'per hour';
+ return "The account is at {$max_recipients} recipients {$type}, cool down.";
}
- return new Response(Response::ACTION_DUNNO);
+ return null;
}
}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -274,10 +274,6 @@
'woat_ns1' => env('WOAT_NS1', 'ns01.' . env('APP_DOMAIN')),
'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"
diff --git a/src/config/policy.php b/src/config/policy.php
new file mode 100644
--- /dev/null
+++ b/src/config/policy.php
@@ -0,0 +1,26 @@
+<?php
+
+return [
+ // ---------------------------------------
+ // Mail submission rate limit
+ // ---------------------------------------
+ 'ratelimit' => [
+ // Whitelist of email addresses
+ 'whitelist' => explode(',', env('RATELIMIT_WHITELIST', '')),
+
+ // Hourly limits
+ 'max_recipients' => (int) env('RATELIMIT_MAX_RECIPIENTS', 100),
+ 'max_recipients_restricted' => (int) env('RATELIMIT_MAX_RECIPIENTS_RESTRICTED'),
+ 'suspend_max_recipients' => (int) env('RATELIMIT_SUSPEND_MAX_RECIPIENTS', 250),
+ 'suspend_max_recipients_restricted' => (int) env('RATELIMIT_SUSPEND_MAX_RECIPIENTS_RESTRICTED'),
+
+ // Daily limits
+ 'max_recipients_daily' => (int) env('RATELIMIT_MAX_RECIPIENTS_DAILY', 1000),
+ 'max_recipients_restricted_daily' => (int) env('RATELIMIT_MAX_RECIPIENTS_RESTRICTED_DAILY'),
+ 'suspend_max_recipients_daily' => (int) env('RATELIMIT_SUSPEND_MAX_RECIPIENTS_DAILY', 2000),
+ 'suspend_max_recipients_restricted_daily' => (int) env('RATELIMIT_SUSPEND_MAX_RECIPIENTS_RESTRICTED_DAILY'),
+
+ // How long retain the rate-limit tracking records
+ 'retention_months' => (int) env('RATELIMIT_RETENTION_MONTHS', 3),
+ ],
+];
diff --git a/src/tests/Feature/Controller/PolicyTest.php b/src/tests/Feature/Controller/PolicyTest.php
--- a/src/tests/Feature/Controller/PolicyTest.php
+++ b/src/tests/Feature/Controller/PolicyTest.php
@@ -468,8 +468,8 @@
$this->assertSame('HOLD', $json['response']);
$this->assertSame('Sender deleted or suspended', $json['reason']);
- // Test app.ratelimit_whitelist
- \config(['app.ratelimit_whitelist' => ['alias@test.domain']]);
+ // Test whitelist configuration
+ \config(['policy.ratelimit.whitelist' => ['alias@test.domain']]);
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(200);
diff --git a/src/tests/Feature/Policy/RateLimitTest.php b/src/tests/Feature/Policy/RateLimitTest.php
--- a/src/tests/Feature/Policy/RateLimitTest.php
+++ b/src/tests/Feature/Policy/RateLimitTest.php
@@ -2,11 +2,8 @@
namespace Tests\Feature\Policy;
-use App\Discount;
-use App\Domain;
use App\Policy\RateLimit;
use App\Policy\Response;
-use App\Transaction;
use App\User;
use Tests\TestCase;
@@ -22,36 +19,34 @@
$this->setUpTest();
RateLimit::query()->delete();
- Transaction::query()->delete();
+
+ // Set some low limits for tests
+ \config([
+ 'policy.ratelimit.whitelist' => [],
+ 'policy.ratelimit.max_recipients' => 10,
+ 'policy.ratelimit.max_recipients_restricted' => 5,
+ 'policy.ratelimit.suspend_max_recipients' => 20,
+ 'policy.ratelimit.suspend_max_recipients_restricted' => 10,
+ 'policy.ratelimit.max_recipients_daily' => 20,
+ 'policy.ratelimit.max_recipients_restricted_daily' => 10,
+ 'policy.ratelimit.suspend_max_recipients_daily' => 30,
+ 'policy.ratelimit.suspend_max_recipients_restricted_daily' => 20,
+ ]);
}
protected function tearDown(): void
{
RateLimit::query()->delete();
- Transaction::query()->delete();
parent::tearDown();
}
/**
- * Test verifyRequest() method for an individual account cases
+ * Test verifyRequest() method for an individual account
*/
public function testVerifyRequestIndividualAccount()
{
- // Verify an individual can send an email unrestricted, so long as the account is active.
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['someone@test.domain']);
-
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
-
- // Verify a whitelisted individual account is in fact whitelisted
- RateLimit::truncate();
- RateLimit\Whitelist::create([
- 'whitelistable_id' => $this->publicDomainUser->id,
- 'whitelistable_type' => User::class,
- ]);
-
+ // Verify an individual can send an email unrestricted
// first 9 requests
for ($i = 1; $i <= 9; $i++) {
$result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
@@ -60,372 +55,275 @@
$this->assertSame('', $result->reason);
}
- // normally, request #10 would get blocked
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['0010@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
-
- // requests 11 through 26
- for ($i = 11; $i <= 26; $i++) {
- $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
-
- // Verify an individual trial user is automatically suspended.
- RateLimit::truncate();
- RateLimit\Whitelist::truncate();
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
-
- // the next 16 requests for 25 total
- for ($i = 10; $i <= 25; $i++) {
+ // requests 10 through 19 get DEFERed
+ for ($i = 10; $i <= 19; $i++) {
$result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 10 messages per hour, cool down.', $result->reason);
+ $this->assertSame('The account is at 10 recipients per hour, cool down.', $result->reason);
}
+ // not suspended yet
$this->publicDomainUser->refresh();
- $this->assertTrue($this->publicDomainUser->isSuspended());
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- // Verify a suspended individual can not send an email
- RateLimit::truncate();
-
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['someone@test.domain']);
+ // Test that message to the same recipient is not being counted as new
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0019@test.domain']);
$this->assertSame(403, $result->code);
- $this->assertSame(Response::ACTION_HOLD, $result->action);
- $this->assertSame('Sender deleted or suspended', $result->reason);
-
- // Verify an individual can run out of messages per hour
- RateLimit::truncate();
- $this->publicDomainUser->unsuspend();
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 10 recipients per hour, cool down.', $result->reason);
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ // not suspended yet
+ $this->publicDomainUser->refresh();
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- // the tenth request should be blocked
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['0010@test.domain']);
+ // request #20 (with a new recipient) should suspend
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0020@test.domain']);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 10 messages per hour, cool down.', $result->reason);
-
- // Verify a paid for individual account does not simply run out of messages
- RateLimit::truncate();
+ $this->assertSame('The account is at 10 recipients per hour, cool down.', $result->reason);
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ $this->publicDomainUser->refresh();
+ $this->assertTrue($this->publicDomainUser->isSuspended());
- // the tenth request should be blocked
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['0010@test.domain']);
+ // next request is on HOLD
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0030@test.domain']);
$this->assertSame(403, $result->code);
- $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 10 messages per hour, cool down.', $result->reason);
+ $this->assertSame(Response::ACTION_HOLD, $result->action);
+ $this->assertSame('Sender deleted or suspended', $result->reason);
+
+ $this->publicDomainUser->unsuspend();
- // create a credit transaction
- $this->publicDomainUser->wallets()->first()->credit(1111);
+ // Test whitelisted user
+ $whitelist = RateLimit\Whitelist::create([
+ 'whitelistable_id' => $this->publicDomainUser->id,
+ 'whitelistable_type' => User::class,
+ ]);
- // the next request should now be allowed
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['0010@test.domain']);
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0040@test.domain']);
$this->assertSame(200, $result->code);
$this->assertSame(Response::ACTION_DUNNO, $result->action);
$this->assertSame('', $result->reason);
- // Verify a 100% discount for individual account does not simply run out of messages
- RateLimit::truncate();
- $wallet = $this->publicDomainUser->wallets()->first();
- $wallet->discount()->associate(Discount::where('description', 'Free Account')->first());
- $wallet->save();
+ // Test whitelisted but suspended user
+ $this->publicDomainUser->suspend();
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0050@test.domain']);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_HOLD, $result->action);
+ $this->assertSame('Sender deleted or suspended', $result->reason);
- // the tenth request should now be allowed
- $result = RateLimit::verifyRequest($this->publicDomainUser, ['someone@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ // Test deleted user
+ $this->publicDomainUser->unsuspend();
+ $this->publicDomainUser->delete();
- // Verify that an individual user in its trial can run out of recipients.
- RateLimit::truncate();
- $wallet->discount_id = null;
- $wallet->balance = 0;
- $wallet->save();
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0060@test.domain']);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_HOLD, $result->action);
+ $this->assertSame('Sender deleted or suspended', $result->reason);
+ }
+
+ /**
+ * Test verifyRequest() method for an individual account regarding daily limits
+ */
+ public function testVerifyRequestIndividualAccountDailyLimits()
+ {
+ // Create first 15 requests
+ for ($i = 1; $i <= 15; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ }
- // first 2 requests (34 recipients each)
- for ($x = 1; $x <= 2; $x++) {
- $recipients = [];
- for ($y = 1; $y <= 34; $y++) {
- $recipients[] = sprintf('%04d@test.domain', $x * $y);
- }
+ // and move them 2h back
+ RateLimit::query()->update(['updated_at' => now()->subHours(3)]);
- $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
+ // next 4 messages should be unlimited again
+ for ($i = 16; $i <= 19; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
$this->assertSame(200, $result->code);
$this->assertSame(Response::ACTION_DUNNO, $result->action);
$this->assertSame('', $result->reason);
}
- // on to the third request, resulting in 102 recipients total
- $recipients = [];
- for ($y = 1; $y <= 34; $y++) {
- $recipients[] = sprintf('%04d@test.domain', 3 * $y);
+ // next 5 should not suspend the user yet, but block mail delivery
+ for ($i = 20; $i <= 24; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 20 recipients per day, cool down.', $result->reason);
}
- $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
- $this->assertSame(403, $result->code);
- $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 100 recipients per hour, cool down.', $result->reason);
-
- // Verify that an individual user that has paid for its account doesn't run out of recipients.
- RateLimit::truncate();
- $wallet->balance = 0;
- $wallet->save();
+ // not suspended yet
+ $this->publicDomainUser->refresh();
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", $x * $y);
- }
+ RateLimit::query()->where('updated_at', '>=', now()->subHour())->update(['updated_at' => now()->subHours(2)]);
- $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ // next hour
+ for ($i = 25; $i <= 29; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 20 recipients per day, cool down.', $result->reason);
}
- // on to the third request, resulting in 102 recipients total
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", 2 * $y);
- }
+ // not suspended yet
+ $this->publicDomainUser->refresh();
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
+ // message #30, auto-suspend limit reached
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0030@test.domain']);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 100 recipients per hour, cool down.', $result->reason);
+ $this->assertSame('The account is at 20 recipients per day, cool down.', $result->reason);
- $wallet->award(11111);
-
- // the tenth request should now be allowed
- $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ $this->publicDomainUser->refresh();
+ $this->assertTrue($this->publicDomainUser->isSuspended());
}
/**
- * Test verifyRequest() with group account cases
+ * Test verifyRequest() method for a restricted account
*/
- public function testVerifyRequestGroupAccount()
+ public function testVerifyRequestRestrictedAccount()
{
- // Verify that a group owner can send email
- $result = RateLimit::verifyRequest($this->domainOwner, ['someone@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
-
- // Verify that a domain owner can run out of messages
- RateLimit::truncate();
+ $this->publicDomainUser->status |= User::STATUS_RESTRICTED;
+ $this->publicDomainUser->save();
- // first 9 requests
- for ($i = 0; $i < 9; $i++) {
- $result = RateLimit::verifyRequest($this->domainOwner, [sprintf("%04d@test.domain", $i)]);
+ // Verify an individual can send an email unrestricted
+ // first 4 requests
+ for ($i = 1; $i <= 4; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
$this->assertSame(200, $result->code);
$this->assertSame(Response::ACTION_DUNNO, $result->action);
$this->assertSame('', $result->reason);
}
- // the tenth request should be blocked
- $result = RateLimit::verifyRequest($this->domainOwner, ['0010@test.domain']);
- $this->assertSame(403, $result->code);
- $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 10 messages per hour, cool down.', $result->reason);
-
- $this->domainOwner->refresh();
- $this->assertFalse($this->domainOwner->isSuspended());
-
- // Verify that a domain owner can run out of recipients
- RateLimit::truncate();
- $this->domainOwner->unsuspend();
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ // requests 5 through 10 get DEFERed
+ for ($i = 5; $i <= 9; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 5 recipients per hour, cool down.', $result->reason);
}
- // on to the third request, resulting in 102 recipients total
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", 2 * $y);
- }
+ // not suspended yet
+ $this->publicDomainUser->refresh();
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
+ // request #10 should suspend
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0020@test.domain']);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 100 recipients per hour, cool down.', $result->reason);
-
- $this->domainOwner->refresh();
- $this->assertFalse($this->domainOwner->isSuspended());
+ $this->assertSame('The account is at 5 recipients per hour, cool down.', $result->reason);
- // Verify that a paid for group account can send messages.
- RateLimit::truncate();
+ $this->publicDomainUser->refresh();
+ $this->assertTrue($this->publicDomainUser->isSuspended());
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", $x * $y);
- }
+ // next request is on HOLD
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0030@test.domain']);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_HOLD, $result->action);
+ $this->assertSame('Sender deleted or suspended', $result->reason);
+ }
- $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ /**
+ * Test verifyRequest() method for a restricted account regarding daily limits
+ */
+ public function testVerifyRequestRestrictedAccountDailyLimits()
+ {
+ $this->publicDomainUser->status |= User::STATUS_RESTRICTED;
+ $this->publicDomainUser->save();
- // on to the third request, resulting in 102 recipients total
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", 2 * $y);
+ // Create first 8 requests
+ for ($i = 1; $i <= 8; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
}
- $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
- $this->assertSame(403, $result->code);
- $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 100 recipients per hour, cool down.', $result->reason);
+ // and move them 2h back
+ RateLimit::query()->update(['updated_at' => now()->subHours(4)]);
- $wallet = $this->domainOwner->wallets()->first();
- $wallet->credit(1111);
-
- $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
+ // new hour, next request should be unlimited
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0009@test.domain']);
$this->assertSame(200, $result->code);
$this->assertSame(Response::ACTION_DUNNO, $result->action);
$this->assertSame('', $result->reason);
- // Verify that a user for a domain owner can send email.
- RateLimit::truncate();
+ // next 3 messages should reach daily limit
+ for ($i = 10; $i <= 12; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 10 recipients per day, cool down.', $result->reason);
+ }
- $result = RateLimit::verifyRequest($this->domainUsers[0], ['someone@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ RateLimit::query()->where('updated_at', '>=', now()->subHour())->update(['updated_at' => now()->subHours(3)]);
- // Verify that the users in a group account can be limited.
- RateLimit::truncate();
- $wallet->balance = 0;
- $wallet->save();
+ // new hour, 4 requests pass unlimited
+ for ($i = 13; $i <= 16; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 10 recipients per day, cool down.', $result->reason);
+ }
- // the first eight requests should be accepted
- for ($i = 0; $i < 8; $i++) {
- $result = RateLimit::verifyRequest($this->domainUsers[0], [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ RateLimit::query()->where('updated_at', '>=', now()->subHour())->update(['updated_at' => now()->subHours(2)]);
+
+ // new hour, 3 requests pass unlimited
+ for ($i = 17; $i <= 19; $i++) {
+ $result = RateLimit::verifyRequest($this->publicDomainUser, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 10 recipients per day, cool down.', $result->reason);
}
- // the ninth request from another group user should also be accepted
- $result = RateLimit::verifyRequest($this->domainUsers[1], ['0009@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
+ // not suspended yet
+ $this->publicDomainUser->refresh();
+ $this->assertFalse($this->publicDomainUser->isSuspended());
- // the tenth request from another group user should be rejected
- $result = RateLimit::verifyRequest($this->domainUsers[1], ['0010@test.domain']);
+ // request #20 should suspend the user
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0020@test.domain']);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 10 messages per hour, cool down.', $result->reason);
+ $this->assertSame('The account is at 10 recipients per day, cool down.', $result->reason);
- // Test a trial user
- RateLimit::truncate();
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", $x * $y);
- }
+ $this->publicDomainUser->refresh();
+ $this->assertTrue($this->publicDomainUser->isSuspended());
+ }
- $result = RateLimit::verifyRequest($this->domainUsers[0], $recipients);
+ /**
+ * Test verifyRequest() with group account cases
+ */
+ public function testVerifyRequestGroupAccount()
+ {
+ // send some mail as one user
+ for ($i = 1; $i <= 9; $i++) {
+ $result = RateLimit::verifyRequest($this->domainOwner, [sprintf("%04d@test.domain", $i)]);
$this->assertSame(200, $result->code);
$this->assertSame(Response::ACTION_DUNNO, $result->action);
$this->assertSame('', $result->reason);
}
- // on to the third request, resulting in 102 recipients total
- $recipients = [];
- for ($y = 0; $y < 34; $y++) {
- $recipients[] = sprintf("%04d@test.domain", 2 * $y);
+ // the tenth request should be blocked even if done by another user in that account
+ for ($i = 10; $i <= 19; $i++) {
+ $result = RateLimit::verifyRequest($this->jack, [sprintf("%04d@test.domain", $i)]);
+ $this->assertSame(403, $result->code);
+ $this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
+ $this->assertSame('The account is at 10 recipients per hour, cool down.', $result->reason);
}
- $result = RateLimit::verifyRequest($this->domainUsers[0], $recipients);
+ $this->domainOwner->refresh();
+ $this->assertFalse($this->domainOwner->isSuspended());
+
+ // Finally another user can suspend the whole account
+ $result = RateLimit::verifyRequest($this->joe, ['0202@test.domain']);
$this->assertSame(403, $result->code);
$this->assertSame(Response::ACTION_DEFER_IF_PERMIT, $result->action);
- $this->assertSame('The account is at 100 recipients per hour, cool down.', $result->reason);
-
- // Verify a whitelisted group domain is in fact whitelisted
- RateLimit::truncate();
- RateLimit\Whitelist::create([
- 'whitelistable_id' => $this->domainHosted->id,
- 'whitelistable_type' => Domain::class,
- ]);
-
- $request = [
- 'sender' => $this->domainUsers[0]->email,
- 'recipients' => [],
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $result = RateLimit::verifyRequest($this->domainUsers[0], [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ $this->assertSame('The account is at 10 recipients per hour, cool down.', $result->reason);
- // normally, request #10 would get blocked
- $result = RateLimit::verifyRequest($this->domainUsers[0], ['0010@test.domain']);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
-
- // requests 11 through 26
- for ($i = 11; $i <= 26; $i++) {
- $result = RateLimit::verifyRequest($this->domainUsers[0], [sprintf("%04d@test.domain", $i)]);
- $this->assertSame(200, $result->code);
- $this->assertSame(Response::ACTION_DUNNO, $result->action);
- $this->assertSame('', $result->reason);
- }
+ $this->domainOwner->refresh();
+ $this->assertTrue($this->domainOwner->isSuspended());
+ $this->joe->refresh();
+ $this->assertTrue($this->joe->isSuspended());
+ $this->jack->refresh();
+ $this->assertTrue($this->jack->isSuspended());
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 4:09 PM (17 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18777951
Default Alt Text
D5865.1775232543.diff (42 KB)
Attached To
Mode
D5865: Rework Rate Limiting
Attached
Detach File
Event Timeline