Page MenuHomePhorge

D5865.1775178368.diff
No OneTemporary

Authored By
Unknown
Size
42 KB
Referenced Files
None
Subscribers
None

D5865.1775178368.diff

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

Mime Type
text/plain
Expires
Fri, Apr 3, 1:06 AM (21 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18777951
Default Alt Text
D5865.1775178368.diff (42 KB)

Event Timeline