Page MenuHomePhorge

D5220.1775348666.diff
No OneTemporary

Authored By
Unknown
Size
106 KB
Referenced Files
None
Subscribers
None

D5220.1775348666.diff

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

Mime Type
text/plain
Expires
Sun, Apr 5, 12:24 AM (18 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18794589
Default Alt Text
D5220.1775348666.diff (106 KB)

Event Timeline