Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117888699
D5220.1775359751.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
106 KB
Referenced Files
None
Subscribers
None
D5220.1775359751.diff
View Options
diff --git a/src/app/Console/Commands/Policy/RateLimit/Whitelist/CreateCommand.php b/src/app/Console/Commands/Policy/RateLimit/Whitelist/CreateCommand.php
--- a/src/app/Console/Commands/Policy/RateLimit/Whitelist/CreateCommand.php
+++ b/src/app/Console/Commands/Policy/RateLimit/Whitelist/CreateCommand.php
@@ -51,7 +51,7 @@
$type = \App\User::class;
}
- \App\Policy\RateLimitWhitelist::create(
+ \App\Policy\RateLimit\Whitelist::create(
[
'whitelistable_id' => $id,
'whitelistable_type' => $type
diff --git a/src/app/Console/Commands/Policy/RateLimit/Whitelist/DeleteCommand.php b/src/app/Console/Commands/Policy/RateLimit/Whitelist/DeleteCommand.php
--- a/src/app/Console/Commands/Policy/RateLimit/Whitelist/DeleteCommand.php
+++ b/src/app/Console/Commands/Policy/RateLimit/Whitelist/DeleteCommand.php
@@ -51,7 +51,7 @@
$type = \App\User::class;
}
- \App\Policy\RateLimitWhitelist::where(
+ \App\Policy\RateLimit\Whitelist::where(
[
'whitelistable_id' => $id,
'whitelistable_type' => $type
diff --git a/src/app/Console/Commands/Policy/RateLimit/Whitelist/ReadCommand.php b/src/app/Console/Commands/Policy/RateLimit/Whitelist/ReadCommand.php
--- a/src/app/Console/Commands/Policy/RateLimit/Whitelist/ReadCommand.php
+++ b/src/app/Console/Commands/Policy/RateLimit/Whitelist/ReadCommand.php
@@ -27,7 +27,7 @@
*/
public function handle()
{
- \App\Policy\RateLimitWhitelist::each(
+ \App\Policy\RateLimit\Whitelist::each(
function ($item) {
$whitelistable = $item->whitelistable;
diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php
--- a/src/app/Http/Controllers/API/V4/PolicyController.php
+++ b/src/app/Http/Controllers/API/V4/PolicyController.php
@@ -3,13 +3,11 @@
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
-use App\Policy\Mailfilter\RequestHandler as Mailfilter;
+use App\Policy\Greylist;
+use App\Policy\Mailfilter;
use App\Policy\RateLimit;
-use App\Policy\RateLimitWhitelist;
-use App\Transaction;
+use App\Policy\SPF;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Validator;
class PolicyController extends Controller
{
@@ -20,9 +18,7 @@
*/
public function greylist()
{
- $data = \request()->input();
-
- $request = new \App\Policy\Greylist\Request($data);
+ $request = new Greylist(\request()->input());
$shouldDefer = $request->shouldDefer();
@@ -76,185 +72,25 @@
return response()->json(['response' => 'DUNNO'], 200);
}
- //
- // Examine the individual sender
- //
+ // Find the Kolab user
$user = \App\User::withTrashed()->where('email', $sender)->first();
if (!$user) {
$alias = \App\UserAlias::where('alias', $sender)->first();
if (!$alias) {
- // external sender through where this policy is applied
- return response()->json(['response' => 'DUNNO'], 200);
- }
-
- $user = $alias->user;
- }
-
- if (empty($user) || $user->trashed() || $user->isSuspended()) {
- // use HOLD, so that it is silent (as opposed to REJECT)
- return response()->json(['response' => 'HOLD', 'reason' => 'Sender deleted or suspended'], 403);
- }
-
- //
- // Examine the domain
- //
- $domain = \App\Domain::withTrashed()->where('namespace', $domain)->first();
-
- if (!$domain) {
- // external sender through where this policy is applied
- return response()->json(['response' => 'DUNNO'], 200);
- }
-
- if ($domain->trashed() || $domain->isSuspended()) {
- // use HOLD, so that it is silent (as opposed to REJECT)
- return response()->json(['response' => 'HOLD', 'reason' => 'Sender domain deleted or suspended'], 403);
- }
-
- // see if the user or domain is whitelisted
- // use ./artisan policy:ratelimit:whitelist:create <email|namespace>
- if (RateLimitWhitelist::isListed($user) || RateLimitWhitelist::isListed($domain)) {
- return response()->json(['response' => 'DUNNO'], 200);
- }
-
- // user nor domain whitelisted, continue scrutinizing the request
- $recipients = (array)$data['recipients'];
- sort($recipients);
-
- $recipientCount = count($recipients);
- $recipientHash = hash('sha256', implode(',', $recipients));
-
- //
- // Retrieve the wallet to get to the owner
- //
- $wallet = $user->wallet();
-
- // wait, there is no wallet?
- if (!$wallet || !$wallet->owner) {
- return response()->json(['response' => 'HOLD', 'reason' => 'Sender without a wallet'], 403);
- }
-
- $owner = $wallet->owner;
-
- // find or create the request
- $request = RateLimit::where('recipient_hash', $recipientHash)
- ->where('user_id', $user->id)
- ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour())
- ->first();
-
- if (!$request) {
- $request = RateLimit::create([
- 'user_id' => $user->id,
- 'owner_id' => $owner->id,
- 'recipient_hash' => $recipientHash,
- 'recipient_count' => $recipientCount
- ]);
- } else {
- // ensure the request has an up to date timestamp
- $request->updated_at = \Carbon\Carbon::now();
- $request->save();
- }
+ // TODO: How about sender is a distlist address?
- // exempt owners that have 100% discount.
- if ($wallet->discount && $wallet->discount->discount == 100) {
- return response()->json(['response' => 'DUNNO'], 200);
- }
-
- // 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) {
+ // external sender through where this policy is applied
return response()->json(['response' => 'DUNNO'], 200);
}
- }
-
- //
- // Examine the rates at which the owner (or its users) is sending
- //
- $ownerRates = RateLimit::where('owner_id', $owner->id)
- ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour());
-
- if (($count = $ownerRates->count()) >= 10) {
- $result = [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'The account is at 10 messages per hour, cool down.'
- ];
-
- // automatically suspend (recursively) if 2.5 times over the original limit and younger than two months
- $ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($count >= 25 && $owner->created_at > $ageThreshold) {
- $owner->suspendAccount();
- }
- return response()->json($result, 403);
+ $user = $alias->user()->withTrashed()->first();
}
- if (($recipientCount = $ownerRates->sum('recipient_count')) >= 100) {
- $result = [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'The account is at 100 recipients per hour, cool down.'
- ];
-
- // automatically suspend if 2.5 times over the original limit and younger than two months
- $ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($recipientCount >= 250 && $owner->created_at > $ageThreshold) {
- $owner->suspendAccount();
- }
+ $result = RateLimit::verifyRequest($user, (array) $data['recipients']);
- return response()->json($result, 403);
- }
-
- //
- // Examine the rates at which the user is sending (if not also the owner)
- //
- if ($user->id != $owner->id) {
- $userRates = RateLimit::where('user_id', $user->id)
- ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour());
-
- if (($count = $userRates->count()) >= 10) {
- $result = [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'User is at 10 messages per hour, cool down.'
- ];
-
- // automatically suspend if 2.5 times over the original limit and younger than two months
- $ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($count >= 25 && $user->created_at > $ageThreshold) {
- $user->suspend();
- }
-
- return response()->json($result, 403);
- }
-
- if (($recipientCount = $userRates->sum('recipient_count')) >= 100) {
- $result = [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'User is at 100 recipients per hour, cool down.'
- ];
-
- // automatically suspend if 2.5 times over the original limit
- $ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
-
- if ($recipientCount >= 250 && $user->created_at > $ageThreshold) {
- $user->suspend();
- }
-
- return response()->json($result, 403);
- }
- }
-
- return response()->json(['response' => 'DUNNO'], 200);
+ return $result->jsonResponse();
}
/*
@@ -264,153 +100,8 @@
*/
public function senderPolicyFramework()
{
- $data = \request()->input();
-
- if (!array_key_exists('client_address', $data)) {
- \Log::error("SPF: Request without client_address: " . json_encode($data));
-
- return response()->json(
- [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'Temporary error. Please try again later.',
- 'log' => ["SPF: Request without client_address: " . json_encode($data)]
- ],
- 403
- );
- }
-
- list($netID, $netType) = \App\Utils::getNetFromAddress($data['client_address']);
-
- // This network can not be recognized.
- if (!$netID) {
- \Log::error("SPF: Request without recognizable network: " . json_encode($data));
-
- return response()->json(
- [
- 'response' => 'DEFER_IF_PERMIT',
- 'reason' => 'Temporary error. Please try again later.',
- 'log' => ["SPF: Request without recognizable network: " . json_encode($data)]
- ],
- 403
- );
- }
-
- $senderLocal = 'unknown';
- $senderDomain = 'unknown';
-
- if (strpos($data['sender'], '@') !== false) {
- list($senderLocal, $senderDomain) = explode('@', $data['sender']);
-
- if (strlen($senderLocal) >= 255) {
- $senderLocal = substr($senderLocal, 0, 255);
- }
- }
-
- if ($data['sender'] === null) {
- $data['sender'] = '';
- }
-
- // Compose the cache key we want.
- $cacheKey = "{$netType}_{$netID}_{$senderDomain}";
-
- $result = \App\Policy\SPF\Cache::get($cacheKey);
-
- if (!$result) {
- $environment = new \SPFLib\Check\Environment(
- $data['client_address'],
- $data['client_name'],
- $data['sender']
- );
-
- $result = (new \SPFLib\Checker())->check($environment);
+ $response = SPF::handle(\request()->input());
- \App\Policy\SPF\Cache::set($cacheKey, serialize($result));
- } else {
- $result = unserialize($result);
- }
-
- $fail = false;
- $prependSPF = '';
-
- switch ($result->getCode()) {
- case \SPFLib\Check\Result::CODE_ERROR_PERMANENT:
- $fail = true;
- $prependSPF = "Received-SPF: Permerror";
- break;
-
- case \SPFLib\Check\Result::CODE_ERROR_TEMPORARY:
- $prependSPF = "Received-SPF: Temperror";
- break;
-
- case \SPFLib\Check\Result::CODE_FAIL:
- $fail = true;
- $prependSPF = "Received-SPF: Fail";
- break;
-
- case \SPFLib\Check\Result::CODE_SOFTFAIL:
- $prependSPF = "Received-SPF: Softfail";
- break;
-
- case \SPFLib\Check\Result::CODE_NEUTRAL:
- $prependSPF = "Received-SPF: Neutral";
- break;
-
- case \SPFLib\Check\Result::CODE_PASS:
- $prependSPF = "Received-SPF: Pass";
- break;
-
- case \SPFLib\Check\Result::CODE_NONE:
- $prependSPF = "Received-SPF: None";
- break;
- }
-
- $prependSPF .= " identity=mailfrom;";
- $prependSPF .= " client-ip={$data['client_address']};";
- $prependSPF .= " helo={$data['client_name']};";
- $prependSPF .= " envelope-from={$data['sender']};";
-
- if ($fail) {
- // TODO: check the recipient's policy, such as using barracuda for anti-spam and anti-virus as a relay for
- // inbound mail to a local recipient address.
- $objects = null;
- if (array_key_exists('recipient', $data)) {
- $objects = \App\Utils::findObjectsByRecipientAddress($data['recipient']);
- }
-
- if (!empty($objects)) {
- // check if any of the recipient objects have whitelisted the helo, first one wins.
- foreach ($objects as $object) {
- if (method_exists($object, 'senderPolicyFrameworkWhitelist')) {
- $result = $object->senderPolicyFrameworkWhitelist($data['client_name']);
-
- if ($result) {
- $response = [
- 'response' => 'DUNNO',
- 'prepend' => ["Received-SPF: Pass Check skipped at recipient's discretion"],
- 'reason' => 'HELO name whitelisted'
- ];
-
- return response()->json($response, 200);
- }
- }
- }
- }
-
- $result = [
- 'response' => 'REJECT',
- 'prepend' => [$prependSPF],
- 'reason' => "Prohibited by Sender Policy Framework"
- ];
-
- return response()->json($result, 403);
- }
-
- $result = [
- 'response' => 'DUNNO',
- 'prepend' => [$prependSPF],
- 'reason' => "Don't know"
- ];
-
- return response()->json($result, 200);
+ return $response->jsonResponse();
}
}
diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php
--- a/src/app/Observers/DomainObserver.php
+++ b/src/app/Observers/DomainObserver.php
@@ -63,7 +63,7 @@
*/
public function deleting(Domain $domain)
{
- \App\Policy\RateLimitWhitelist::where(
+ \App\Policy\RateLimit\Whitelist::where(
[
'whitelistable_id' => $domain->id,
'whitelistable_type' => Domain::class
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -276,7 +276,7 @@
}
// regardless of force delete, we're always purging whitelists... just in case
- \App\Policy\RateLimitWhitelist::where(
+ \App\Policy\RateLimit\Whitelist::where(
[
'whitelistable_id' => $user->id,
'whitelistable_type' => User::class
diff --git a/src/app/Policy/Greylist/Request.php b/src/app/Policy/Greylist.php
rename from src/app/Policy/Greylist/Request.php
rename to src/app/Policy/Greylist.php
--- a/src/app/Policy/Greylist/Request.php
+++ b/src/app/Policy/Greylist.php
@@ -1,10 +1,12 @@
<?php
-namespace App\Policy\Greylist;
+namespace App\Policy;
+use App\Policy\Greylist\Connect;
+use App\Policy\Greylist\Whitelist;
use Illuminate\Support\Facades\DB;
-class Request
+class Greylist
{
protected $connect;
protected $header;
@@ -84,7 +86,7 @@
$recipient = $this->recipientFromRequest();
- $this->sender = $this->senderFromRequest();
+ $this->sender = \App\Utils::normalizeAddress($this->request['sender']);
if (strpos($this->sender, '@') !== false) {
list($this->senderLocal, $this->senderDomain) = explode('@', $this->sender);
@@ -243,9 +245,4 @@
return $recipient;
}
-
- protected function senderFromRequest()
- {
- return \App\Utils::normalizeAddress($this->request['sender']);
- }
}
diff --git a/src/app/Policy/Mailfilter/RequestHandler.php b/src/app/Policy/Mailfilter.php
rename from src/app/Policy/Mailfilter/RequestHandler.php
rename to src/app/Policy/Mailfilter.php
--- a/src/app/Policy/Mailfilter/RequestHandler.php
+++ b/src/app/Policy/Mailfilter.php
@@ -1,12 +1,15 @@
<?php
-namespace App\Policy\Mailfilter;
+namespace App\Policy;
+use App\Policy\Mailfilter\MailParser;
+use App\Policy\Mailfilter\Modules;
+use App\Policy\Mailfilter\Result;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
-class RequestHandler
+class Mailfilter
{
/**
* SMTP Content Filter
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,6 +2,9 @@
namespace App\Policy;
+use App\Transaction;
+use App\User;
+use Carbon\Carbon;
use App\Traits\BelongsToUserTrait;
use Illuminate\Database\Eloquent\Model;
@@ -19,4 +22,166 @@
/** @var string Database table name */
protected $table = 'policy_ratelimit';
+
+
+ /**
+ * Check the submission request agains rate limits
+ *
+ * @param User $user Sender user
+ * @param array $recipients List of mail recipients
+ *
+ * @return Response Policy respone
+ */
+ public static function verifyRequest(User $user, array $recipients = []): Response
+ {
+ if ($user->trashed() || $user->isSuspended()) {
+ // use HOLD, so that it is silent (as opposed to REJECT)
+ return new Response(Response::ACTION_HOLD, 'Sender deleted or suspended', 403);
+ }
+
+ // Examine the domain
+ $domain = $user->domain();
+
+ if (!$domain) {
+ // external sender through where this policy is applied
+ return new Response(); // DUNNO
+ }
+
+ if ($domain->trashed() || $domain->isSuspended()) {
+ // use HOLD, so that it is silent (as opposed to REJECT)
+ return new Response(Response::ACTION_HOLD, 'Sender domain deleted or suspended', 403);
+ }
+
+ // see if the user or domain is whitelisted
+ // use ./artisan policy:ratelimit:whitelist:create <email|namespace>
+ if (RateLimit\Whitelist::isListed($user) || RateLimit\Whitelist::isListed($domain)) {
+ return new Response(); // 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();
+
+ // wait, there is no wallet?
+ if (!$wallet || !$wallet->owner) {
+ return new Response(Response::ACTION_HOLD, 'Sender without a wallet', 403);
+ }
+
+ $owner = $wallet->owner;
+
+ // find or create the request
+ $request = RateLimit::where('recipient_hash', $recipientHash)
+ ->where('user_id', $user->id)
+ ->where('updated_at', '>=', Carbon::now()->subHour())
+ ->first();
+
+ if (!$request) {
+ $request = RateLimit::create([
+ 'user_id' => $user->id,
+ 'owner_id' => $owner->id,
+ 'recipient_hash' => $recipientHash,
+ 'recipient_count' => $recipientCount
+ ]);
+ } else {
+ // ensure the request has an up to date timestamp
+ $request->updated_at = Carbon::now();
+ $request->save();
+ }
+
+ // exempt owners that have 100% discount.
+ if ($wallet->discount && $wallet->discount->discount == 100) {
+ return new Response(); // DUNNO
+ }
+
+ // 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();
+ }
+ }
+
+ // Examine the rates at which the owner (or its users) is sending
+ $ownerRates = RateLimit::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) {
+ $owner->suspendAccount();
+ }
+
+ return new Response(
+ Response::ACTION_DEFER_IF_PERMIT,
+ 'The account is at 10 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) {
+ $owner->suspendAccount();
+ }
+
+ return new Response(
+ Response::ACTION_DEFER_IF_PERMIT,
+ 'The account is at 100 recipients per hour, cool down.',
+ 403
+ );
+ }
+
+ // Examine the rates at which the user is sending (if not also the owner)
+ if ($user->id != $owner->id) {
+ $userRates = RateLimit::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) {
+ $user->suspend();
+ }
+
+ return new Response(
+ Response::ACTION_DEFER_IF_PERMIT,
+ 'User is at 10 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) {
+ $user->suspend();
+ }
+
+ return new Response(
+ Response::ACTION_DEFER_IF_PERMIT,
+ 'The account is at 100 recipients per hour, cool down.',
+ 403
+ );
+ }
+ }
+
+ return new Response(); // DUNNO
+ }
}
diff --git a/src/app/Policy/RateLimitWhitelist.php b/src/app/Policy/RateLimit/Whitelist.php
rename from src/app/Policy/RateLimitWhitelist.php
rename to src/app/Policy/RateLimit/Whitelist.php
--- a/src/app/Policy/RateLimitWhitelist.php
+++ b/src/app/Policy/RateLimit/Whitelist.php
@@ -1,17 +1,17 @@
<?php
-namespace App\Policy;
+namespace App\Policy\RateLimit;
use Illuminate\Database\Eloquent\Model;
/**
- * The eloquent definition of a RateLimitWhitelist entry.
+ * The eloquent definition of a RateLimit Whitelist entry.
*
* @property ?object $whitelistable The whitelistable object
* @property int|string $whitelistable_id The whitelistable object identifier
* @property string $whitelistable_type The whitelistable object type
*/
-class RateLimitWhitelist extends Model
+class Whitelist extends Model
{
/** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
diff --git a/src/app/Policy/Response.php b/src/app/Policy/Response.php
new file mode 100644
--- /dev/null
+++ b/src/app/Policy/Response.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Policy;
+
+use Illuminate\Http\JsonResponse;
+
+class Response
+{
+ public const ACTION_DEFER_IF_PERMIT = 'DEFER_IF_PERMIT';
+ public const ACTION_DUNNO = 'DUNNO';
+ public const ACTION_HOLD = 'HOLD';
+ public const ACTION_REJECT = 'REJECT';
+
+ /** @var string Postfix action */
+ public string $action = self::ACTION_DUNNO;
+
+ /** @var string Optional response reason message */
+ public string $reason = '';
+
+ /** @var int HTTP response code */
+ public int $code = 200;
+
+ /** @var array<string> Log entries */
+ public array $logs = [];
+
+ /** @var array<string> Headers to prepend */
+ public array $prepends = [];
+
+
+ /**
+ * Object constructor
+ *
+ * @param string $action Action to take on the Postfix side
+ * @param string $reason Optional reason for the action
+ * @param int $code HTTP response code
+ */
+ public function __construct($action = self::ACTION_DUNNO, $reason = '', $code = 200)
+ {
+ $this->action = $action;
+ $this->reason = $reason;
+ $this->code = $code;
+ }
+
+ /**
+ * Convert this object into a JSON response
+ */
+ public function jsonResponse(): JsonResponse
+ {
+ $response = [
+ 'response' => $this->action,
+ ];
+
+ if ($this->reason) {
+ $response['reason'] = $this->reason;
+ }
+
+ if (!empty($this->logs)) {
+ $response['log'] = $this->logs;
+ }
+
+ if (!empty($this->prepends)) {
+ $response['prepend'] = $this->prepends;
+ }
+
+ return response()->json($response, $this->code);
+ }
+}
diff --git a/src/app/Policy/SPF.php b/src/app/Policy/SPF.php
new file mode 100644
--- /dev/null
+++ b/src/app/Policy/SPF.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace App\Policy;
+
+use App\Policy\SPF\Cache;
+
+class SPF
+{
+ /**
+ * Handle a policy request
+ *
+ * @param array $data Input data (client_address, client_name, sender, recipient)
+ */
+ public static function handle($data): Response
+ {
+ if (!array_key_exists('client_address', $data)) {
+ \Log::error("SPF: Request without client_address: " . json_encode($data));
+
+ $response = new Response(Response::ACTION_DEFER_IF_PERMIT, 'Temporary error. Please try again later.', 403);
+ $response->logs[] = "SPF: Request without client_address: " . json_encode($data);
+
+ return $response;
+ }
+
+ list($netID, $netType) = \App\Utils::getNetFromAddress($data['client_address']);
+
+ // This network can not be recognized.
+ if (!$netID) {
+ \Log::error("SPF: Request without recognizable network: " . json_encode($data));
+
+ $response = new Response(Response::ACTION_DEFER_IF_PERMIT, 'Temporary error. Please try again later.', 403);
+ $response->logs[] = "SPF: Request without recognizable network: " . json_encode($data);
+
+ return $response;
+ }
+
+ $senderLocal = 'unknown';
+ $senderDomain = 'unknown';
+
+ if (!isset($data['sender'])) {
+ $data['sender'] = '';
+ }
+
+ if (strpos($data['sender'], '@') !== false) {
+ list($senderLocal, $senderDomain) = explode('@', $data['sender']);
+
+ if (strlen($senderLocal) >= 255) {
+ $senderLocal = substr($senderLocal, 0, 255);
+ }
+ }
+
+ // Compose the cache key we want
+ $cacheKey = "{$netType}_{$netID}_{$senderDomain}";
+
+ $result = Cache::get($cacheKey);
+
+ if (!$result) {
+ $environment = new \SPFLib\Check\Environment(
+ $data['client_address'],
+ $data['client_name'],
+ $data['sender']
+ );
+
+ $result = (new \SPFLib\Checker())->check($environment);
+
+ Cache::set($cacheKey, serialize($result));
+ } else {
+ $result = unserialize($result);
+ }
+
+ $fail = false;
+ $prependSPF = '';
+
+ switch ($result->getCode()) {
+ case \SPFLib\Check\Result::CODE_ERROR_PERMANENT:
+ $fail = true;
+ $prependSPF = 'Received-SPF: Permerror';
+ break;
+
+ case \SPFLib\Check\Result::CODE_ERROR_TEMPORARY:
+ $prependSPF = 'Received-SPF: Temperror';
+ break;
+
+ case \SPFLib\Check\Result::CODE_FAIL:
+ $fail = true;
+ $prependSPF = 'Received-SPF: Fail';
+ break;
+
+ case \SPFLib\Check\Result::CODE_SOFTFAIL:
+ $prependSPF = 'Received-SPF: Softfail';
+ break;
+
+ case \SPFLib\Check\Result::CODE_NEUTRAL:
+ $prependSPF = 'Received-SPF: Neutral';
+ break;
+
+ case \SPFLib\Check\Result::CODE_PASS:
+ $prependSPF = 'Received-SPF: Pass';
+ break;
+
+ case \SPFLib\Check\Result::CODE_NONE:
+ $prependSPF = 'Received-SPF: None';
+ break;
+ }
+
+ $prependSPF .= " identity=mailfrom;"
+ . " client-ip={$data['client_address']};"
+ . " helo={$data['client_name']};"
+ . " envelope-from={$data['sender']};";
+
+ if ($fail) {
+ // TODO: check the recipient's policy, such as using barracuda for anti-spam and anti-virus as a relay for
+ // inbound mail to a local recipient address.
+ $objects = null;
+ if (array_key_exists('recipient', $data)) {
+ $objects = \App\Utils::findObjectsByRecipientAddress($data['recipient']);
+ }
+
+ if (!empty($objects)) {
+ // check if any of the recipient objects have whitelisted the helo, first one wins.
+ foreach ($objects as $object) {
+ if (method_exists($object, 'senderPolicyFrameworkWhitelist')) {
+ $result = $object->senderPolicyFrameworkWhitelist($data['client_name']);
+
+ if ($result) {
+ $response = new Response(Response::ACTION_DUNNO, 'HELO name whitelisted');
+ $response->prepends[] = "Received-SPF: Pass Check skipped at recipient's discretion";
+
+ return $response;
+ }
+ }
+ }
+ }
+
+ $response = new Response(Response::ACTION_REJECT, 'Prohibited by Sender Policy Framework', 403);
+ $response->prepends[] = $prependSPF;
+
+ return $response;
+ }
+
+ $response = new Response(Response::ACTION_DUNNO);
+ $response->prepends[] = $prependSPF;
+
+ return $response;
+ }
+}
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
@@ -56,8 +56,6 @@
/**
* Test greylist policy webhook
- *
- * @group greylist
*/
public function testGreylist()
{
@@ -125,4 +123,98 @@
// TODO: Test two modules that both modify the mail content
$this->markTestIncomplete();
}
+
+ /**
+ * Test ratelimit policy webhook
+ */
+ public function testRatelimit()
+ {
+ // Note: Only basic tests here. More detailed policy handler tests are in another place
+
+ // Test a valid user
+ $post = [
+ 'sender' => $this->testUser->email,
+ 'recipients' => 'someone@sender.domain',
+ ];
+
+ $response = $this->post('/api/webhooks/policy/ratelimit', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('DUNNO', $json['response']);
+
+ // Test invalid sender
+ $post['sender'] = 'non-existing';
+ $response = $this->post('/api/webhooks/policy/ratelimit', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('HOLD', $json['response']);
+ $this->assertSame('Invalid sender email', $json['reason']);
+
+ // Test unknown sender
+ $post['sender'] = 'non-existing@example.com';
+ $response = $this->post('/api/webhooks/policy/ratelimit', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('DUNNO', $json['response']);
+
+ // Test alias sender
+ $this->testUser->suspend();
+ $this->testUser->aliases()->create(['alias' => 'alias@test.domain']);
+ $post['sender'] = 'alias@test.domain';
+ $response = $this->post('/api/webhooks/policy/ratelimit', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('HOLD', $json['response']);
+ $this->assertSame('Sender deleted or suspended', $json['reason']);
+
+ // Test app.ratelimit_whitelist
+ \config(['app.ratelimit_whitelist' => ['alias@test.domain']]);
+ $response = $this->post('/api/webhooks/policy/ratelimit', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('DUNNO', $json['response']);
+ }
+
+ /**
+ * Test SPF webhook
+ *
+ * @group data
+ * @group skipci
+ */
+ public function testSenderPolicyFramework(): void
+ {
+ // Note: Only basic tests here. More detailed policy handler tests are in another place
+
+ // TODO: Make a test that does not depend on data/dns (remove skipci)
+
+ // Test a valid user
+ $post = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-fail.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email
+ ];
+
+ $response = $this->post('/api/webhooks/policy/spf', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('REJECT', $json['response']);
+ $this->assertSame('Prohibited by Sender Policy Framework', $json['reason']);
+ $this->assertSame(['Received-SPF: Fail identity=mailfrom; client-ip=212.103.80.148;'
+ . ' helo=mx.kolabnow.com; envelope-from=sender@spf-fail.kolab.org;'], $json['prepend']);
+ }
}
diff --git a/src/tests/Feature/Stories/GreylistTest.php b/src/tests/Feature/Policy/GreylistTest.php
rename from src/tests/Feature/Stories/GreylistTest.php
rename to src/tests/Feature/Policy/GreylistTest.php
--- a/src/tests/Feature/Stories/GreylistTest.php
+++ b/src/tests/Feature/Policy/GreylistTest.php
@@ -1,15 +1,13 @@
<?php
-namespace Tests\Feature\Stories;
+namespace Tests\Feature\Policy;
use App\Domain;
use App\Policy\Greylist;
-use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* @group data
- * @group greylist
*/
class GreylistTest extends TestCase
{
@@ -46,71 +44,44 @@
parent::tearDown();
}
- public function testWithTimestamp()
- {
- $request = new Greylist\Request(
- [
- 'sender' => 'someone@sender.domain',
- 'recipient' => $this->domainOwner->email,
- 'client_address' => $this->clientAddress,
- 'client_name' => 'some.mx',
- 'timestamp' => \Carbon\Carbon::now()->subDays(7)->toString()
- ]
- );
-
- $timestamp = $this->getObjectProperty($request, 'timestamp');
-
- $this->assertTrue(
- \Carbon\Carbon::parse($timestamp, 'UTC') < \Carbon\Carbon::now()
- );
- }
-
- public function testNoNet()
+ /**
+ * Test shouldDefer() method
+ */
+ public function testShouldDefer()
{
- $request = new Greylist\Request(
- [
+ // Test no net
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => '127.128.129.130',
'client_name' => 'some.mx'
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
- }
- public function testIp6Net()
- {
- $request = new Greylist\Request(
- [
+ // Test IPv6 net
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => '2a00:1450:400a:803::2005',
'client_name' => 'some.mx'
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
- }
- // public function testMultiRecipientThroughAlias() {}
-
- public function testWhitelistNew()
- {
+ // Test a new whitelist
$whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
$this->assertNull($whitelist);
for ($i = 0; $i < 5; $i++) {
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => "someone{$i}@sender.domain",
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress,
'client_name' => 'some.mx',
'timestamp' => \Carbon\Carbon::now()->subDays(1)
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
}
@@ -119,37 +90,28 @@
$this->assertNotNull($whitelist);
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => "someone5@sender.domain",
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress,
'client_name' => 'some.mx',
'timestamp' => \Carbon\Carbon::now()->subDays(1)
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
- }
-
- // public function testWhitelistedHit() {}
-
- public function testWhitelistStale()
- {
- $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
- $this->assertNull($whitelist);
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+ Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
+ // Test a stale whitelist
for ($i = 0; $i < 5; $i++) {
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => "someone{$i}@sender.domain",
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress,
'client_name' => 'some.mx',
'timestamp' => \Carbon\Carbon::now()->subDays(1)
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
}
@@ -158,15 +120,13 @@
$this->assertNotNull($whitelist);
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => "someone5@sender.domain",
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress,
'client_name' => 'some.mx',
'timestamp' => \Carbon\Carbon::now()->subDays(1)
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
@@ -174,14 +134,12 @@
$whitelist->save(['timestamps' => false]);
$this->assertTrue($request->shouldDefer());
- }
- // public function testWhitelistUpdate() {}
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+ Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
- public function testRetry()
- {
- $connect = Greylist\Connect::create(
- [
+ // test retry
+ $connect = Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $this->domainOwner->email),
@@ -190,27 +148,23 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
$connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
$connect->save();
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
- }
- public function testInvalidRecipient()
- {
- $connect = Greylist\Connect::create(
- [
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+
+ // Test invalid recipient
+ $connect = Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $this->domainOwner->email),
@@ -219,24 +173,20 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => 'not.someone@that.exists',
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
- }
- public function testUserDisabled()
- {
- $connect = Greylist\Connect::create(
- [
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+
+ // Test user disabled
+ $connect = Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $this->domainOwner->email),
@@ -245,39 +195,33 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
$this->domainOwner->setSetting('greylist_enabled', 'false');
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
// Ensure we also find the setting by alias
$this->domainOwner->setAliases(['alias1@test2.domain2']);
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => 'alias1@test2.domain2',
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
- }
- public function testUserEnabled()
- {
- $connect = Greylist\Connect::create(
- [
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+
+ // Test user enabled
+ $connect = Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $this->domainOwner->email),
@@ -286,18 +230,15 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
$this->domainOwner->setSetting('greylist_enabled', 'true');
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertTrue($request->shouldDefer());
@@ -305,26 +246,62 @@
$connect->save();
$this->assertFalse($request->shouldDefer());
+
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+
+ // Test controller new
+ $request = new Greylist([
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress,
+ 'client_name' => 'some.mx'
+ ]);
+
+ $this->assertTrue($request->shouldDefer());
+
+ Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
+ Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
+
+ // test controller not new
+ $connect = Greylist\Connect::create([
+ 'sender_local' => 'someone',
+ 'sender_domain' => 'sender.domain',
+ 'recipient_hash' => hash('sha256', $this->domainOwner->email),
+ 'recipient_id' => $this->domainOwner->id,
+ 'recipient_type' => \App\User::class,
+ 'connect_count' => 1,
+ 'net_id' => $this->net->id,
+ 'net_type' => \App\IP4Net::class
+ ]);
+
+ $connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $request = new Greylist([
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress,
+ 'client_name' => 'some.mx'
+ ]);
+
+ $this->assertFalse($request->shouldDefer());
}
/**
- * @group slow
+ * Test shouldDefer() for multiple users case
*/
public function testMultipleUsersAllDisabled()
{
$this->setUpTest();
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
foreach ($this->domainUsers as $user) {
- Greylist\Connect::create(
- [
+ Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $user->email),
@@ -333,8 +310,7 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
$user->setSetting('greylist_enabled', 'false');
@@ -342,36 +318,31 @@
continue;
}
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $user->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
$this->assertFalse($request->shouldDefer());
}
}
/**
- * @group slow
+ * Test shouldDefer() for multiple users case
*/
public function testMultipleUsersAnyEnabled()
{
$this->setUpTest();
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $this->domainOwner->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
foreach ($this->domainUsers as $user) {
- Greylist\Connect::create(
- [
+ Greylist\Connect::create([
'sender_local' => 'someone',
'sender_domain' => 'sender.domain',
'recipient_hash' => hash('sha256', $user->email),
@@ -380,8 +351,7 @@
'connect_count' => 1,
'net_id' => $this->net->id,
'net_type' => \App\IP4Net::class
- ]
- );
+ ]);
$user->setSetting('greylist_enabled', ($user->id == $this->jack->id) ? 'true' : 'false');
@@ -389,13 +359,11 @@
continue;
}
- $request = new Greylist\Request(
- [
+ $request = new Greylist([
'sender' => 'someone@sender.domain',
'recipient' => $user->email,
'client_address' => $this->clientAddress
- ]
- );
+ ]);
if ($user->id == $this->jack->id) {
$this->assertTrue($request->shouldDefer());
@@ -404,44 +372,4 @@
}
}
}
-
- public function testControllerNew()
- {
- $request = new Greylist\Request([
- 'sender' => 'someone@sender.domain',
- 'recipient' => $this->domainOwner->email,
- 'client_address' => $this->clientAddress,
- 'client_name' => 'some.mx'
- ]);
-
- $this->assertTrue($request->shouldDefer());
- }
-
- public function testControllerNotNew()
- {
- $connect = Greylist\Connect::create(
- [
- 'sender_local' => 'someone',
- 'sender_domain' => 'sender.domain',
- 'recipient_hash' => hash('sha256', $this->domainOwner->email),
- 'recipient_id' => $this->domainOwner->id,
- 'recipient_type' => \App\User::class,
- 'connect_count' => 1,
- 'net_id' => $this->net->id,
- 'net_type' => \App\IP4Net::class
- ]
- );
-
- $connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
- $connect->save();
-
- $request = new Greylist\Request([
- 'sender' => 'someone@sender.domain',
- 'recipient' => $this->domainOwner->email,
- 'client_address' => $this->clientAddress,
- 'client_name' => 'some.mx'
- ]);
-
- $this->assertFalse($request->shouldDefer());
- }
}
diff --git a/src/tests/Feature/Policy/RateLimitTest.php b/src/tests/Feature/Policy/RateLimitTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Policy/RateLimitTest.php
@@ -0,0 +1,430 @@
+<?php
+
+namespace Tests\Feature\Policy;
+
+use App\Policy\RateLimit;
+use App\Policy\Response;
+use App\Transaction;
+use App\User;
+use Illuminate\Support\Facades\DB;
+use Tests\TestCase;
+
+/**
+ * @group data
+ */
+class RateLimitTest extends TestCase
+{
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->setUpTest();
+
+ RateLimit::query()->delete();
+ Transaction::query()->delete();
+ }
+
+ public function tearDown(): void
+ {
+ RateLimit::query()->delete();
+ Transaction::query()->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test verifyRequest() method for an individual account cases
+ */
+ 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
+ ]);
+
+ // 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);
+ }
+
+ // 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++) {
+ $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->publicDomainUser->refresh();
+ $this->assertTrue($this->publicDomainUser->isSuspended());
+
+ // Verify a suspended individual can not send an email
+ RateLimit::truncate();
+
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['someone@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();
+
+ // 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 tenth request should be blocked
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['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);
+
+ // Verify a paid for individual account does not simply run out of messages
+ RateLimit::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 tenth request should be blocked
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['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);
+
+ // create a credit transaction
+ $this->publicDomainUser->wallets()->first()->credit(1111);
+
+ // the next request should now be allowed
+ $result = RateLimit::verifyRequest($this->publicDomainUser, ['0010@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(\App\Discount::where('description', 'Free Account')->first());
+ $wallet->save();
+
+ // 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 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);
+
+ // Verify that an individual user in its trial can run out of recipients.
+ RateLimit::truncate();
+ $wallet->discount_id = null;
+ $wallet->balance = 0;
+ $wallet->save();
+
+ // 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);
+ }
+
+ $result = RateLimit::verifyRequest($this->publicDomainUser, $recipients);
+ $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);
+ }
+
+ $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();
+
+ // 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->publicDomainUser, $recipients);
+ $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);
+ }
+
+ $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);
+
+ $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);
+ }
+
+ /**
+ * Test verifyRequest() with group account cases
+ */
+ public function testVerifyRequestGroupAccount()
+ {
+ // 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();
+
+ // first 9 requests
+ for ($i = 0; $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);
+ }
+
+ // 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);
+ }
+
+ // on to the third request, resulting in 102 recipients total
+ $recipients = [];
+ for ($y = 0; $y < 34; $y++) {
+ $recipients[] = sprintf("%04d@test.domain", 2 * $y);
+ }
+
+ $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);
+
+ $this->domainOwner->refresh();
+ $this->assertFalse($this->domainOwner->isSuspended());
+
+ // Verify that a paid for group account can send messages.
+ 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);
+ }
+
+ $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
+ $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);
+ }
+
+ $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);
+
+ $wallet = $this->domainOwner->wallets()->first();
+ $wallet->credit(1111);
+
+ $result = RateLimit::verifyRequest($this->domainOwner, $recipients);
+ $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();
+
+ $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);
+
+ // Verify that the users in a group account can be limited.
+ RateLimit::truncate();
+ $wallet->balance = 0;
+ $wallet->save();
+
+ // 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);
+ }
+
+ // 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);
+
+ // the tenth request from another group user should be rejected
+ $result = RateLimit::verifyRequest($this->domainUsers[1], ['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);
+
+ // 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);
+ }
+
+ $result = RateLimit::verifyRequest($this->domainUsers[0], $recipients);
+ $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);
+ }
+
+ $result = RateLimit::verifyRequest($this->domainUsers[0], $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 a whitelisted group domain is in fact whitelisted
+ RateLimit::truncate();
+ RateLimit\Whitelist::create([
+ 'whitelistable_id' => $this->domainHosted->id,
+ 'whitelistable_type' => \App\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);
+ }
+
+ // 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);
+ }
+ }
+}
diff --git a/src/tests/Feature/Policy/SPFTest.php b/src/tests/Feature/Policy/SPFTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Policy/SPFTest.php
@@ -0,0 +1,271 @@
+<?php
+
+namespace Tests\Feature\Policy;
+
+use App\Domain;
+use App\Policy\SPF;
+use Tests\TestCase;
+
+/**
+ * @group data
+ */
+class SPFTest extends TestCase
+{
+ private $testDomain;
+ private $testUser;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->testDomain = $this->getTestDomain('test.domain', [
+ 'type' => Domain::TYPE_EXTERNAL,
+ 'status' => Domain::STATUS_ACTIVE | Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED
+ ]);
+
+ $this->testUser = $this->getTestUser('john@test.domain');
+ }
+
+ public function tearDown(): void
+ {
+ $this->deleteTestUser($this->testUser->email);
+ $this->deleteTestDomain($this->testDomain->namespace);
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test SPF handle
+ */
+ public function testHandle()
+ {
+ // Test sender fail (IPv6)
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-fail.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ // actually IN AAAA gmail.com.
+ 'client_address' => '2a00:1450:400a:801::2005',
+ 'recipient' => $this->testUser->email
+ ]);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('DEFER_IF_PERMIT', $response->action);
+ $this->assertSame('Temporary error. Please try again later.', $response->reason);
+ $this->assertSPFHeader($response, $data, null);
+
+ // Test none sender
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-none.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Neutral');
+
+ // Test sender no net
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-none.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '256.0.0.1',
+ 'recipient' => $this->testUser->email
+ ]);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('DEFER_IF_PERMIT', $response->action);
+ $this->assertSame('Temporary error. Please try again later.', $response->reason);
+ $this->assertSPFHeader($response, $data, null);
+
+ // Test sender pass
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-pass.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Pass');
+
+ // Test sender pass all
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-passall.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Pass');
+
+ // Test sender relay policy HELO exact negative
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@amazon.co.uk',
+ 'client_name' => 'helo.some.relayservice.domain',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Fail');
+
+ $this->testUser->setSetting('spf_whitelist', json_encode(['the.only.acceptable.helo']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Fail');
+
+ // Test sender relay policy HELO exact positive
+ $this->testUser->setSetting('spf_whitelist', json_encode(['helo.some.relayservice.domain']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('HELO name whitelisted', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Pass', 'Check skipped at recipient\'s discretion');
+
+ // Test sender relay policy regexp negative
+ $this->testUser->setSetting('spf_whitelist', json_encode(['/a\.domain/']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Fail');
+
+ // Test sender relay policy regexp positive
+ $this->testUser->setSetting('spf_whitelist', json_encode(['/relayservice\.domain/']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('HELO name whitelisted', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Pass', 'Check skipped at recipient\'s discretion');
+
+ // Test sender relay policy wildcard subdomain negative
+ $this->testUser->setSetting('spf_whitelist', json_encode(['.helo.some.relayservice.domain']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Fail');
+
+ // Test sender relay policy wildcard subdomain positive
+ $this->testUser->setSetting('spf_whitelist', json_encode(['.some.relayservice.domain']));
+
+ $response = SPF::handle($data);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('HELO name whitelisted', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Pass', 'Check skipped at recipient\'s discretion');
+ }
+
+ /**
+ * @group skipci
+ */
+ public function testHandleSenderErrors(): void
+ {
+ // Test sender temp error
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-temperror.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(200, $response->code);
+ $this->assertSame('DUNNO', $response->action);
+ $this->assertSame('', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Temperror');
+
+ // Test sender permament error
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-permerror.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Permerror');
+
+ // Test sender soft fail
+ $response = SPF::handle($data = [
+ 'instance' => 'test.local.instance',
+ 'protocol_state' => 'RCPT',
+ 'sender' => 'sender@spf-fail.kolab.org',
+ 'client_name' => 'mx.kolabnow.com',
+ 'client_address' => '212.103.80.148',
+ 'recipient' => $this->testUser->email,
+ ]);
+
+ $this->assertSame(403, $response->code);
+ $this->assertSame('REJECT', $response->action);
+ $this->assertSame('Prohibited by Sender Policy Framework', $response->reason);
+ $this->assertSPFHeader($response, $data, 'Fail');
+ }
+
+ /**
+ * Assert Received-SPF header in the response
+ */
+ private function assertSPFHeader($response, $data, $state, $content = ''): void
+ {
+ if (!$state) {
+ $this->assertCount(0, $response->prepends);
+ return;
+ }
+
+ $headers = array_filter($response->prepends, fn ($h) => str_starts_with($h, 'Received-SPF:'));
+ $this->assertCount(1, $headers);
+
+ if ($content) {
+ $expected = "Received-SPF: {$state} {$content}";
+ } else {
+ $expected = sprintf(
+ 'Received-SPF: %s identity=mailfrom; client-ip=%s; helo=%s; envelope-from=%s;',
+ $state,
+ $data['client_address'],
+ $data['client_name'],
+ $data['sender']
+ );
+ }
+
+ $this->assertSame($expected, $headers[0]);
+ }
+}
diff --git a/src/tests/Feature/Stories/RateLimitTest.php b/src/tests/Feature/Stories/RateLimitTest.php
deleted file mode 100644
--- a/src/tests/Feature/Stories/RateLimitTest.php
+++ /dev/null
@@ -1,533 +0,0 @@
-<?php
-
-namespace Tests\Feature\Stories;
-
-use App\Policy\RateLimit;
-use App\Transaction;
-use Illuminate\Support\Facades\DB;
-use Tests\TestCase;
-
-/**
- * @group data
- * @group ratelimit
- */
-class RateLimitTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
-
- $this->setUpTest();
- $this->useServicesUrl();
-
- Transaction::query()->delete();
- }
-
- public function tearDown(): void
- {
- Transaction::query()->delete();
-
- parent::tearDown();
- }
-
- /**
- * Verify an individual can send an email unrestricted, so long as the account is active.
- */
- public function testIndividualDunno()
- {
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => [ 'someone@test.domain' ]
- ];
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- /**
- * Verify a whitelisted individual account is in fact whitelisted
- */
- public function testIndividualWhitelist()
- {
- \App\Policy\RateLimitWhitelist::create(
- [
- 'whitelistable_id' => $this->publicDomainUser->id,
- 'whitelistable_type' => \App\User::class
- ]
- );
-
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => []
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // normally, request #10 would get blocked
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
-
- // requests 11 through 26
- for ($i = 11; $i <= 26; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
- }
-
- /**
- * Verify an individual trial user is automatically suspended.
- */
- public function testIndividualAutoSuspendMessages()
- {
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => []
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // the next 16 requests for 25 total
- for ($i = 10; $i <= 25; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(403);
- }
-
- $this->assertTrue($this->publicDomainUser->fresh()->isSuspended());
- }
-
- /**
- * Verify a suspended individual can not send an email
- */
- public function testIndividualSuspended()
- {
- $this->publicDomainUser->suspend();
-
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => ['someone@test.domain']
- ];
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(403);
- }
-
- /**
- * Verify an individual can run out of messages per hour
- */
- public function testIndividualTrialMessages()
- {
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => []
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // the tenth request should be blocked
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(403);
- }
-
- /**
- * Verify a paid for individual account does not simply run out of messages
- */
- public function testIndividualPaidMessages()
- {
- $wallet = $this->publicDomainUser->wallets()->first();
-
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => ['someone@test.domain']
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // the tenth request should be blocked
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
-
- // create a credit transaction
- $wallet->credit(1111);
-
- // the next request should now be allowed
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
- }
-
- /**
- * Verify a 100% discount for individual account does not simply run out of messages
- */
- public function testIndividualDiscountMessages()
- {
- $wallet = $this->publicDomainUser->wallets()->first();
-
- $wallet->discount()->associate(\App\Discount::where('description', 'Free Account')->first());
- $wallet->save();
-
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => ['someone@test.domain']
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // the tenth request should now be allowed
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
- }
-
- /**
- * Verify that an individual user in its trial can run out of recipients.
- */
- public function testIndividualTrialRecipients()
- {
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => []
- ];
-
- // first 2 requests (34 recipients each)
- for ($x = 1; $x <= 2; $x++) {
- $request['recipients'] = [];
-
- for ($y = 1; $y <= 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // on to the third request, resulting in 102 recipients total
- $request['recipients'] = [];
-
- for ($y = 1; $y <= 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", 3 * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
- }
-
- /**
- * Verify that an individual user that has paid for its account doesn't run out of recipients.
- */
- public function testIndividualPaidRecipients()
- {
- $wallet = $this->publicDomainUser->wallets()->first();
-
- $request = [
- 'sender' => $this->publicDomainUser->email,
- 'recipients' => []
- ];
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // on to the third request, resulting in 102 recipients total
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", 2 * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(403);
-
- $wallet->award(1111);
-
- // the tenth request should now be allowed
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- /**
- * Verify that a group owner can send email
- */
- public function testGroupOwnerDunno()
- {
- $request = [
- 'sender' => $this->domainOwner->email,
- 'recipients' => [ 'someone@test.domain' ]
- ];
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- /**
- * Verify that a domain owner can run out of messages
- */
- public function testGroupTrialOwnerMessages()
- {
- $request = [
- 'sender' => $this->domainOwner->email,
- 'recipients' => []
- ];
-
- // first 9 requests
- for ($i = 0; $i < 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // the tenth request should be blocked
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(403);
-
- $this->assertFalse($this->domainOwner->fresh()->isSuspended());
- }
-
- /**
- * Verify that a domain owner can run out of recipients
- */
- public function testGroupTrialOwnerRecipients()
- {
- $request = [
- 'sender' => $this->domainOwner->email,
- 'recipients' => []
- ];
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // on to the third request, resulting in 102 recipients total
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", 2 * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
-
- $this->assertFalse($this->domainOwner->fresh()->isSuspended());
- }
-
- /**
- * Verify that a paid for group account can send messages.
- */
- public function testGroupPaidOwnerRecipients()
- {
- $wallet = $this->domainOwner->wallets()->first();
-
- $request = [
- 'sender' => $this->domainOwner->email,
- 'recipients' => []
- ];
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // on to the third request, resulting in 102 recipients total
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", 2 * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
-
- $wallet->credit(1111);
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
- }
-
- /**
- * Verify that a user for a domain owner can send email.
- */
- public function testGroupUserDunno()
- {
- $request = [
- 'sender' => $this->domainUsers[0]->email,
- 'recipients' => [ 'someone@test.domain' ]
- ];
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- /**
- * Verify that the users in a group account can be limited.
- */
- public function testGroupTrialUserMessages()
- {
- $user = $this->domainUsers[0];
-
- $request = [
- 'sender' => $user->email,
- 'recipients' => []
- ];
-
- // the first eight requests should be accepted
- for ($i = 0; $i < 8; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
- }
-
- $request['sender'] = $this->domainUsers[1]->email;
-
- // the ninth request from another group user should also be accepted
- $request['recipients'] = ['0009@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
-
- // the tenth request from another group user should be rejected
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
- }
-
- public function testGroupTrialUserRecipients()
- {
- $request = [
- 'sender' => $this->domainUsers[0]->email,
- 'recipients' => []
- ];
-
- // first 2 requests (34 recipients each)
- for ($x = 0; $x < 2; $x++) {
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", $x * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // on to the third request, resulting in 102 recipients total
- $request['recipients'] = [];
-
- for ($y = 0; $y < 34; $y++) {
- $request['recipients'][] = sprintf("%04d@test.domain", 2 * $y);
- }
-
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(403);
- }
-
- /**
- * Verify a whitelisted group domain is in fact whitelisted
- */
- public function testGroupDomainWhitelist()
- {
- \App\Policy\RateLimitWhitelist::create(
- [
- 'whitelistable_id' => $this->domainHosted->id,
- 'whitelistable_type' => \App\Domain::class
- ]
- );
-
- $request = [
- 'sender' => $this->domainUsers[0]->email,
- 'recipients' => []
- ];
-
- // first 9 requests
- for ($i = 1; $i <= 9; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
-
- // normally, request #10 would get blocked
- $request['recipients'] = ['0010@test.domain'];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
- $response->assertStatus(200);
-
- // requests 11 through 26
- for ($i = 11; $i <= 26; $i++) {
- $request['recipients'] = [sprintf("%04d@test.domain", $i)];
- $response = $this->post('api/webhooks/policy/ratelimit', $request);
-
- $response->assertStatus(200);
- }
- }
-}
diff --git a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php
deleted file mode 100644
--- a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-
-namespace Tests\Feature\Stories;
-
-use App\Domain;
-use Tests\TestCase;
-
-/**
- * @group data
- * @group spf
- */
-class SenderPolicyFrameworkTest extends TestCase
-{
- private $testDomain;
- private $testUser;
-
- public function setUp(): void
- {
- parent::setUp();
-
- $this->testDomain = $this->getTestDomain('test.domain', [
- 'type' => Domain::TYPE_EXTERNAL,
- 'status' => Domain::STATUS_ACTIVE | Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED
- ]);
-
- $this->testUser = $this->getTestUser('john@test.domain');
-
- $this->useServicesUrl();
- }
-
- public function tearDown(): void
- {
- $this->deleteTestUser($this->testUser->email);
- $this->deleteTestDomain($this->testDomain->namespace);
-
- parent::tearDown();
- }
-
- /**
- * @group skipci
- */
- public function testSenderFailv4()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-fail.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderFailv6()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-fail.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- // actually IN AAAA gmail.com.
- 'client_address' => '2a00:1450:400a:801::2005',
- 'recipient' => $this->testUser->email
- ];
-
- $this->assertFalse(strpos(':', $data['client_address']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderNone()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-none.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- public function testSenderNoNet()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-none.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '256.0.0.1',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderPass()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-pass.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- public function testSenderPassAll()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-passall.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- /**
- * @group skipci
- */
- public function testSenderPermerror()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-permerror.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- /**
- * @group skipci
- */
- public function testSenderSoftfail()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-fail.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderTemperror()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@spf-temperror.kolab.org',
- 'client_name' => 'mx.kolabnow.com',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- public function testSenderRelayPolicyHeloExactNegative()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['the.only.acceptable.helo']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderRelayPolicyHeloExactPositive()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['helo.some.relayservice.domain']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- public function testSenderRelayPolicyRegexpNegative()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['/a\.domain/']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderRelayPolicyRegexpPositive()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['/relayservice\.domain/']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-
- public function testSenderRelayPolicyWildcardSubdomainNegative()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['.helo.some.relayservice.domain']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
- }
-
- public function testSenderRelayPolicyWildcardSubdomainPositive()
- {
- $data = [
- 'instance' => 'test.local.instance',
- 'protocol_state' => 'RCPT',
- 'sender' => 'sender@amazon.co.uk',
- 'client_name' => 'helo.some.relayservice.domain',
- 'client_address' => '212.103.80.148',
- 'recipient' => $this->testUser->email
- ];
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(403);
-
- $this->testUser->setSetting('spf_whitelist', json_encode(['.some.relayservice.domain']));
-
- $response = $this->post('/api/webhooks/policy/spf', $data);
-
- $response->assertStatus(200);
- }
-}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 5, 3:29 AM (20 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18794589
Default Alt Text
D5220.1775359751.diff (106 KB)
Attached To
Mode
D5220: Policies code refactoring
Attached
Detach File
Event Timeline